近况
这是最后一篇若依微服务博客吧,坦白说若依微服务中的东西有很多看不太懂,比如webflux,SpringSecurity
只能说整个项目看懂大概一半吧,项目细节很多,搞定完这篇就要准备找工作了,也不知道好不好找
如果让我客观评价自己,我觉得自己刚入门吧,虽然学习了很多东西,但很多技术不使用就容易忘记,
前后端,微服务,docker部署都了解一点,但我最在意的还是自己基础不行,计网,操作系统虽然也学过,
但学的不完整,效果不好,还有数据结构和算法吧,这才是程序员最重要的内容,当然也是最难提升的,
今年的目标是重拾基础,看完计网,操作系统,jvm,spring网课,Spring原理值得关注,之前感觉只学习点皮毛吧
回归正题,下面是本篇博客目录
Springboot使用七牛云
看了一下七牛云的JavaSDK,对七牛云的使用有了大致的了解,花了点时间写了一个七牛云工具类
直接放在github上吧,这里篇幅不想太大了,看到七牛云的安全机制还是挺有意思的,等下记录一下
七牛云的基本使用
导入依赖
<dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>[7.2.0, 7.2.99]</version> </dependency>
项目配置文件中:
oss: qiniu: accessKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx secretKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 空间名称 bucket: 空间名称 # 访问域名 domain: 自定义源站域名或者自定义 CDN 加速域名或者七牛云的测试域名
编写七牛云配置类
@Component @ConfigurationProperties(prefix = "oss.qiniu") @Data @AllArgsConstructor @NoArgsConstructor public class QiniuProperties { private String accessKey; private String secretKey; private String bucket; private String domain; private Long expireInSeconds; }
编写七牛云工具类
工具类篇幅过大,已放github
七牛云的安全机制学习
通过对七牛云的了解,我们知道,上传,下载,管理文件时都需要凭证,
凭证是根据accessKey,secretKey生成的一串字符串,凭证是七牛云存储用于验证请求合法性的机制。
那么这个验证请求合法性的机制就值得我们学习了,先来看看上传凭证的生成过程,上传凭证
了解完它的生成过程,我们知道,七牛云的服务端会怎么验证这个凭证字符串?七牛云服务器端接收到这个字符串,解析出accessKey,然后找到对应的secretKey,secretKey是七牛云
签发的,所以当然能找到,然后将凭证字符串中的encodedSign,进行base64解码再使用secretKey解密
得到上传策略的base64编码数据,这个数据与凭证字符串中携带的数据相对比,如果一致,则说明用户拥有
请求的合法性,反之则用户没有请求合法性,这就是验证请求合法性的机制这个验证机制值得我们学习,以后在设计验证用户请求时也可以使用这种方式。
Springboot使用pagehelper物理分页插件
本来还想写一写,一看别人写的东西简直吊打我,没动力写下去了,有些东西不必重复造轮子吧
很多东西学了就忘,重要的是信息收集和整合能力吧(好吧,我承认我懒)。
基本使用
引入依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.10</version> </dependency>
添加配置
# 分页配置 pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true params: count=countSql
实际使用
public PageInfo<Blog> ajaxBlog(Integer pageNum,Integer pageSize){ PageHelper.startPage(pageNum,pageSize); List<Blog> blogList = blogMapper.selectByExample(new BlogExample()); PageInfo<Blog> pageInfo = new PageInfo<Blog>(blogList); return pageInfo; }
文章推荐
JJWT的使用
引入依赖
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
编写资源配置类,或者和若依项目中写死
public class TokenConstants { /** * 令牌自定义标识 */ public static final String AUTHENTICATION = "Authorization"; /** * 令牌前缀 */ public static final String PREFIX = "Bearer "; /** * 令牌秘钥 */ public final static String SECRET = "abcdefghijklmnopqrstuvwxyz"; }
JwtUtils工具类
/** * Jwt工具类 */ public class JwtUtils { public static String secret = TokenConstants.SECRET; /** * 从数据声明生成令牌 * * @param claims 数据声明 * @return 令牌 */ public static String createToken(Map<String, Object> claims) { String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact(); return token; } /** * 从令牌中获取数据声明 * * @param token 令牌 * @return 数据声明 */ public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } /** * 根据令牌获取用户标识 * * @param token 令牌 * @return 用户ID */ public static String getUserKey(String token) { Claims claims = parseToken(token); return getValue(claims, SecurityConstants.USER_KEY); } /** * 根据令牌获取用户标识 * * @param claims 身份信息 * @return 用户ID */ public static String getUserKey(Claims claims) { return getValue(claims, SecurityConstants.USER_KEY); } /** * 根据令牌获取用户ID * * @param token 令牌 * @return 用户ID */ public static String getUserId(String token) { Claims claims = parseToken(token); return getValue(claims, SecurityConstants.DETAILS_USER_ID); } /** * 根据身份信息获取用户ID * * @param claims 身份信息 * @return 用户ID */ public static String getUserId(Claims claims) { return getValue(claims, SecurityConstants.DETAILS_USER_ID); } /** * 根据令牌获取用户名 * * @param token 令牌 * @return 用户名 */ public static String getUserName(String token) { Claims claims = parseToken(token); return getValue(claims, SecurityConstants.DETAILS_USERNAME); } /** * 根据身份信息获取用户名 * * @param claims 身份信息 * @return 用户名 */ public static String getUserName(Claims claims) { return getValue(claims, SecurityConstants.DETAILS_USERNAME); } /** * 根据身份信息获取键值 * * @param claims 身份信息 * @param key 键 * @return 值 */ public static String getValue(Claims claims, String key) { return Convert.toStr(claims.get(key), ""); } }
工具类中需要的类已放入github
文章推荐
若依微服务中登录认证流程
若依微服务中登录验证码实现
若依使用的是谷歌的验证码库kaptcha,用户请求验证码/code,
使用kaptcha生成数据,例如3+5=8
,然后生成一个uuid,将该uuid作为key,8作为value保存到redis中
然后根据3+5
生成base64的图片数据,最后将uuid和图片数据返回给浏览器,浏览器渲染出验证码图片
当用户填写完账号,密码,验证码值后,发出登录请求/login,该请求中包含了之前生成的uuid数据,
在gateway网关处,根据用户传递过来的uuid,取出redis中的值与用户填入的验证码值进行对比。
以上就是验证码的流程。若依微服务中登录流程
登录验证流程在ruoyi-auth模块中进行
首先检查用户输入账号密码格式是否正确,例如用户名是否为空
密码是否在指定范围内等等,然后根据用户名查询出该用户的信息,再次检查该用户是否存在,是否已删除,是否已停用
账号密码是否正确,如果一切通过,则做登录日志,将日志持久化到数据库接着就是封装好用户数据,设置好用户账号,id,密码,一个uuid,封装成loginUser对象
然后根据对象中uuid作为key,loginUser对象作为value,将用户数据保存在redis中
这个操作是属于ruoyi-common-security的TokenService中刷新令牌有效期的方法
意思是每次登陆都会保存用户数据到redis中,并重新设置好过期时间,在执行这个方法之前
会先进入HeaderInterceptor拦截器,在该拦截器中将用户数据保存到当前线程一份,以方便后续使用
使用的是阿里巴巴的TransmittableThreadLocal,简称TTL,原理不太懂,
好像是用来解决InheritableThreadLocal在线程池复用问题的,具体怎么解决看不太懂。最后就是使用jwt封装用户数据,生成token,将该token返回给浏览器,之后每次请求
都将携带该token进行验证。服务器收到token解析出其中的uuid,根据uuid就能找到用户数据了
Gateway中自定义局部过滤器
Spring Gateway中局部过滤器分为内置局部过滤器和自定义局部过滤器,如果要自定义局部过滤器
需要自定义过滤器实现AbstractGatewayFilterFactory抽象类,例如,下面是若依项目中黑名单过滤器
package com.ruoyi.gateway.filter;
/**
* 黑名单过滤器
*/
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config> {
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
//主要处理逻辑处
String url = exchange.getRequest().getURI().getPath();
if (config.matchBlacklist(url)) {
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
}
return chain.filter(exchange);
};
}
public BlackListUrlFilter() {
super(Config.class);
}
public static class Config {
private List<String> blacklistUrl;
private List<Pattern> blacklistUrlPattern = new ArrayList<>();
public boolean matchBlacklist(String url) {
return blacklistUrlPattern.isEmpty() ?
false : blacklistUrlPattern.stream()
.filter(p -> p.matcher(url).find()).findAny().isPresent();
}
public List<String> getBlacklistUrl() {
return blacklistUrl;
}
public void setBlacklistUrl(List<String> blacklistUrl) {
this.blacklistUrl = blacklistUrl;
this.blacklistUrlPattern.clear();
this.blacklistUrl.forEach(
url -> { this.blacklistUrlPattern
.add(Pattern.compile(url.replaceAll("\\*\\*",
"(.*?)"),Pattern.CASE_INSENSITIVE));
});
}
}
}
实现过程:
首先我们的自定义过滤器要继承该抽象类,AbstractGatewayFilterFactory
其次我们的自定义过滤器要有一个config内部类,用来支持在yml文件中给自定义过滤器配置特定参数
就是在使用自定义过滤器的时候,可以向自定义过滤器中追加一些数据。例如spring: cloud: gateway: routes: # 认证中心 - id: ruoyi-auth uri: lb://ruoyi-auth predicates: - Path=/auth/** filters: - StripPrefix=1 #去掉前一个路径,如果请求是/auth/code,去除之后:/code - name: BlackListUrlFilter #使用自定义局部过滤器 - arg: #给自定义局部过滤器传参 blacklistUrl: - /xxx
最后,我们需要显示声明自定义过滤器的无参数构造器,并传递Config内部类,例如上面的:
这样我们才能在apply方法中拿到config对象。public BlackListUrlFilter() { super(Config.class); }
上面便是创建一个Gateway的自定义局部过滤器必须的步骤
另外,当自定义过滤器判断请求可以通过时,使用:return chain.filter(exchange);
如果判断失败,不能通过,则通常使用下面的方式:
//1,不合法
ServerHttpResponse response = exchange.getResponse();
//设置headers
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
//设置body
String warningStr = "未授权的请求,请登录";
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
//2,或者下面方式:
ServerHttpResponse response = exchange.getResponse();
String warningStr = "登录超时";
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
//3,而若依微服务中则是通过ServletUtils工具类的方法处理,如下:
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param contentType content-type
* @param status http状态码
* @param code 响应状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response,
String contentType, HttpStatus status, Object value, int code)
{
response.setStatusCode(status);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
R<?> result = R.fail(code, value.toString());
DataBuffer dataBuffer = response.bufferFactory().wrap(JSONObject.toJSONString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
以上便是Gateway中创建自定义局部过滤器的使用方式。
文章推荐
feign自定义拦截器
在使用Feign调用其他模块接口时,可能会需要创建一个Feign的拦截器,用来传递当前请求携带的数据
若依项目中,Feign的拦截器如下:
package com.ruoyi.common.security.feign;
/**
* feign 请求拦截器
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor
{
@Override
public void apply(RequestTemplate requestTemplate)
{
HttpServletRequest httpServletRequest = ServletUtils.getRequest();
if (StringUtils.isNotNull(httpServletRequest))
{
Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
// 传递用户信息请求头,防止丢失
String userId = headers.get(SecurityConstants.DETAILS_USER_ID);
if (StringUtils.isNotEmpty(userId))
{
requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);
}
String userName = headers.get(SecurityConstants.DETAILS_USERNAME);
if (StringUtils.isNotEmpty(userName))
{
requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);
}
String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
if (StringUtils.isNotEmpty(authentication))
{
requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
}
// 配置客户端IP
requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr(ServletUtils.getRequest()));
}
}
}