代码地址与接口看总目录:【学习笔记】记录冷冷-pig项目的学习过程,大概包括Authorization Server、springcloud、Mybatis Plus~~~_清晨敲代码的博客-CSDN博客
终于结束从零搭建springcloud的部分了,目前也仅仅是学习了最最基本的逻辑,同时包含了开发系统的一些基本的逻辑。接下来就按照 pig 文档将其余基本的内容再熟悉一下,看一遍和写一遍真的不一样呐~~~
那接下来就一小模块一小模块的学习啦,加油吧少年!
本文及以后的文章还是基于前面的No6系列文章开发的,可以看之前文章顶部的内容总结,简单了解详情~
目录
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 值,然后再调用登录接口~