若依微服务中验证码校验逻辑

        若依微服务的验证码流程简单来说:

        前端发起验证码请求 --> 后端响应返回验证码 -->前端渲染 --> 登录 --> 后端校验验证码

        后端生成验证码在gateway模块,验证码校验在auth模块(配置了filter过滤器,filter在gateway模块下)

目录

1、前端发起请求,后端返回验证码

1.1前端发起

1.2后端响应code请求

1.3后端生成验证码

1.4前端登录 

1.5后端验证码校验


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,判断时间是否过期以及是否正确。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值