使用自定义注解实现接口加密的思路
1 产生需求的原因
如图,假如这个地址暴露了,那么谁拿到地址都可以直接发送请求,但是前端发送请求一定是通过http实现的,也因此这个地址一定会曝光,有心人也一定能拿到这个地址,因此需要加一个sign签名,生成签名的方式别人不知道,这样就能保证别人拿到这个地址也没办法使用
2 加密原理
当客户端向服务端发送数据包时,会基于该数据包生成签名A并带上一起发送过去,当服务端收到数据包时,也会基于数据包生成签名B,并且对比签名A和B,两者相同则认为数据包确实是由客户端合法发送过来的,服务端才会响应请求,如果签名不一致,则表示数据包的数据被临时篡改了
整个过程中最重要的就是基于数据包实现加密的方式,因此sign签名不是随便什么字段来充当的,而是基于一定的加密规则实现的,这个加密规则不管是客户端还是服务端都是重点保密的数据
3 签名时效性
sign签名应当具备时效性,否则别人拦截了单次请求后,把这个请求向服务端发送100次,那么就出乱子了,比如用户本想发送一个付费道具,结果被人拦截请求并反复发送请求,就变成了让客户端发送100次礼物,那用户的钱不就没了吗。
实现时效性有两种方式:
-
在用户登录后由服务器下发该用户所属token,每个用户都不一样,签名的生成规则中以该token为加密秘钥,由于token具有有效期,就能令生成的签名也具备时效性,但是这样就得做两层加密,登录返回的token得加密,不能明文返回,然后才是正常的对数据包进行加密
-
令请求携带时间戳,要求请求响应时间与时间戳不能超过XX秒,这就等于为签名设置了过期时间,假设这个过期时间设置为2秒,那么请求刚被拦截下来,还没完成打包再发送呢,签名就过期了,这就实现了时效性
如果遇到恶意请求攻击,那就需要使用动态过滤器,那就是另外一套解决方案了,不是签名时效性能解决的,都不是一码事
4 选择性加密(动态验签)
并不是所有接口都需要加密,比如拉取房间人数,拉取礼物数据,这些就没必要加密了,只有很重要的功能,比如支付、充值需要加密
要实现加密,就是在请求到达controller之前生成签名并完成比对,所以就需要设置拦截器
5 实现方式
5.1 创建简单的拦截器
将拦截器写在公共模块下
首先创建一个拦截器
然后在spring配置类中配置拦截器
@Configuration
public class DefaultConfig implements WebMvcConfigurer {
Logger logger = LoggerFactory.getLogger(DefaultConfig.class);
@Resource
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/**")
.excludePathPatterns("/login/**");
}
}
5.2 理清拦截器能拦截的数据
现在,直接由前端请求接口,就会被拦截器拦截
拦截器可以识别请求要访问的接口以及具体的方法,也可以看到这个方法上是否有注解,有哪些注解,那么我们只需要自定义一个注解,然后放在需要加密的方法上,当过滤器发现前端请求要访问的是有这个注解的方法时,就进行拦截,这样就可以实现动态验签,只对重要的方法进行验证
5.3 自定义注解
在公共模块自定义一个注解,设置生命周期、注解类型以及一个布尔变量
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
boolean isCheck() default true;
}
如图,把这个注解放在需要加密的方法上,就实现了定点拦截的目的
5.4 完成过滤器的编写
5.4.1 获取方法的注解
过滤器基于如图所示方法就可以获取到被请求的方法的注解
5.4.2 自定义加密方式
- 拓展:TreeMap实现排序
代码如图
效果如图
加密方式只要够复杂,别人很难获取和破解就行了,没有什么固定的说法
比如我们设置一种加密方式,首先把数据包中的属性按照A-Z或者Z-A排序,然后转为字符串,使用TreeMap就能实现A-Z或者Z-A排序
然后把排好序的属性挨个取出,拼成字符串,类似下图的效果,图中C为数组,因此有多个值,值与值之间就用逗号间隔
代码如下,在把属性拼接完毕后,最后拼接一个用户登录后服务器下发给用户的token,这样在发现签名不匹配时就可以记录这个token,反向追踪到发送了假数据的用户,然后可以封了用户或者报警让网警处理
最后对这个字符串sb进行加密,然后与前端发送过来的数据包里的签名进行比对,如果比对正确则放行
5.4.3 过滤器代码
整体代码如下
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取被请求的方法上的注解
Auth methodAnnotation = ((HandlerMethod) handler).getMethodAnnotation(Auth.class);
//如果能获取到@Auth注解并且注解的isCheck是true则进行拦截和验证
if(methodAnnotation != null && methodAnnotation.isCheck()){
//验证
Map<String, String[]> parameterMap = request.getParameterMap();
TreeMap<String, String[]> treeMap = new TreeMap<>(parameterMap);
StringBuilder sb = new StringBuilder();
treeMap.forEach((s, strings) -> {
if(!s.equals("sign")){
sb.append(s);
sb.append("=");
sb.append(strings);
sb.append("&");
}
});
sb.append("token="+"asdasdasdasdasdas");
//这步操作:借助hutool工具类 实现MD5加密 得到服务端的签名A
//获取前端传递的签名B
String sign = request.getParameter("sign");
//这步操作:比较前端和服务端的签名 两者一致才放行请求
}
return false;
}
}