JWT加解密应用方案设计与实现

为什么要用令牌技术?

这个问题其实问的就是Cookice、Session、Token(令牌)之间的区别了。

首先,存放的位置做一下比较,Cookice小饼干存放在客户端的浏览器当中,Session会话存放在服务器线程当中(本质上还是需要利用Cookice实现),而Token存放位置不固定,但是一般是服务端存放在Redis中,客户端一般存放在Storage中(这个主要看业务定义,并不是固定的)。

其次,安全性做一下比较,Cookice存放在客户端当中容易被窃取或者修改,首当其冲安全性是最低的,但是Session不是也是基于Cookice实现吗?Session只是利用Cookice技术记录了Session会话ID而已,并没有什么特殊的敏感信息,但是也是会有泄露的风险,但没有篡改的风险,而Token只是一种概念设计,可自定义加密的算法以及内容,三者之中安全性还是有一定的保障的。

那为什么现在大部分的认证鉴权啥的都用的令牌技术呢?首先第一点是安全性比其他两者都好,其次现在有些场景不支持Cookice,比如说小程序、移动应用、桌面应用等等,而Token可以放在头部信息当中发给服务端进行校验,应用场景比其他两者范围广泛。

总结下来就是以下的五点优势,使用 令牌技术 相较于传统的基于会话的认证机制,可以减少服务器存储开销和管理复杂性,实现跨域支持和水平扩展,并且更适应无状态和微服务架构。

  1. 无需服务器存储状态: 传统的基于会话的认证机制需要服务器在会话中存储用户的状态信息,包括用户的登录状态、权限等。而使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都可以包含在 Token 中,使得系统可以更容易地进行水平扩展。
  2. 跨域支持: 由于 Token 包含了完整的认证和授权信息,因此可以轻松地在多个域之间进行传递和使用,实现跨域授权。
  3. 适应微服务架构: 在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 Token 可以满足这种需求,每次请求携带 Token 即可实现认证和授权。
  4. 自包含: Token 可以包含认证和授权信息,以及其他自定义的声明,这些信息都被编码在 Token 中,在服务端解码后使用。Token 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制。
  5. 扩展性: Token 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。

什么是JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络上安全传输信息的简洁、自包含的方式,它通常被用于身份验证和授权机制。

JWT 由三部分组成:头部(Header) 、载荷(Payload) 、 签名(Signature)。

头部(Header): 包含了关于生成该 JWT 的信息以及所使用的算法类型。

载荷(Payload): 包含了要传递的数据,例如身份信息和其他附属数据。JWT 官方规定了 7 个字段,分别是:签发者(Issuer)、主题(Subject)、接收者(Audience)、过期时间(Expiration time)、生效时间(Not Before)、签发时间(Issued At)、编号(JWT ID)

签名(Signature): 使用密钥对头部和载荷进行签名,以验证其完整性。

生成非对称加密证书

Java keytool 工具生成密钥对

Java JDK安装好之后再,/${Java_Home}/bin目录下就会有一个可执行文件kettool,该工具可以帮助我们生成非对称加解密的 密钥文件(私钥) 和 证书文件(公钥)。

生成密钥文件命令:keytool -genkey -alias IF010 -keyalg RSA -keysize 2048 -validity 365 -keystore if010-private.jks

生成证书文件命令:keytool -export -alias IF010 -file if010-public.cer -keystore if010-private.jks

命令选项解析:IF010是密钥的别名,if010-private.jks是密钥库文件的名称,if010-public.cer是导出的证书文件名称

  • keytool -genkey:用于生成密钥对。
  • -alias:设置密钥的别名。
  • -keyalg:设置密钥算法,常用的是RSA。
  • -keypass:设置密钥密码。
  • -storepass:设置密钥库密码。
  • -keystore:设置密钥库文件路径和名称。
  • -export:导出证书到文件。
  • -file:设置导出的证书文件名称。
  • -keysize:证书加密长度,越长则越难破译。
  • -validity:证书有效期,以天为单位。
[root@localhost Desktop]# keytool -genkey -alias IF010 -keyalg RSA -keysize 2048 -validity 365 -keystore if010-private.jks
输入密钥库口令:  
再次输入新口令: 
输入唯一判别名。提供单个点 (.) 以将子组件留空,或按 ENTER 以使用大括号中的默认值。
您的名字与姓氏是什么?
  [Unknown]:  IF010
您的组织单位名称是什么?
  [Unknown]:  IF010
您的组织名称是什么?
  [Unknown]:  IF010
您所在的城市或区域名称是什么?
  [Unknown]:  GZ
您所在的省/市/自治区名称是什么?
  [Unknown]:  GD
该单位的双字母国家/地区代码是什么?
  [Unknown]:  CN
CN=IF010, OU=IF010, O=IF010, L=GZ, ST=GD, C=CN是否正确?
  []:  Y

正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA384withRSA) (有效期为 365):
	 CN=IF010, OU=IF010, O=IF010, L=GZ, ST=GD, C=CN

[root@localhost Desktop]# keytool -export -alias IF010 -file if010-public.cer -keystore if010-private.jks
输入密钥库口令:  
存储在文件 <if010-public.cer> 中的证书

方案设计

这里只是提供一种设计方案,且建立在Spring Cloud微服务架构当中,在实际的开发场景当中还是需要结合业务本身

实现思路

  1. 用户携带账号密码请求网关,网关转发请求到认证服务中心进行校验,校验通过则生成 全局唯一的UUID Token使用私钥生成的JWT Token (这里注意的是生成JWT Token时,里面必须包含用户唯一身份标识),最后只将UUID Token交给客户端;
  2. 用户拿到返回的UUID Token后,将携带该Token的请求发送给网关,网关进行校验或者网关不进行校验转发给服务进行校验(有些场景是交给网关进行校验后拿到唯一的身份用户标识传递给下面的微服务,降低微服务的计算压力,这种操作其实是非常不建议的,存在极大的安全风险,假设我知道了用户的唯一身份标识,内部人员在内网可以直接对微服务发起请求,当然也是可以配置白名单进行防范);
  3. 网关获取到该请求携带的UUID Token后,可在Reids当中取得对应JWT Token;
  4. 网关将取得的JWT Token使用公钥进行解密,解密失败则表示Token失效了,返回告知需重新认证登录,反之解析成功的话,网关将得到该用户的唯一身份标识,用唯一身份标识从Redis中取出用户最后一次登录所生成的UUID Token;
  5. 将用户最后一次登录所生成的UUID Token 与该次携带的UUID Token进行比较,如果相同这代表该次请求正常,反之表明有新的客户端使用了该身份登录

注意:非对称加密场景当中,认证服务拿私钥加密,网关用公钥解密,认证服务存在于内网当中,私钥相对于比较安全,而公钥放在整个网络最边缘的网关当中泄露了也是无法伪造,这就是非对称加密为何会比较安全了

这里为什么非要UUID Token多次一举呢?

1、用于防止多客户端同时在线所带来的安全性问题,如果业务允许在不同的设备同时登陆在线,我们可以考虑别的方案。

2、如果我们将Token放在请求头部,这数据长度不能超过最大长度4KB(4096字节),对我我们非对称加密的JWT Token而言非常的紧张。

方案实现

方案实现代码结构

这里是将Jwt Token的加解密校验拦截 做成了一个通用模块,可能会显得臃肿了些,但是这是为了可以更加清晰分明方便理解,在实际当中可以对某些地方加以改造和优化

约定:

1、以下有引用到HuTool工具包进行加解密

2、以下的RedisService是自己制作的通用模块,参考了若依的微服务框架里的通用模块,有兴趣的可以连接了解一下

3、拦截器部分添加了路径白名单的机制,定义了那些请求需要校验,那些不需要校验,这个可以根据业务需求进行调整定义

4、配置属性、公钥和私钥存储的位置可以根据业务进行自定义的调整

加解密部分 - 定义配置类

package com.if010.common.security.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;

/**
 * 【配置】Jwt配置类
 * @Author Kim同学
 */
@Data
@ConfigurationProperties(prefix = "security.jwt")
public class JwtProperties {
    /**
     * 加密模式 (对称加密: symmetry | 非对称加密: asymmetric)
     */
    private String mode;

    /**
     * 私钥文件位置
     */
    private Resource privateKeyLocation;

    /**
     * 签名算法
     * 对称签名: HS256(HmacSHA256)、HS384(HmacSHA384)、HS512(HmacSHA512)
     * 非对称签名: RS256(SHA256withRSA)、RS384(SHA384withRSA)、RS512(SHA512withRSA)、ES256(SHA256withECDSA)、ES384(SHA384withECDSA)、ES512(SHA512withECDSA)
     */
    private String algorithm;

    /**
     * 私钥文件访问密码, 如果采用的是对称加密,那么这个用作 密钥配置
     */
    private String password;

    /**
     * 证书别名
     */
    private String alias;

    /**
     * 证书类型
     */
    private String type;

    /**
     * 证书格式
     */
    private String format;

    /**
     * token有效期
     */
    private Long expire;
}

加解密部分 - 定义配置注册类

package com.if010.common.security.config;

import com.alibaba.fastjson2.JSONObject;
import com.if010.common.core.constant.TokenConstants;
import com.if010.common.redis.service.RedisService;
import com.if010.common.security.properties.JwtProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import java.security.KeyPair;
import java.security.PublicKey;

/**
 * 安全配置注册类
 * @author Kim同学
 */

@Configuration
@Slf4j
@RequiredArgsConstructor
@EnableConfigurationProperties(JwtProperties.class)
public class SecurityConfig {

    /**
     * 注入Redis服务类
     */
    private final RedisService redisService;


    /**
     * 密码加密器 (加了点盐巴~)
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * JWT 密钥工厂容器,通过 security.jwt.mode 配置项决定使用对称加密还是非对称加密,非对称则注入
     */
    @Bean
    @ConditionalOnProperty(name = "security.jwt.mode", havingValue = "asymmetric")
    public KeyPair keyPair(JwtProperties properties) {
        // 判断是否有配置密钥库文件,有则表示可以注入完整的公私密钥对,没有配置则只能从redis里读取公钥证书进行解密验证jwt
        if (properties.getPrivateKeyLocation() != null) {
            // 获取秘钥工厂
            KeyStoreKeyFactory keyStoreKeyFactory =
                    new KeyStoreKeyFactory(
                            properties.getPrivateKeyLocation(),
                            properties.getPassword().toCharArray());

            KeyPair keyPair = keyStoreKeyFactory.getKeyPair(
                    properties.getAlias(),
                    properties.getPassword().toCharArray());

            // 缓存公钥
            redisService.setCacheObject(TokenConstants.TOEKN_PUBLIC_KEY, keyPair.getPublic());
  
            //返回钥匙对
            return keyPair;
        } else {
            // 尝试读取公钥证书文件
            try {
                PublicKey publicKey = JSONObject.parseObject(redisService.getCacheObject(TokenConstants.TOEKN_PUBLIC_KEY).toString(), PublicKey.class);
                return new KeyPair(publicKey, null);
            } catch (Exception e) {
                throw new RuntimeException("获取公钥证书失败, {}", e);
            }
        }
    }
}

加解密部分 - 服务接口

package com.if010.common.security.service;

import java.util.Map;

/**
 * 【服务-接口】Jwt加解密服务接口
 * @author Kim同学
 */

public interface JwtCryptionService {
    /**
     * 创建令牌方法
     */
    String createToken(Map<String, Object> claims);

    /**
     * 解析令牌方法
     */
    Map<String, Object> parseToken(String token);
}
加解密部分 - (非对称加解密) 服务接口服务实现
package com.if010.common.security.service.impl;

import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.if010.common.core.constant.HttpStatus;
import com.if010.common.core.constant.TokenConstants;
import com.if010.common.core.exception.UnauthorizedException;
import com.if010.common.redis.service.RedisService;
import com.if010.common.security.properties.JwtProperties;
import com.if010.common.security.service.JwtCryptionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

import java.security.KeyPair;
import java.util.Date;
import java.util.Map;

/**
 * 【服务-实现】Jwt(非对称对称加解密)服务接口实现类
 * @author Kim同学
 */

@Slf4j
@Service
@ConditionalOnProperty(name = "security.jwt.mode", havingValue = "asymmetric")
public class JwtCryptionServiceImplByAsymmetric implements JwtCryptionService {

    // 初始化定义 JWTSigner
    private final JWTSigner jwtSigner;

    // 注入 Jwt 配置类
    private final JwtProperties properties;

    // 注入 Redis 服务
    private final RedisService redisService;

    @Autowired
    public JwtCryptionServiceImplByAsymmetric(KeyPair keyPair, JwtProperties properties, RedisService redisService) {
        this.properties = properties;
        this.jwtSigner = JWTSignerUtil.createSigner(properties.getAlgorithm(), keyPair);
        this.redisService = redisService;
    }

    /**
     * JWT 生成方法
     */
    @Override
    public String createToken(Map<String, Object> claims) {
        // 1.生成jws
        return JWT.create()
                .setPayload("claims", claims)
                .setExpiresAt(
                        new Date(System.currentTimeMillis() + (
                                Long.parseLong(redisService.getCacheObject(TokenConstants.TOEKN_KEY_EXPIRE)) * 1000L)
                        )
                )
                .setSigner(jwtSigner)
                .sign();
    }

    /**
     * JWT 解密方法
     */
    @Override
    public Map<String, Object> parseToken(String token) {
        // 1.校验token是否为空
        if (token == null) {
            throw new UnauthorizedException("未登录", HttpStatus.UNAUTHORIZED);
        }

        // 2.校验并解析jwt
        JWT jwt;
        try {
            jwt = JWT.of(token).setSigner(jwtSigner);
        } catch (Exception e) {
            throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED);
        }

        // 3.校验是否过期
        try {
            JWTValidator.of(jwt).validateDate();
        } catch (ValidateException e) {
            throw new UnauthorizedException("Token令牌已经过期", HttpStatus.FORBIDDEN);
        }

        // 4.数据格式校验
        Object claims = jwt.getPayload("claims");
        if (claims == null) {
            // 数据为空
            throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED);
        }

        // 5.数据解析
        try {
            return (Map<String, Object>) claims;
        } catch (RuntimeException e) {
            // 数据格式有误
            throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED);
        }
    }
}
加解密部分 - (对称加解密) 服务接口服务实现
package com.if010.common.security.service.impl;

import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.if010.common.core.constant.HttpStatus;
import com.if010.common.core.constant.TokenConstants;
import com.if010.common.core.exception.UnauthorizedException;
import com.if010.common.redis.service.RedisService;
import com.if010.common.security.properties.JwtProperties;
import com.if010.common.security.service.JwtCryptionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Map;

/**
 * 【服务-实现】Jwt(对称对称加解密)服务接口实现类
 * @author Kim同学
 */

@Service
@ConditionalOnProperty(name = "security.jwt.mode", havingValue = "symmetry")
public class JwtCryptionServiceImplBySymmetry implements JwtCryptionService {

    // 初始化定义 JWTSigner
    private final JWTSigner jwtSigner;

    // 注入 Jwt 配置类
    private final JwtProperties properties;

    // 注入 Redis 服务
    private final RedisService redisService;

    /**
     * 构造方法注入 Jwt 配置类, 初始化 JWTSigner
     */
    @Autowired
    public JwtCryptionServiceImplBySymmetry(JwtProperties properties, RedisService redisService) {
        this.properties = properties;
        this.jwtSigner = JWTSignerUtil.createSigner(properties.getAlgorithm(), properties.getPassword().getBytes());
        this.redisService = redisService;
    }

    /**
     * JWT 生成方法
     */
    @Override
    public String createToken(Map<String, Object> claims) {
        return JWT.create()
                .setPayload("claims", claims)
                .setExpiresAt(
                        new Date(System.currentTimeMillis() + (
                                Long.parseLong(redisService.getCacheObject(TokenConstants.TOEKN_KEY_EXPIRE)) * 1000L)
                        )
                )
                .setSigner(jwtSigner)
                .sign();
    }


    /**
     * JWT 解析方法
     */
    @Override
    public Map<String, Object> parseToken(String token) {
        // 1.校验token是否为空
        if (token == null) {
            throw new UnauthorizedException("未登录", HttpStatus.UNAUTHORIZED);
        }

        // 2.校验并解析jwt
        JWT jwt;
        try {
            jwt = JWT.of(token).setSigner(jwtSigner);
        } catch (Exception e) {
            throw new UnauthorizedException("无效的token", HttpStatus.UNAUTHORIZED);
        }

        // 2.校验jwt是否有效
        if (!jwt.verify()) {
            // 验证失败
            throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED);
        }

        // 3.校验是否过期
        try {
            JWTValidator.of(jwt).validateDate();
        } catch (ValidateException e) {
            throw new UnauthorizedException("Token令牌已经过期", HttpStatus.FORBIDDEN);
        }

        // 4.数据格式校验
        Object claims = jwt.getPayload("claims");
        if (claims == null) {
            // 数据为空
            throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED);
        }

        // 5.数据解析
        try {
            return (Map<String, Object>) claims;
        } catch (RuntimeException e) {
            // 数据格式有误
            throw new UnauthorizedException("无效的Token令牌", HttpStatus.UNAUTHORIZED);
        }
    }
}

拦截校验部分 - 定义拦截过滤器

package com.if010.common.security.interceptor;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.if010.common.core.constant.CacheConstants;
import com.if010.common.core.constant.Constants;
import com.if010.common.core.constant.HttpStatus;
import com.if010.common.core.constant.TokenConstants;
import com.if010.common.core.exception.UnauthorizedException;
import com.if010.common.core.utils.UserContext;
import com.if010.common.redis.service.RedisService;
import com.if010.common.security.service.JwtCryptionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Toekn 校验拦截器
 * @author Kim同学
 */
@Slf4j
@RequiredArgsConstructor
public class TokenAuthInterceptor implements HandlerInterceptor {
    // AntPathMatcher,用于请求比较 (Spring提供的一个工具类)
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    // 注入JWT加密解密工具类
    private final JwtCryptionService jwtCryptionService;

    // 注入Redis服务类
    private final RedisService redisService;

    // 注入 Spring Boot 环境变量
    private final Environment environment;

    /**
     * Toekn 校验拦截器
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1、判断请求路径是否在白名单中
        if (isAllowPath(request)) {
            return true;
        }

        // 1、通过请求头中的 authorization UUID Token 从 Redis 中取出 Jwt Token
        String uuidToken = request.getHeader(TokenConstants.AUTHENTICATION);
        String jwtToken = redisService.getCacheObject(TokenConstants.TOEKN_KEY_PREFIX+uuidToken);
        if (jwtToken == null) {
            throw new UnauthorizedException("未查询到有效的登录信息, 拒绝服务请求 !", HttpStatus.UNAUTHORIZED);
        }

        // 2、检验解析Token
        Map<String, Object> userTokenClaims = null;
        try {
            userTokenClaims = jwtCryptionService.parseToken(jwtToken);
        } catch (Exception e) {
            throw new UnauthorizedException("未知身份, 拒绝服务请求 !", HttpStatus.FORBIDDEN);
        }

        // 3、判断系统是否配置了不能多终端同时登录
        Boolean isAllowDifferentDrvieLogin = Boolean.parseBoolean(redisService.getCacheObject(CacheConstants.SYS_CONFIG_SECURITY_PREFIX + "AllowDifferentDrvieLogin"));
        if (!isAllowDifferentDrvieLogin) {
            // 通过解析Token拿到userId,用userId获取最新一次登录所获取的UUID Token
            String lastUuidToken = redisService.getCacheObject(TokenConstants.TOEKN_KEY_PREFIX + userTokenClaims.get("userId").toString()).toString();
            // 判断当前的UUID Token是否与最新颁发的Token一致,不一致则删除redis中的 旧 Token 存储信息,并抛出异常
            if (!lastUuidToken.equals(uuidToken)) {
                redisService.deleteObject(TokenConstants.TOEKN_KEY_PREFIX + uuidToken);
                throw new UnauthorizedException("该账号已在其他设备登录, 请重新登录 !", HttpStatus.FORBIDDEN);
            }
        }

        //3、将 Jwt Token 解析成 User 信息存入线程当中
        HashMap<String, Object> userTempText = new HashMap<>();
        userTempText.put("userId", userTokenClaims.get("userId").toString());
        userTempText.put("userToken", uuidToken);
        UserContext.setUserTempText(userTempText);

        // 4、放行
        return true;
    }

    /**
     * 清除线程中的用户信息
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清除线程中的 User 信息, 防止内存泄露
        UserContext.removeUserTempText();
    }

    /**
     * 判断请求是否命中白名单
     */
    private boolean isAllowPath(HttpServletRequest request) {
        boolean flag = false;

        // 从 Redis 中取出 白名单 配置列表
        String authExcludePathsStr = redisService.getCacheObject(CacheConstants.SYS_CONFIG_SECURITY_AUTH_EXCLUDE_PATH);
        List<String> authExcludePathsJsonArray = JSON.parseArray(authExcludePathsStr, String.class);

        log.info("isAllowPath: {}", authExcludePathsJsonArray.toString());

        // 判断白名单是否为空
        if (authExcludePathsJsonArray == null || authExcludePathsJsonArray.isEmpty()) {
            return flag;
        }

        // 1.获取当前路径 和 请求方法
        String method = request.getMethod();
        String path = request.getRequestURI();
        String serviceName = environment.getProperty("spring.application.name");
        // 2.要放行的路径
        for (String excludePath : authExcludePathsJsonArray) {
            log.info("当前请求方法: {}, 当前请求路径: {}, 当前校验白名单条目: {}", method, Constants.API_PREFIX + serviceName + path, excludePath);
            boolean isMatch = antPathMatcher.match(excludePath, method + ":" + Constants.API_PREFIX + serviceName + path);
            if(isMatch){
                flag = true;
                break;
            }
        }
        return flag;
    }
}

拦截检验部分 - 注册拦截过滤器

package com.if010.common.security.config;

import com.if010.common.redis.service.RedisService;
import com.if010.common.security.interceptor.TokenAuthInterceptor;
import com.if010.common.security.service.JwtCryptionService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Mvc 注册配置类
 * @author Kim同学
 */

@Configuration
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {

    // 注入JWT加密解密工具类
    private final JwtCryptionService jwtCryptionService;

    // 注入Redis服务类
    private final RedisService redisService;

    // 注入 Spring Boot 环境变量
    private final Environment environment;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 注册 Token校验拦截器
        registry.addInterceptor(new TokenAuthInterceptor(jwtCryptionService, redisService, environment));
    }
}

到这里模块基本完成,微服务应用应用后并在配置文件当中加以必要项的配置即可食用,而认证方面这里就不做另外的讲解了,基本就是调用判定一下用户名密码是否正确,正确的话就调用一下createToken的方法传入一个Map集合即可,Map里面装什么根据业务需求定义即可~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/922250.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据集-目标检测系列- 安全背心 检测数据集 safety_vests >> DataBall

数据集-目标检测系列- 安全背心 检测数据集 safety DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。 贵在坚持&#xff01; 数据样例项目地址&#xff1a; * 相关项目 1&#xff09;数据集可视化项目&#xff1a;gi…

C语言菜鸟入门·关键字·int的用法

目录 1. int关键字 1.1 取值范围 1.2 符号类型 1.3 运算 1.3.1 加法运算() 1.3.2 减法运算(-) 1.3.3 乘法运算(*) 1.3.4 除法运算(/) 1.3.5 取余运算(%) 1.3.6 自增()与自减(--) 1.3.7 位运算 2. 更多关键字 1. int关键字 int 是一个关键字&#xff0…

unity中:超低入门级显卡、集显(功耗30W以下)运行unity URP管线输出的webgl程序有那些地方可以大幅优化帧率

删除Global Volume&#xff1a; 删除Global Volume是一项简单且高效的优化措施。实测表明&#xff0c;这一改动可以显著提升帧率&#xff0c;甚至能够将原本无法流畅运行的场景变得可用。 更改前的效果&#xff1a; 更改后的效果&#xff1a; 优化阴影和材质&#xff1a; …

Vue + Websocket播放PCM(base64转ArrayBuffer、 字符串转ArrayBuffer)

文章目录 引言I 音视频处理相关概念和APIII 案例:基于开源库 pcm-player方式播放借助MediaSource和Audio对象播放音频流。基于原生api AudioContext 播放操作III 格式转换js字符串转ArrayBufferbase64 转 ArrayBufferIV 解决pcm-player分片播放问题引言 需求: 基于webscoket传…

【JavaEE进阶】SpringBoot 快速上⼿

了解Maven,并配置国内源 使⽤SpringBoot创建⼀个项⽬, 输出HelloWorld 一、Maven 1.什么是Maven 官⽅对于Maven的描述: Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can man…

QT QFormLayout控件 全面详解

本系列文章全面的介绍了QT中的57种控件的使用方法以及示例&#xff0c;包括 Button(PushButton、toolButton、radioButton、checkBox、commandLinkButton、buttonBox)、Layouts(verticalLayout、horizontalLayout、gridLayout、formLayout)、Spacers(verticalSpacer、horizonta…

PCA算法所体现的核心数学思维

一、PCA算法的基本思想 PCA算法的核心思想是通过线性变换&#xff0c;将数据从原始的高维空间投影到低维空间&#xff0c;同时尽可能保留数据的主要变异性。这种变换是通过找到一组新的坐标轴&#xff08;即主成分&#xff09;来实现的&#xff0c;这些坐标轴是原始数据空间的…

如何解决pdf.js跨域从url动态加载pdf文档

摘要 当我们想用PDF.js从URL加载文档时&#xff0c;将会因遇到跨域问题而中断&#xff0c;且是因为会触发了PDF.js和浏览器的双重CORS block&#xff0c;这篇文章将会介绍&#xff1a;①如何禁用pdf.js的跨域&#xff1f;②如何绕过浏览器的CORS加载URL文件&#xff1f;②如何使…

C语言数据结构——详细讲解 双链表

从单链表到双链表&#xff1a;数据结构的演进与优化 前言一、单链表回顾二、单链表的局限性三、什么是双链表四、双链表的优势1.双向遍历2.不带头双链表的用途3.带头双链表的用途 五、双链表的操作双链表的插入操作&#xff08;一&#xff09;双链表的尾插操作&#xff08;二&a…

Java小白成长记(创作笔记二)

目录 序言 思维导图 续 用户登录/注册 数据表 实体层 持久层 服务层 认证与授权 整合springsecurity controller注册测试 controller登录测试 跨域解决 方法 Java小白成长记&#xff08;创作笔记一&#xff09; Java小白成长记&#xff08;创作笔记二&#xff09;…

案例研究|阿特斯的JumpServer分布式部署和多组织管理实践

苏州阿特斯阳光电力科技有限公司&#xff08;以下简称为阿特斯&#xff09;是一家集太阳能光伏组件制造和为全球客户提供太阳能应用产品研发、设计、制造、销售的专业公司。 阿特斯集团总部位于加拿大&#xff0c;中国区总部位于江苏省苏州市。通过全球战略和多元化的市场布局…

20241123-四元数高阶奇异值分解-(1)

四元数高阶奇异值分解及其在彩色图像处理中的应用-(1) &#x1f4d4; 声明 &#x1f1e8;&#x1f1f3; : 1️⃣ &#x1f4c3; 原文网址链接: 四元数高阶奇异值分解及其在彩色图像处理中的应用 - ScienceDirect &#x1f517; Quaternion … image processing (arxiv.org) ​ …

游戏引擎学习第20天

视频参考:https://www.bilibili.com/video/BV1VkBCYmExt 解释 off-by-one 错误 从演讲者的视角&#xff1a;对代码问题的剖析与修复过程 问题的起因 演讲者提到&#xff0c;他可能无意中在代码中造成了一个错误&#xff0c;这与“调试时间标记索引”有关。他发现了一个逻辑问题…

python开发之Linux

文章目录 1. 基础2. 进阶链接压缩/解压缩 文件权限用户远程操作编辑文件软件安装 1. 基础 # 查看当前目录下文件 ls# 查看当前目录 pwd# 清除界面内容 clear# 切换目录 cd# 创建目录 mkdir# 创建文件 touch 文件 vi 文件# 强制删除 rm -rf # 复制文件 cp 复制文件 复制文件路径…

Docker2:docker快速入门(部署MySQL)

欢迎来到“雪碧聊技术”CSDN博客&#xff01; 在这里&#xff0c;您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者&#xff0c;还是具有一定经验的开发者&#xff0c;相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导&#xff0c;我将…

oracle的静态注册和动态注册

oracle的静态注册和动态注册 静态注册&#xff1a; 静态注册 : 指将实例的相关信息手动告知 listener 侦 听 器 &#xff0c; 可以使用netmgr,netca,oem 以及直接 vi listener.ora 文件来实现静态注册&#xff0c;在动态注册不稳定时使用&#xff0c;特点是&#xff1a;稳定&…

杰发科技AC7840——EEP中RAM的配置

sample和手册中示例代码的sram区地址定义不一样 这个在RAM中使用没有限制&#xff0c;根据这个表格留下足够空间即可 比如需要4096字节的eep空间&#xff0c;可以把RAM的地址改成E000&#xff0c;即E000-EFFF&#xff0c;共4096bytes即可。

洛谷 P1616 疯狂的采药 C语言 记忆化搜索

题目&#xff1a; https://www.luogu.com.cn/problem/P1616?contestId215526 完全背包问题&#xff0c;最后一个超出空间了。完全背包和就是无限次的拿&#xff0c;公式跟01背包差不多。 但是&#xff0c;只有当前能拿和拿不下&#xff0c;换下一个。注意要处理好边界条件。…

分布式 Data Warebase - 构筑 AI 时代数据基石

导读&#xff1a;作者以人类世界一个信息层次模型 DIKW 为出发点&#xff0c;引出对计算机世界&#xff08;系统&#xff09;处理数据过程的介绍。接着以一个民宿平台数据架构随业务发展而不断演进的过程&#xff0c;展示了这场信息革命中&#xff0c;在具体应用场景下&#xf…

zotero7 插件使用

zotero style 1、下载地址 Zotero 插件商店 | Zotero 中文社区 2、配置 在工具插件里 3、配置 style 进入高级→设置编辑器 查找 easy 设置完即可显示&#xff0c; 注1&#xff1a;easyscholar的密钥要自行申请注册&#xff0c;注册地址&#xff1a;easySchol…