1.背景
近期在工作中负责的项目需要对外提供接口,我们的项目是内部服务,只允许内网访问,首先要讲接口对外暴露出去,让外网可以访问到,这一部分是直接找了公司的运维去完成了。关于接口肯定是需要做鉴权的,普遍我们会选择进行签名,这样就可以保证接口的安全性。
2.思路
一般平台是采用appId+appSecret的方式去对外部服务做授权,双方约定好加密方式,通过验签的方式进行加密
认证。本案例是采用了MD5加密,对 对请求方式+url+请求body+密钥以及时间戳+随机字符串进行加密。
加密方式如下:
请求方式:POST
请求地址:/webApi/v1/test
请求体:根据具体业务填,一般是json格式
秘钥:appSecret,由提供接口方 提供给调用方,用于认证
时间戳:timestamp,调用接口的当前时间,防止攻击
随机字符串:nonce,调用方提供,提高接口安全性
调用方式:
可以把appId,timestamp,nouce,signature放入到请求头中
请求体只放入和业务相关的参数
接口调用方按照这个方式加密,传入一个signature;接口提供方按照这个加密方式也加密一次,然后相比较即可。
1.本项目采用的是springSecurity框架,首先在框架的WebSecurityConfigurerAdapter里面做配置对该接口进行放行
2.然后再编写一个SignInterceptor去实现HandlerInterceptor拦截器
3.实现WebMvcConfigurer中的addInterceptors方法把SignInterceptor注入进去
3.实现
(1)签名方法
public static String getSignature(String method, String url, String body, String timestamp, String nonce, String appSecret){
//加密顺序以及方式自己规定即可
return DigestUtils.md5Hex(method + url + body + appSecret + timestamp + nonce);
}
(2)自定义拦截器
WebConfig是对外开放的一个实体,包含appid,appSecret等字段
WebEnum是对外统一提供的状态码
以上两个自行编写,不再提供
public class SignInterceptor implements HandlerInterceptor {
@Autowired
private RedisUtil redisUtil;
@Autowired
private WebConfigMapper webConfigMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//appId验证
String appId = request.getHeader("appId");
if (StringUtils.isEmpty(appId)) {
returnMsg(response,WebEnum.APPID_EMPTY.getCode(), WebEnum.APPID_EMPTY.getMessage());
return false;
}
WebConfig config = WebConfigMapper.selectConfigByAppId(appId);
String appSecret = config.getAppSecret();
if (StringUtils.isEmpty(appSecret)) {
returnMsg(response, WebEnum.APPID_INVALID.getCode(),WebEnum.APPID_INVALID.getMessage());
return false;
}
//时间戳验证
String timestamp = request.getHeader("timestamp");
if (StringUtils.isEmpty(timestamp)) {
returnMsg(response,WebEnum.TIMESTAMP_EMPTY.getCode(), WebEnum.TIMESTAMP_EMPTY.getMessage());
return false;
}
//大于5分钟,非法请求
long diff = System.currentTimeMillis() - Long.parseLong(timestamp);
if (Math.abs(diff) > 1000 * 60 * 5) {
returnMsg(response,WebEnum.TIMESTAMP_EXPIRED.getCode(),WebEnum.TIMESTAMP_EXPIRED.getMessage());
return false;
}
//随机字符串,防止重复提交
String nonce = request.getHeader("nonce");
if (StringUtils.isEmpty(nonce)) {
returnMsg(response,WebEnum.NONCE_EMPTY.getCode(), WebEnum.NONCE_EMPTY.getMessage());
return false;
}
//验证签名
String signature = request.getHeader("signature");
if (StringUtils.isEmpty(nonce)) {
returnMsg(response,WebEnum.SIGNATURE_EMPTY.getCode(), WebEnum.SIGNATURE_EMPTY.getMessage());
return false;
}
String method = request.getMethod();
String url = request.getRequestURI();
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
String signResult = SignUtil.getSignature(method, url, body, timestamp, nonce, appSecret);
// System.out.println("signResult = " + signResult);
if (!signature.equals(signResult)) {
returnMsg(response,WebEnum.SIGNATURE_AUTHENTICATION_FAILED.getCode(), WebEnum.SIGNATURE_AUTHENTICATION_FAILED.getMessage());
return false;
}
//检查是否重复请求
String key = appId + "_" + timestamp + "_" + nonce;
if (redisUtil.hasKey(key)) {
returnMsg(response,WebEnum.REQUEST_DUPLICATED.getCode(), WebEnum.REQUEST_DUPLICATED.getMessage());
return false;
}
//设置5分钟
redisUtil.set(key, signResult, 5 * 60);
request.setAttribute("redisKey", key);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//请求处理完毕之后,移除缓存
String value = (String) request.getAttribute("redisKey");
if (!StringUtils.isEmpty(value)) {
redisUtil.del("value");
}
}
public void returnMsg(HttpServletResponse response, Integer code, String message) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
//返回的数据
JSONObject res = new JSONObject();
res.put("code", code);
res.put("message", message);
PrintWriter out = null;
out = response.getWriter();
out.write(res.toString());
out.flush();
out.close();
}
(3)拦截器注入到WebMvcConfigurer中
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
@Bean
public SignInterceptor signInterceptor(){
return new SignInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//可以在这里添加多个接口拦截
registry.addInterceptor(signInterceptor()).addPathPatterns("/webApi/v1/test");
}
}