图形验证码解耦设计

目的

在调用接口时,要求必须有输入正确的图形验证码才能调用(防刷)。
但是,看一些代码中将这个功能和其它业务功能耦合在一起。每次有新的接口需要用时,又得重新复制一份,就想到值得优化重构。

思路

生成时,从全部字母和数字中随机获取 6 个字符。
接着借助 BufferedImage 和 ImageIO 工具生成字节码格式的图形验证码。
再将字节码形式的图形验证码转换成 base64 字符串,等待发给前端,前端接收后可以转换并显示出来(这样我们就可以统一用 json 的方式来和前端交互,返回格式是一样的;还有一种方式是通过回传 IMAGE 类型的方式,不过缺点是接口不统一,前端要单独处理)。

我们随机生成一个唯一标识(uniqueId),标识这次图形验证码请求,并将对应的验证码(graphValidateCode)存放到缓存(redis)中,并设置过期时间。
下一个请求需要带上这个唯一标识,和验证码。
结合过滤器,这样获取验证码的逻辑就可以和其它业务逻辑解耦了。

哪个接口需要有正确的图形验证码才放行,只需要配置上 api 即可。

过滤器配置

在过滤器中(GraphValidateCodeFilter)验证图形验证码,从缓存中获取,然后比对传入的验证码。
如果没传入参数,或者参数不对,就抛出错误,在统一管理错误的地方进行处理。

错误处理

一般情况,异常是在定义了 ControllerAdvice 注解 或者 RestControllerAdvice 注解的类中管理的。
但是过滤器中的异常是在 Servlet 之前调用的,ControllerAdvice 适用于 Controller。
不过可以换个思路,我们写一个类来继承 ErrorController ,重写 handleError 方法,在过滤器中异常发生时,最后也会来到这个 handleError 方法中,
我们在这里可以直接返回,也可以继续抛出异常,如果选择了继续抛出,接着就可以在 GlobalExceptionHandler 中进行捕获(统一出错出口),
这样就可以解决 filter 在 Servlet 之前调用问题。

参考资料:
How to ExceptionHandler of type ExpiredJwtException from JwtAuthenticationTokenFilter? · Issue #63 · szerhusenBC/jwt-spring-security-demo
It’s because the filter comes before Servlet is invoked. ControllerAdvice only applies to the Controller classes.
I’m also trying to find the solution to same problem. Been reading some stackoverflow articles, this one seems promising: https://stackoverflow.com/questions/34595605/how-to-manage-exceptions-thrown-in-filters-in-spring @josevlad
https://stackoverflow.com/a/50818385

最终效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
* 先获取图形验证码
* http://localhost:8080/api/tool/getGraphValidateCode
* {
* "err_code": 200,
* "err_msg": "ok",
* "data": {
* "image": "iVBORw0KGgoAAAANSUhEUgAAAG4AAAAjCAIAAAD6/6geAAABWklEQVR42u2a0RaEIAhE+f8P3d9oX/alzXIYQLBDp8dCveE4YnIcn75dbmkEjbJRNsq+G2WjbJSNEnv0fGnfIh4WzWVpVzv2YVhxjPVKlHijQod7PUpt0+KOIwJldLuEml0fKIry17kaKNHels1KQkzc+6yiKZxevHuCX99FRHkPlAQUGuW0xTsV2ikr45yDi7jLet1R2AvWCS1DeXrFJToqzKko7YO9Nee+WQn6r61RTsblm/PGXQe3guO7HXAImRP8obulzNBmKP/SZF+U6kU8bgUnNn8gSpWWaVFqBW2RGTI2pLLKYJ+Nifm0MO5YGUIWGQtKdKeYVc4gJH+adEQlgtnGAB5zdVZqBY5zo3ZPyhWeV09wxOcXQanuQ1bBaheUU90wVVW9zvByYxpLy4NomQfHAd8yuhr0NKXyT+Jj0jPUb4xneon/GmKkxhhTvS4V+lGkUnoSTL8omDLPAIEmVgAAAABJRU5ErkJggg==",
* "graphValidateCode": "fHELQ",
* "uniqueId": "d476ef5e8d6b4e34a7dd5fde9fcd3c78"
* }
* }
*/

/*
* 不填写时:
* http://localhost:8080/api/user/login
* {
* "err_code": 400,
* "err_msg": "缺少参数:uniqueId",
* "data": null
* }
*/

/*
* 错误时:
* http://localhost:8080/api/user/login?uniqueId=bbbb&graphValidateCode=aaaa
* {
* "err_code": 400,
* "err_msg": "输入的图形验证码不正确",
* "data": null
* }
*/

/*
* 正确时:
* http://localhost:8080/api/user/login?uniqueId=d476ef5e8d6b4e34a7dd5fde9fcd3c78&graphValidateCode=fHELQ
* {
* "err_code": 200,
* "err_msg": "ok",
* "data": "验证了图形验证码才能看到我:login"
* }
*/

参考资料

RuoYi-VUE

源代码

https://github.com/lyloou/spring-master/tree/master/spring-security-captcha