No7.【spring-cloud-alibaba】用户登录密码加密、密码登录模式添加验证码校验

发布于:2022-11-09 ⋅ 阅读:(16) ⋅ 点赞:(0) ⋅ 评论:(0)

  代码地址与接口看总目录:【学习笔记】记录冷冷-pig项目的学习过程,大概包括Authorization Server、springcloud、Mybatis Plus~~~_清晨敲代码的博客-CSDN博客


终于结束从零搭建springcloud的部分了,目前也仅仅是学习了最最基本的逻辑,同时包含了开发系统的一些基本的逻辑。接下来就按照 pig 文档将其余基本的内容再熟悉一下,看一遍和写一遍真的不一样呐~~~

那接下来就一小模块一小模块的学习啦,加油吧少年!

本文及以后的文章还是基于前面的No6系列文章开发的,可以看之前文章顶部的内容总结,简单了解详情~

目录

A1.用户登录密码加密

B1.步骤

B2.编码

B3.测试

A2.密码登录模式添加验证码校验

B1.步骤

B2.编码

B3.测试


A1.用户登录密码加密

B1.步骤

首先,密码加密用 AES 对称加密,使用 hutool 包就行。然后在 gateway 网关处解拦截登录请求并密用户密码。

B2.编码

仅修改网关模块

1.新增网关配置文件,并添加属性解密密钥,然后在 pig-gateway-dev.yml 里面添加改配置;

2.新增密码解密网关过滤器,在过滤器中拦截请求,将请求中的入参取出,并将密码进行解密,然后重新包装成ServerHttpRequest。

3.在pig-gateway-dev.yml 里面请求 auth 模块的路由过滤器中添加密码解密网关过滤器。

//1.添加网关配置文件,并且加到网关配置config里面
@Data
@RefreshScope
@ConfigurationProperties("gateway")
public class GatewayConfigProperties {

    /**
     * 网关解密登录前端密码 秘钥 {@link com.pig4cloud.pig.gateway.filter.PasswordDecoderFilter}
     */
    private String encodeKey;


}

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GatewayConfigProperties.class)
public class GatewayConfiguration {

。。。
    @Bean
    public PasswordDecoderFilter passwordDecoderFilter(GatewayConfigProperties configProperties) {
        return new PasswordDecoderFilter(configProperties);
    }
。。。
}


//添加 pig-gateway-dev.yml 里面 key 值配置
gateway:
  # AES 的密钥长度需要等于16位,否则会报错:InvalidAlgorithmParameterException: IV must be 16 bytes long.
  encode-key: 'thanks!pig4cloud'
//2.修改 ValidateCodeGatewayFilter 类

@Slf4j
@RequiredArgsConstructor
public class PasswordDecoderFilter extends AbstractGatewayFilterFactory {

    private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    private static final String PASSWORD = "password";

    private static final String 	KEY_ALGORITHM = "AES";

    private final GatewayConfigProperties gatewayConfig;


    @Override
    public GatewayFilter apply(Object config) {
        return ((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            //1.如果不是登录请求,则直接向下执行
            String path = request.getURI().getPath();
            if (!StrUtil.containsAnyIgnoreCase(path, SecurityConstants.OAUTH_TOKEN_URL)) {
                return chain.filter(exchange);
            }
            //2.如果是刷新token模式的请求,则直接向下执行【因为其他模式的也有要校验的,只有刷新token不用校验】
            String grantType = request.getQueryParams().getFirst("grant_type");
            if (StrUtil.equals(grantType, SecurityConstants.REFRESH_TOKEN)) {
                return chain.filter(exchange);
            }

            //3.从request中解密前端传的密码
            Class inClass = String.class;
            Class outClass = String.class;

            ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);

            Mono<?> modifiedBody = serverRequest.bodyToMono(inClass).flatMap(this.decryptAES());

            //4.将解密后的生成新的request【ServerHttpRequest请求对象的请求体只能获取一次,一旦获取了就不能继续往下传递。】

            //todo 没明白接下来的具体实现,而且网上也没搜索到相关信息,所以留一个 todo
            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            headers.remove(HttpHeaders.CONTENT_LENGTH);

            headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);

            return bodyInserter
                    .insert(outputMessage, new BodyInserterContext())
                    .then(Mono.defer(() -> {
                        ServerHttpRequest decorator = decorate(exchange, headers, outputMessage);
                        return chain.filter(exchange.mutate().request(decorator).build());
                    }));
        });
    }

    /**
     * 原文解密
     * @return
     */
    private Function decryptAES() {
        return s -> {
            // 1.构建前端对应解密AES 因子
            AES aes = new AES(Mode.CFB, Padding.NoPadding,
                    new SecretKeySpec(gatewayConfig.getEncodeKey().getBytes(), KEY_ALGORITHM),
                    new IvParameterSpec(gatewayConfig.getEncodeKey().getBytes()));

            // 2.获取请求密码的并解密
            Map<String, String> inParamsMap = HttpUtil.decodeParamMap((String) s, CharsetUtil.CHARSET_UTF_8);
            // 判断入参是否有 password 入参,没有则返回非法参数,有则解密password
            if (inParamsMap.containsKey(PASSWORD)) {
                String password = aes.decryptStr(inParamsMap.get(PASSWORD));
                // 返回修改后报文字符
                inParamsMap.put(PASSWORD, password);
            }
            else {
                log.error("非法请求数据:{}", s);
            }
            //初始化一个Mono对象,将参数放到 mono 里面
            return Mono.just(HttpUtil.toParams(inParamsMap, Charset.defaultCharset(), true));
        };
    }

    /**
     * 报文转换
     * @return
     */
    private ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0) {
                    httpHeaders.setContentLength(contentLength);
                }
                else {
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }

            @Override
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }
}
//3.在pig-gateway-dev.yml 里面请求 auth 模块的路由过滤器中添加密码解密网关过滤器。

# 配置到 nacos 配置中
spring:
 cloud:
   nacos:
   gateway:
     discovery:
       locator:
         enabled: true  # 让gateway可以发现nacos中的微服务
     routes:
       # 认证中心
       - id: pig-auth
         uri: lb://pig-auth
         predicates:
           - Path=/auth/**
         filters:
           - StripPrefix=1  #去掉特定前缀个数
          # 前端密码解密
           - PasswordDecoderFilter

B3.测试

在 pig 提供的 pig4cloud 加密服务 中,按照密钥和密码,生成一个已加密的密码,然后调用接口请求,成功!

  

A2.密码登录模式添加验证码校验

B1.步骤

为了防止恶意登录,我们给密码模式添加验证码。安全意识较强的网站,此时一般会设置允许错误的次数,如3/5次错误即触发账户锁定1小时或者5小时不定,防止密码被暴力破解的隐患。

验证码的获取与校验都在gateway网关处完成就行~

首先,先添加获取校验码接口,然后添加GatewayFilter网关过滤器用来拦截请求,如果是登录请求则校验验证码是否正确,如果不是这直接跳过执行下面。

B2.编码

仅修改网关模块

1.导入校验码的依赖包,我们使用 pig4cloud 项目自创的

2.因为之前测试网关返回图片时已添加了ImageCodeHandler类,所以现在就修改这个类,设置他返回验证码图片,并将验证码数据存储到redis里面。【注意需要将这个接口加到RouterFunction里面~】

3.因为之前测试网关过滤器GatewayFilter已添加了ValidateCodeGatewayFilter类,所以现在就修改这个类,修改 checkCode() 方法,从 redis 里面拿到对应的 code 值,然后和入参进行判断,不一致则返回错误验证码,一致则执行下面。

4.给网关里面的 auth 路由模块添加过滤器ValidateCodeGatewayFilter;

//1.导入校验码的依赖包,我们使用 pig4cloud 项目自创的
        <!--验证码 源码: https://github.com/pig-mesh/easy-captcha -->
        <dependency>
            <groupId>com.pig4cloud.plugin</groupId>
            <artifactId>captcha-spring-boot-starter</artifactId>
            <version>${captcha.version}</version>
        </dependency>
//2.因为之前测试网关返回图片时已添加了ImageCodeHandler类,所以现在就修改这个类,设置他返回验证码图片,并将验证码数据存储到redis里面。【注意需要将这个接口加到RouterFunction里面~】
@Slf4j
@RequiredArgsConstructor
public class ImageCodeHandler implements HandlerFunction<ServerResponse> {

    private static final Integer DEFAULT_IMAGE_WIDTH = 100;

    private static final Integer DEFAULT_IMAGE_HEIGHT = 40;

    private final RedisTemplate<String, Object> redisTemplate;

    @SneakyThrows
    @Override
    public Mono<ServerResponse> handle(ServerRequest request) {
        //1.生成算数校验码
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT);
        String result = captcha.text();

        //2.保存验证码信息
        Optional<String> randomStr = request.queryParam("randomStr");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        randomStr.ifPresent(s ->
                redisTemplate.opsForValue().set(CacheConstants.DEFAULT_CODE_KEY + s, result, SecurityConstants.CODE_TIME, TimeUnit.SECONDS));

        // 3.转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        captcha.out(os);

        // 4.统一服务器接口调用的响应
        return ServerResponse.status(HttpStatus.OK)
                .contentType(MediaType.IMAGE_JPEG)
                .body(BodyInserters.fromResource(new ByteArrayResource(os.toByteArray())));
    }
}
//3.因为之前测试网关过滤器GatewayFilter已添加了ValidateCodeGatewayFilter类,所以现在就修改这个类,修改 checkCode() 方法,从 redis 里面拿到对应的 code 值,然后和入参进行判断,不一致则返回错误验证码,一致则执行下面。

@Slf4j
@RequiredArgsConstructor
public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory<Object> {

    private final ObjectMapper objectMapper;

    private final RedisTemplate<String, Object> redisTemplate;

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            //1.如果不是登录请求,则直接向下执行
            String path = request.getURI().getPath();
            if (!StrUtil.containsAnyIgnoreCase(path, SecurityConstants.OAUTH_TOKEN_URL)) {
                return chain.filter(exchange);
            }
            //2.如果是刷新token模式的请求,则直接向下执行【因为其他模式的也有要校验的,只有刷新token不用校验】
            String grantType = request.getQueryParams().getFirst("grant_type");
            if (StrUtil.equals(grantType, SecurityConstants.REFRESH_TOKEN)) {
                return chain.filter(exchange);
            }

            try {
                //3.校验验证码【密码模式登录或者短信模式登录都需要校验~】
                checkCode(request);
            }
            catch (Exception e) {
                //若有异常则返回ServerHttpResponse类型,输出为 Json 格式
                ServerHttpResponse response = exchange.getResponse();
                response.setStatusCode(HttpStatus.PRECONDITION_REQUIRED);
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                final String errMsg = e.getMessage();

                return response.writeWith(Mono.create(monoSink -> {
                    try {
                        byte[] bytes = objectMapper.writeValueAsBytes(R.failed(errMsg));
                        DataBuffer dataBuffer = response.bufferFactory().wrap(bytes);
                        monoSink.success(dataBuffer);
                    }
                    catch (JsonProcessingException jsonProcessingException) {
                        log.error("对象输出异常", jsonProcessingException);
                        monoSink.error(jsonProcessingException);
                    }
                }));
            }

            return chain.filter(exchange);
        };
    }

    @SneakyThrows
    private void checkCode(ServerHttpRequest request) {
        //1.校验是否有 code 值
        String code = request.getQueryParams().getFirst("code");
        if (CharSequenceUtil.isBlank(code)) {
            log.info("登录请求,验证码为空!");
            throw new RuntimeException("验证码不能为空");
        }

        //2.校验是否有 code 的唯一标识值,密码验证码模式登录从 randomStr 里取,短信模式可以从 mobile 里面取,但保证 mobile 属性必须只有一个!
        String randomStr = request.getQueryParams().getFirst("randomStr");
        if (CharSequenceUtil.isBlank(randomStr)) {
            randomStr = request.getQueryParams().getFirst("mobile");
        }
        //3.从 redis 里面拿到对应的验证码值
        String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;

        Object codeObj = redisTemplate.opsForValue().get(key);
        //4.无论拿没拿到都要进行删除
        redisTemplate.delete(key);
        //5.判断两个值是否一致,不一致则抛出异常
        if (ObjectUtil.isEmpty(codeObj) || !code.equals(codeObj)) {
            throw new ValidateCodeException("验证码不合法");
        }
    }
}
//4.给网关里面的 auth 路由模块添加过滤器ValidateCodeGatewayFilter;

# 配置到 nacos 配置中
spring:
 cloud:
   nacos:
   gateway:
     discovery:
       locator:
         enabled: true  # 让gateway可以发现nacos中的微服务
     routes:
       # 认证中心
       - id: pig-auth
         uri: lb://pig-auth
         predicates:
           - Path=/auth/**
         filters:
           - StripPrefix=1  #去掉特定前缀个数
           # 验证码处理
           - ValidateCodeGatewayFilter
          # 前端密码解密
           - PasswordDecoderFilter

 

B3.测试

给 /oauth2/token 接口添加入参 code ,先调用获取验证码的接口拿到 code 值,然后再调用登录接口~