SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证

文章目录

      • 一、 Gateway filter应用
        • 一、filter简介
        • 二、全局过滤器的使用
      • 二、 Gateway filter + JWT实现token拦截
        • 一、jwt简介
        • 二、jwt工具类
        • 三、登录签发token
        • 四、filter拦截token验证,并对特殊接口放行

一、 Gateway filter应用

一、filter简介

1、gateway filter的生命周期

Spring Cloud Gateway同zuul类似,有“pre”和“post”两种方式的filter。客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端

  • pre类型的filter:在业务逻辑之前
  • post类型的filter:在业务逻辑之后

SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第1张图片

2、gateway filter的应用场景

  • pre类型的过滤器:参数校验、权限校验、流量监控、日志输出、协议转换等
  • post类型的过滤器:响应内容、响应头的修改,日志的输出,流量监控等
二、全局过滤器的使用

1、引入依赖和application.yml配置文件

引入依赖

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>3.0.7</version>
        </dependency>
        <dependency>
            <groupId>com.mdx</groupId>
            <artifactId>mdx-shop-common</artifactId>
            <version>1.0.0</version>
        </dependency>

application.yml配置

server:
  port: 9010

spring:
  application:
    name: mdx-shop-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: mdx
        group: mdx
    gateway:
      discovery:
        locator:
          enabled: true  #开启通过服务中心的自动根据 serviceId 创建路由的功能


gateway:
  routes:
    config:
      data-id: gateway-routes  #动态路由
      group: shop
      namespace: mdx



注:

  • 配置文件中的相关路由信息已经配置在了nacos的配置中心
  • springcloud gateway路由相关和引入gateway依赖和spring-boot-starter-web依赖冲突问题可以先看下面的文章
  • springcloud gateway的使用 +
    nacos动态路由

2、创建自定义全局过滤器

新建自定义filter类,需要实现GlobalFilter, Ordered类
其中GlobalFilter是gateway的全局过滤类
他的实现类如下:
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第2张图片

Ordered类是过滤器的执行级别,数值越小执行顺序越靠前

MdxAuthFilter完整代码
注:先简单的模拟了一个token验证的流程

package com.mdx.gateway.filter;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * @author : jiagang
 * @date : Created in 2022/8/8 15:30
 */
@Component
@Slf4j
public class MdxAuthFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("=========================请求进入filter=========================");

        // 模拟token验证
        String token = exchange.getRequest().getHeaders().getFirst("token");
        if (!"token123".equals(token)){
            log.error("token验证失败...");
            return writeResponse(exchange.getResponse(),401,"token验证失败");
        }
        log.info("token验证成功...");
        return chain.filter(exchange);
    }

    /**
     * 值越小执行顺序越靠前
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * 构建返回内容
     *
     * @param response ServerHttpResponse
     * @param code     返回码
     * @param msg     返回数据
     * @return Mono
     */
    protected Mono<Void> writeResponse(ServerHttpResponse response, Integer code, String msg) {
        JSONObject message = new JSONObject();
        message.put("code", code);
        message.put("msg", msg);
        byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.OK);
        // 指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
}

writeResponse是对返回错误信息结果的封装
这里有个比较容易犯错的点,我们通常会在业务系统中建立全局异常处理器,来建立友好的错误返回信息,但是对于filter来说是不生效的,因为filter是在controller之前执行的,所全局异常处理器是不生效的

测试
通过访问网关服务路由到其他服务,查看filter反应
我这里是启动了一个网关服务和一个订单服务
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第3张图片
访问接口并在请求头添加token(9010端口为网关服务)
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第4张图片
可以看到请求是成功经过了过滤器
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第5张图片

测试传递一个错误的token
成功返回错误信息
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第6张图片

二、 Gateway filter + JWT实现token拦截

一、jwt简介

1、官方解释什么是jwt

JWT(JSON WEB
TOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。广义上讲JWT是一个标准的名称;狭义上JWT指的就是用来传递的那个token字符串

2、jwt的结构

  • Header Header 主要包括两部分,分别是Token的类型(typ)和签名算法(alg)
  • Payload Payload 表示有效负载,主要`是关于实体和其他数据的声明。主要有三种类型,分别是 registered(已注册信息),
    public(公开信息),private(私有信息)

3、jwt的作用
由于http协议是无状态的,所以客户端每次访问都是新的请求。这样每次请求都需要验证身份,传统方式是用session+cookie来记录/传输用户信息,而JWT就是更安全方便的方式。它的特点就是简洁,紧凑和自包含,而且不占空间,传输速度快,而且有利于多端分离,接口的交互等等
JWT是一种Token规范,主要面向的还是登录、验证和授权方向,当然也可以用只来传递信息。一般都是存在header里,也可以存在cookie里

3、jwt和token的关系

  • Token是服务器签发的一串加密字符串,是为了给客户端重复访问的一个令牌,作用是为了证明请求者(客户端)的身份,保持用户长期保持登录状态
  • JWT就是token的一种载体,或者说JWT是一种标准,而Token是一个概念,而JWT就是这个概念执行的一种规范,通俗点说token就是一串字符串,jwt就是加上类型,信息等数据再加密包装一下成为一个新的token
二、jwt工具类

1、引入jwt依赖

		<!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

2、jwt工具类

package com.mdx.gateway.utils;
import com.mdx.common.utils.LocalDateUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWTProvider需要至少提供两个方法,一个用来创建我们的token,另一个根据token获取Authentication。
 * provider需要保证Key密钥是唯一的,使用init()构建,否则会抛出异常。
 * @author : jiagang
 * @date : Created in 2022/2/9 14:12
 */
@Component
@Slf4j
public class JWTProvider {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;


    /**
     * 根据用户信息生成token
     *
     * @param userName
     * @return
     */
    public String generateToken(String userName) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("CLAIM_KEY_USERNAME", userName);
        claims.put("CLAIM_KEY_CREATED", new Date());
        return generateToken(claims);
    }

    /**
     * 从token中获取登录用户名
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token){
        String username;
        try {
            Claims claims = getClaimsFormToken(token);
//            username = claims.getSubject();
            username = claims.get("CLAIM_KEY_USERNAME").toString();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 验证token是否有效
     * @param token
     * @param userName
     * @return
     */
    public boolean validateToken(String token,String userName){
        String username = getUserNameFromToken(token);
        return username.equals(userName) && !isTokenExpired(token);
    }


    /**
     * 判断token是否可以被刷新
     * @param token
     * @return
     */
    public boolean canRefresh(String token){
        return !isTokenExpired(token);
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public String refreshToken(String token){
        Claims claims = getClaimsFormToken(token);
        claims.put("CLAIM_KEY_CREATED",new Date());
        return generateToken(claims);
    }

    /**
     * 判断token是否失效
     * @param token
     * @return
     */
    private boolean isTokenExpired(String token) {
        Date expireDate = getExpiredDateFromToken(token);
        return expireDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     * @param token
     * @return
     */
    public Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFormToken(token);
        return claims.getExpiration();
    }

    /**
     * 从token中获取荷载
     * @param token
     * @return
     */
    private Claims getClaimsFormToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return claims;
    }


    /**
     * 根据荷载生成JWT TOKEN
     *
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setSubject(claims.get("CLAIM_KEY_USERNAME").toString())
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 生成token失效时间
     *
     * @return
     */
    private Date generateExpirationDate() {
        // 向后推7天
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    public static void main(String[] args) {
        Date date = new Date(System.currentTimeMillis() + 604800 * 1000);
        String s = LocalDateUtil.dateToString(date, "yyyy-MM-dd HH:mm:ss");
        System.out.println(s);
    }

}

2、配置文件

# 这个是 jwt 的配置
jwt:
  tokenHeader: Authorization
  secret: mdx-secrt000001
  expiration: 604800 #秒 7天
  prefix: Bearer
三、登录签发token

1、引入mysql和jpa依赖(redis也会用到)
注:此文章是连载文章,因为之前没有用到mysql,在这里登录要查数据库,所以在这里引入mysql

SpringCloud Alibaba 入门可以从这里看:
springcloud alibaba微服务工程搭建(保姆级)

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

注意: 因为gateway模块引用了webflux,webflux是无法使用mysql的,所以如果你的mysql是放在了最父级的pom中,启动gateway是会报错的,所以建议将mysql相关依赖放到其他子模块,或者可以在gateway启动类增加注解:@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

2、配置文件

server:
  port: 9090

spring:
  application:
    name: mdx-shop-user

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://127.0.0.1:3306/mdx_shop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    driverClassName: com.mysql.cj.jdbc.Driver
    username: root
    password: Bendi+Ceshi+

  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: mdx
        group: mdx
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard地址
        port: 8719

  redis:
    database: 0
    host: localhost
    port: 6379
    jedis:
      pool:
        max-active: 100
        max-idle: 3
        max-wait: -1
        min-idle: 0
    timeout: 2000

feign:
  sentinel:
    enabled: true

# 这个是 jwt 的配置
jwt:
  tokenHeader: Authorization
  secret: mdx-secrt000001
  expiration: 604800 #秒
  prefix: Bearer

3、表结构

/*
 Navicat Premium Data Transfer

 Source Server         : 本地2
 Source Server Type    : MySQL
 Source Server Version : 50724
 Source Host           : localhost:3306
 Source Schema         : mdx_shop

 Target Server Type    : MySQL
 Target Server Version : 50724
 File Encoding         : 65001

 Date: 09/08/2022 10:07:37
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for mdx_user
-- ----------------------------
DROP TABLE IF EXISTS `mdx_user`;
CREATE TABLE `mdx_user`  (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名',
  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码',
  `nick` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '昵称',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
  `email` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电子邮件',
  `status` int(1) NULL DEFAULT NULL COMMENT '状态 0 启用 1禁用',
  `sex` int(1) NULL DEFAULT NULL COMMENT '性别 01 女',
  `remarks` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '个人描述',
  `last_login_time` datetime(0) NOT NULL COMMENT '上次登录时间',
  `create_time` datetime(0) NOT NULL COMMENT '创建时间',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`user_id`) USING BTREE,
  INDEX `idx_phone`(`phone`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of mdx_user
-- ----------------------------
INSERT INTO `mdx_user` VALUES (1, 'admin', '$2a$10$c./nfmokuQSEn1KKbXbGw.AgTyT5a.Hs3O/qaXQ5BTjb8xRivgytK', '管理员', '13612345678', '123456789@qq.com', 0, 18, NULL, '2022-02-08 17:15:11', '2022-02-08 17:15:03', NULL);

SET FOREIGN_KEY_CHECKS = 1;

4、用户登录接口实现

jpa接口

/**
 * @author : jiagang
 * @date : Created in 2022/2/8 17:01
 */
@Repository
public interface MdxUserRepository extends JpaRepository<MdxUser,Long> {

    /**
     * 获取用户信息
     * @param userName
     * @return
     */
    MdxUser findByUserName(String userName);
}

service实现类

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private OrderFeign orderFeign;

    @Autowired
    private MdxUserRepository userRepository;

    @Autowired
    private JWTProvider jwtProvider;

    @Autowired
    private RedisManager redisManager;

    @Value("${jwt.prefix}")
    private String prefix;

    /**
     * 登录
     * @param mdxUserDTO
     * @return
     */
    @Override
    public LoginVo login(MdxUserDTO mdxUserDTO) {
        MdxUser mdxUser = userRepository.findByUserName(mdxUserDTO.getUserName());
        if (mdxUser == null){
            throw new BizException("用户不存在");
        }

        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        // 判断用户名密码是否正确
        if (StringUtils.isEmpty(mdxUser.getUserName()) ||
                ! encoder.matches(mdxUserDTO.getPassword(), mdxUser.getPassword())){
            throw new BizException("用户名或者密码错误");
        }
        // 生成token
        String token = jwtProvider.generateToken(mdxUser.getUserName());

        // 将token存入redis
        redisManager.set(UserConstant.USER_TOKEN_KEY_REDIS + mdxUser.getUserName(),token,604800);

        return LoginVo.builder()
                .userId(mdxUser.getUserId().toString())
                .userName(mdxUser.getUserName())
                .token(prefix + " " + token).build();
    }

    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String password = encoder.encode("admin");
        System.out.println(password);
    }

    @Override
    public String getOrderNo(String userId, String tenantId, HttpServletRequest request) {
        return orderFeign.getOrderNo(userId,tenantId, request.getHeader("token"));
    }
}


密码加密方法

public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String password = encoder.encode("admin");
        System.out.println(password);
    }

controller

@Autowired
    private UserService userService;

    /**
     * 登录
     * @param mdxUserDTO
     * @return
     */
    @PostMapping("login")
    public CommonResponse<LoginVo> login(@RequestBody MdxUserDTO mdxUserDTO){
        return CommonResponse.success(userService.login(mdxUserDTO));
    }

测试
成功返回jwt token
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第7张图片

SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第8张图片

四、filter拦截token验证,并对特殊接口放行

1、重新修改我们之前gateway服务中的filter

完成代码如下:

@Component
@Slf4j
public class MdxAuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisManager redisManager;

    @Autowired
    private JWTProvider jwtProvider;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.prefix}")
    private String prefix;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("=========================请求进入filter=========================");
        // 验证token
        String authHeader = exchange.getRequest().getHeaders().getFirst(tokenHeader);
        if (authHeader != null && authHeader.startsWith(prefix)){
            String authToken = authHeader.substring(prefix.length());
            String userName = jwtProvider.getUserNameFromToken(authToken);

            // 查询redis
            Object token = redisManager.get(UserConstant.USER_TOKEN_KEY_REDIS + userName);
            if (token == null){
                log.error("token验证失败或已过期...");
                return writeResponse(exchange.getResponse(),401,"token验证失败或已过期...请重新登录");
            }

            // 这里也可以使用 jwtProvider.validateToken() 来验证token,使用redis是因为管理员可以在任意时间将用户token踢出
            // 去除首尾空格
            String trimAuthToken = authToken.trim();
            if (! trimAuthToken.equals(token.toString())){
                log.error("token验证失败或已过期...");
                return writeResponse(exchange.getResponse(),401,"token验证失败或已过期...请重新登录");
            }
        }else {
            return writeResponse(exchange.getResponse(),500,"token不存在");
        }

        log.info("token验证成功...");
        return chain.filter(exchange);
    }

    /**
     * 值越小执行顺序越靠前
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * 构建返回内容
     *
     * @param response ServerHttpResponse
     * @param code     返回码
     * @param msg     返回数据
     * @return Mono
     */
    protected Mono<Void> writeResponse(ServerHttpResponse response, Integer code, String msg) {
        JSONObject message = new JSONObject();
        message.put("code", code);
        message.put("msg", msg);
        byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.OK);
        // 指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
}

2、测试

通过访问gateway服务路由到order服务的接口来看下效果

我们先不传token,访问接口(其中9010端口为gateway服务)
提示token不存在
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第9张图片

添加一个正确token,将我们上面登录时获取的token填入即可
成功返回订单服务结果
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第10张图片

再测试一个错误的token
可以看到返回401
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第11张图片

3、特殊接口放行

在一些实际业务中有一些接口是不用登录的,比如登录接口,注册接口等,所以我们需要对这些接口放行。

我们先试下通过网关访问登录接口,不做特殊接口处理的情况
提示我们token不存在
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第12张图片

然后我们对登录接口进行放行
配置文件添加如下配置,一般是将以下配置放到nacos配置中心

# 不用登录就可以访问的接口
allowed:
  paths: /mdx-shop-user/user/login

filter方法新增逻辑
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第13张图片
完整代码如下

/**
 * @author : jiagang
 * @date : Created in 2022/8/8 15:30
 */
@Component
@Slf4j
public class MdxAuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisManager redisManager;

    @Autowired
    private JWTProvider jwtProvider;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.prefix}")
    private String prefix;

    @Value("${allowed.paths}")
    private String paths; // 不需要登录就能访问的路径

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("=========================请求进入filter=========================");

        ServerHttpRequest request = exchange.getRequest();
        String requestPath = request.getPath().toString();

        boolean allowedPath = false;
        if (paths != null && !paths.equals("")){
            allowedPath = StringUtil.checkSkipAuthUrls(requestPath, paths.split(","));
        }
        if (allowedPath || StringUtils.isEmpty(requestPath)){
            return chain.filter(exchange);
        }

        // 验证token
        String authHeader = exchange.getRequest().getHeaders().getFirst(tokenHeader);
        if (authHeader != null && authHeader.startsWith(prefix)){
            String authToken = authHeader.substring(prefix.length());
            String userName = jwtProvider.getUserNameFromToken(authToken);

            // 查询redis
            Object token = redisManager.get(UserConstant.USER_TOKEN_KEY_REDIS + userName);
            if (token == null){
                log.error("token验证失败或已过期...");
                return writeResponse(exchange.getResponse(),401,"token验证失败或已过期...请重新登录");
            }

            // 这里也可以使用 jwtProvider.validateToken() 来验证token,使用redis是因为管理员可以在任意时间将用户token踢出
            // 去除首尾空格
            String trimAuthToken = authToken.trim();
            if (! trimAuthToken.equals(token.toString())){
                log.error("token验证失败或已过期...");
                return writeResponse(exchange.getResponse(),401,"token验证失败或已过期...请重新登录");
            }
        }else {
            return writeResponse(exchange.getResponse(),500,"token不存在");
        }

        log.info("token验证成功...");
        return chain.filter(exchange);
    }

    /**
     * 值越小执行顺序越靠前
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * 构建返回内容
     *
     * @param response ServerHttpResponse
     * @param code     返回码
     * @param msg     返回数据
     * @return Mono
     */
    protected Mono<Void> writeResponse(ServerHttpResponse response, Integer code, String msg) {
        JSONObject message = new JSONObject();
        message.put("code", code);
        message.put("msg", msg);
        byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.OK);
        // 指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
}

checkSkipAuthUrls工具类方法

/**
     * 将通配符表达式转化为正则表达式
     *
     * @param path
     * @return
     */
    public static String getRegPath(String path) {
        char[] chars = path.toCharArray();
        int len = chars.length;
        StringBuilder sb = new StringBuilder();
        boolean preX = false;
        for (int i = 0; i < len; i++) {
            if (chars[i] == '*') {// 遇到*字符
                if (preX) {// 如果是第二次遇到*,则将**替换成.*
                    sb.append(".*");
                    preX = false;
                } else if (i + 1 == len) {// 如果是遇到单星,且单星是最后一个字符,则直接将*转成[^/]*
                    sb.append("[^/]*");
                } else {// 否则单星后面还有字符,则不做任何动作,下一把再做动作
                    preX = true;
                    continue;
                }
            } else {// 遇到非*字符
                if (preX) {// 如果上一把是*,则先把上一把的*对应的[^/]*添进来
                    sb.append("[^/]*");
                    preX = false;
                }
                if (chars[i] == '?') {// 接着判断当前字符是不是?,是的话替换成.
                    sb.append('.');
                } else {// 不是?的话,则就是普通字符,直接添进来
                    sb.append(chars[i]);
                }
            }
        }
        return sb.toString();
    }

    public static boolean checkSkipAuthUrls(String reqPath,String[] skipAuthUrls) {
        for (String skipAuthUrl:skipAuthUrls) {
            if(wildcardEquals(skipAuthUrl, reqPath)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 通配符模式
     *
     * @param skipAuthUrl - 需要跳过的地址
     * @param reqPath   - 请求地址
     * @return
     */
    public static boolean wildcardEquals(String skipAuthUrl, String reqPath) {
        String regPath = getRegPath(skipAuthUrl);
        return Pattern.compile(regPath).matcher(reqPath).matches();
    }

再次访问登录接口
可以看到gateway已经放行,成功获取到结果
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证_第14张图片

创作不易,点个赞吧

最后的最后送大家一句话

白驹过隙,沧海桑田

与君共勉

上一篇文章
springcloud gateway的使用 + nacos动态路由

文章持续更新,可以关注下方公众号或者微信搜一搜「 最后一支迷迭香 」第一时间阅读,获取更完整的链路资料。回复【mdx-shop】查看项目源码

你可能感兴趣的:(SpringCloud,Alibaba,微服务,spring,cloud,gateway,java,jwt)