代码地址与接口看总目录:【学习笔记】记录冷冷-pig项目的学习过程,大概包括Authorization Server、springcloud、Mybatis Plus~~~_清晨敲代码的博客-CSDN博客
终于结束从零搭建springcloud的部分了,目前也仅仅是学习了最最基本的逻辑,同时包含了开发系统的一些基本的逻辑。接下来就按照 pig 文档将其余基本的内容再熟悉一下,看一遍和写一遍真的不一样呐~~~
那接下来就一小模块一小模块的学习啦,加油吧少年!
本文及以后的文章还是基于前面的No6系列文章开发的,可以看之前文章顶部的内容总结,简单了解详情~
目录
首先,密码加密用 AES 对称加密,使用 hutool 包就行。然后在 gateway 网关处解拦截登录请求并密用户密码。
仅修改网关模块
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
> 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
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
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
在 pig 提供的 pig4cloud 加密服务 中,按照密钥和密码,生成一个已加密的密码,然后调用接口请求,成功!
为了防止恶意登录,我们给密码模式添加验证码。安全意识较强的网站,此时一般会设置允许错误的次数,如3/5次错误即触发账户锁定1小时或者5小时不定,防止密码被暴力破解的隐患。
验证码的获取与校验都在gateway网关处完成就行~
首先,先添加获取校验码接口,然后添加GatewayFilter网关过滤器用来拦截请求,如果是登录请求则校验验证码是否正确,如果不是这直接跳过执行下面。
仅修改网关模块
1.导入校验码的依赖包,我们使用 pig4cloud 项目自创的
2.因为之前测试网关返回图片时已添加了ImageCodeHandler类,所以现在就修改这个类,设置他返回验证码图片,并将验证码数据存储到redis里面。【注意需要将这个接口加到RouterFunction里面~】
3.因为之前测试网关过滤器GatewayFilter已添加了ValidateCodeGatewayFilter类,所以现在就修改这个类,修改 checkCode() 方法,从 redis 里面拿到对应的 code 值,然后和入参进行判断,不一致则返回错误验证码,一致则执行下面。
4.给网关里面的 auth 路由模块添加过滤器ValidateCodeGatewayFilter;
- //1.导入校验码的依赖包,我们使用 pig4cloud 项目自创的
-
- <dependency>
- <groupId>com.pig4cloud.plugingroupId>
- <artifactId>captcha-spring-boot-starterartifactId>
- <version>${captcha.version}version>
- dependency>
- //2.因为之前测试网关返回图片时已添加了ImageCodeHandler类,所以现在就修改这个类,设置他返回验证码图片,并将验证码数据存储到redis里面。【注意需要将这个接口加到RouterFunction里面~】
- @Slf4j
- @RequiredArgsConstructor
- public class ImageCodeHandler implements HandlerFunction
{ -
- private static final Integer DEFAULT_IMAGE_WIDTH = 100;
-
- private static final Integer DEFAULT_IMAGE_HEIGHT = 40;
-
- private final RedisTemplate
redisTemplate; -
- @SneakyThrows
- @Override
- public Mono
handle(ServerRequest request) { - //1.生成算数校验码
- ArithmeticCaptcha captcha = new ArithmeticCaptcha(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT);
- String result = captcha.text();
-
- //2.保存验证码信息
- Optional
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
-
- private final ObjectMapper objectMapper;
-
- private final RedisTemplate
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
给 /oauth2/token 接口添加入参 code ,先调用获取验证码的接口拿到 code 值,然后再调用登录接口~