近况

这是最后一篇若依微服务博客吧,坦白说若依微服务中的东西有很多看不太懂,比如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物理分页插件

本来还想写一写,一看别人写的东西简直吊打我,没动力写下去了,有些东西不必重复造轮子吧
很多东西学了就忘,重要的是信息收集和整合能力吧(好吧,我承认我懒)

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()));
        }
    }
}