若依微服务的验证码流程简单来说:
前端发起验证码请求 --> 后端响应返回验证码 -->前端渲染 --> 登录 --> 后端校验验证码
后端生成验证码在gateway模块,验证码校验在auth模块(配置了filter过滤器,filter在gateway模块下)
目录
1、前端发起请求,后端返回验证码
1.1前端发起
若依微服务前端发起程序就是getCode()函数,在created()钩子函数触发,页面初始创建的时候,就发起get请求,地址是/code,代码如下,很简单:
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (this.captchaEnabled) {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
}
});
},
1.2后端响应code请求
假设我们此时在后端全局搜索@GetMapping("/code")或者搜索@GetMapping("code"),会发现搜不到。咦?那是怎么响应的?我们全局搜索"/code"会在 src/main/java/com/ruoyi/gateway/config/RouterFunctionConfiguration.java,找到一个,代码如下:
/**
* 路由配置信息
*
* @author ruoyi
*/
@Configuration
public class RouterFunctionConfiguration
{
@Autowired
private ValidateCodeHandler validateCodeHandler;
@SuppressWarnings("rawtypes")
@Bean
public RouterFunction routerFunction()
{
return RouterFunctions.route(
RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
validateCodeHandler);
}
}
解释:这是后端用了spring webflux,而不用传统的spring mvc。因为webflux是异步非阻塞,可以提高性能和效率,而且gatewya作为网关,更要关注性能效率,我们可以看到若依微服务gateway层面大部分都是响应式编程。本篇就不过多涉及响应式webflux,上面代码还涉及了设计模式。
通过RouterFunctions.route匹配到了"/code"路径,并且是get方法;匹配后需要执行一个HandlerFunction<ServerResponse> 方法,此时若依是实现了ValidateCodeHandler 类。代码如下:
/**
* 验证码获取
*
* @author ruoyi
*/
@Component
public class ValidateCodeHandler implements HandlerFunction<ServerResponse>
{
@Autowired
private ValidateCodeService validateCodeService;
@Override
public Mono<ServerResponse> handle(ServerRequest serverRequest)
{
AjaxResult ajax;
try
{
ajax = validateCodeService.createCaptcha();
}
catch (CaptchaException | IOException e)
{
return Mono.error(e);
}
return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax));
}
}
上述代码,调用了handle方法。通过validateCodeService进一步获取验证码详情,上述代码只是将返回的验证码数据封装,返回。validateCodeService.createCaptcha()代码如下:
/**
* 生成验证码
*/
@Override
public AjaxResult createCaptcha() throws IOException, CaptchaException
{
AjaxResult ajax = AjaxResult.success();
boolean captchaEnabled = captchaProperties.getEnabled();
ajax.put("captchaEnabled", captchaEnabled);
if (!captchaEnabled)
{
return ajax;
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null;
BufferedImage image = null;
String captchaType = captchaProperties.getType();
// 生成验证码
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
1.3后端生成验证码
主要代码就是createCaptcha()中,过程:
生成uuid(后面验证码校验用) -- > 生成验证码 --> 存一份redis --> 转为输出流 --> 转为Base64
返回的数据如下图所示:
1.4前端登录
前端登录除了携带username登录名,password密码外,还携带了uuid和code。
uuid:就是生成验证码的唯一标识
code:就是验证码的值
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
let data = res.data
setToken(data.access_token)
commit('SET_TOKEN', data.access_token)
setExpiresIn(data.expires_in)
commit('SET_EXPIRES_IN', data.expires_in)
resolve()
}).catch(error => {
reject(error)
})
})
},
1.5后端验证码校验
后端验证码校验是在前端请求login接口时,通过过滤器ValidateCodeFilter来校验的。
因为login请求的完整路径是:/auth/login,位于auth模块,我们不妨去nacos下对应gateway.yml看看auth配置:
# 认证中心
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
# 验证码处理
- CacheRequestFilter
- ValidateCodeFilter
- StripPrefix=1
可以看到若依已经贴心的写了注释:验证码处理:ValidateCodeFilter
现在去看ValidateCodeFilter代码:
/**
* 验证码过滤器
*
* @author ruoyi
*/
@Component
public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object>
{
private final static String[] VALIDATE_URL = new String[] { "/auth/login", "/auth/register" };
@Autowired
private ValidateCodeService validateCodeService;
@Autowired
private CaptchaProperties captchaProperties;
private static final String CODE = "code";
private static final String UUID = "uuid";
@Override
public GatewayFilter apply(Object config)
{
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 非登录/注册请求或验证码关闭,不处理
if (!StringUtils.equalsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled())
{
return chain.filter(exchange);
}
try
{
String rspStr = resolveBodyFromRequest(request);
JSONObject obj = JSON.parseObject(rspStr);
validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID));
}
catch (Exception e)
{
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
}
return chain.filter(exchange);
};
}
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest)
{
// 获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
return bodyRef.get();
}
}
ValidateCodeFilter继承了AbstractGatewayFilterFactory类,需要实现apply()方法,并返回一个GatewayFilter 对象。若依微服务中涉及了两个过滤器AbstractGatewayFilterFactory 和 GlobalFilter。一个是局部,一个是全局,这边也不过多涉及。
从前端request中获取到code和uuid,交给validateCodeService.checkCaptcha()方法校验。代码如下:
/**
* 校验验证码
*/
@Override
public void checkCaptcha(String code, String uuid) throws CaptchaException
{
if (StringUtils.isEmpty(code))
{
throw new CaptchaException("验证码不能为空");
}
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
String captcha = redisService.getCacheObject(verifyKey);
if (captcha == null)
{
throw new CaptchaException("验证码已失效");
}
redisService.deleteObject(verifyKey);
if (!code.equalsIgnoreCase(captcha))
{
throw new CaptchaException("验证码错误");
}
}
}
也就是从redis那数据,key是uuid,value是code,判断时间是否过期以及是否正确。