JWT整合springboot自定义定时更换秘钥方式

 更新时间:2025年11月18日 08:53:20   作者:骑猪少年  
文章介绍了JWT( JSON Web Token)的基本概念、结构和使用场景,并重点讲解了在Spring Boot中自定义定时更换JWT秘钥的过程,通过定义用户信息类、JWT工具类、配置拦截器和定时修改秘钥工具类,实现了一个基本的JWT认证和密钥管理机制

JWT整合springboot自定义定时更换秘钥

jwt概要:

JWT(JSON WEB TOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。

它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。广义上讲JWT是一个标准的名称;狭义上JWT指的就是用来传递的那个token字符串。

jwt是一种无状态token,可用于oss单点登录

JWT的数据结构以及签发的过程

JWT由三部分构成:header(头部)、payload(载荷)和signature(签名)。

  • Header 头部信息:指定类型和算法
  • Payload 荷载信息:存放Claims声明信息
  • Signature签名:把前两者对应的Json结构进行base64url编码之后的字符串拼接起来和密钥放一起加密后的签名 组成方式为header.payload.signature

Header的结构

  • type 声明类型
  • algorithm 声明加密算法
  • 格式: {“typ”: “JWT”,“alg”: “HS256”}

payload的结构

  • 声明信息
  • payload用来承载要传递的数据,它的json结构实际上是对JWT要传递的数据的声明,这些声明被JWT标准称为claims(声明),它的每个属性键值对其实就是一个claim,JWT常用的有两种声明,一种是
  • Reserved claims(保留声明),也就是JWT规定的标准声明。
  • 一种是Private claims(自定义声明),我们在这里定义要传递的信息
  • 还有一种是public claims(公共声明),这个目前没用到。

标准声明(JWT保留声明)

  • iss(Issuser):代表这个JWT的签发主体;
  • sub(Subject):代表这个JWT的主体,即它的所有人;
  • aud(Audience):代表这个JWT的接收对象;
  • exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
  • nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
  • iat(Issuedat):是一个时间戳,代表这个JWT的签发时间; jti(JWT ID):是JWT的唯一标识。

signature

  • 签名
  • 把header和payload对应的json结构进行base64url编码之后得到的字符串用点号拼接起来,然后根据header里面alg指定的签名算法生成出来的,然后添加自己设定的key进行加密签名

java代码实现

maven依赖

<dependency>
	<groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

定义一个用户信息类,存放一些用户基本信息(如用户名、权限(可进行菜单鉴权))

/**
 * @author : ljt
 * @version V1.0
 * @Description: 用户信息
 * @date Date : 2021年08月06日 13:56
 */
@Data
public class UserLoginBO {
    /**
     * 用户id
     */
    private Long id;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 用户昵称
     */
    private String nickName;
    /**
     * 用户真实姓名
     */
    private String realName;

    /**
     * 用户openid
     */
    private String openId;
    /**
     * 用户token
     */
    private String accessToken;

}

JWT工具类:

/**
 * @author : ljt
 * @version V1.0
 * @Description: jwt工具类
 * @date Date : 2021年08月06日 13:22
 */
@Component
public class JwtTokenUtil {

    /**
     * jwt生成token秘钥,此处动态更新所以为空,可随便自定义
     */
    public static String TOKEN_SECRET = "";

    /**
     * 定义token有效期 秒
     */
    public static Integer tokenExpiration=1800;

    /**
     * 生成token
     * @param subject  用户名
     * @param claims 用户信息
     * @return token字符串
     */
    public static String generateToken(String subject, Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(new Date())
                .setExpiration(generateExpirationDate(tokenExpiration))
                .compressWith(CompressionCodecs.DEFLATE)
                .signWith(SignatureAlgorithm.HS256, TOKEN_SECRET)
                .compact();
    }

    /***
     * 解析token 信息
     * @param token token字符串
     * @return 用户map信息
     */
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(TOKEN_SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }


    /**
     * 生成失效时间
     * @param expiration 失效时长
     * @return 到期时间 时间格式
     */
    private static Date generateExpirationDate(long expiration) {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 根据token 获取用户信息
     * @param token token
     * @return 自定义的用户对象
     */
    public static UserLoginBO getUserFromToken(String token) {
        UserLoginBO userDetail;
        try {
            final Claims claims = getClaimsFromToken(token);
            userDetail = new UserLoginBO();
            userDetail.setId(Long.parseLong(claims.get("id").toString()));
            userDetail.setUserName(String.valueOf(claims.get("userName")));
            userDetail.setNickName(String.valueOf(claims.get("nickName")));
            userDetail.setRealName(String.valueOf(claims.get("UserName")));
            userDetail.setOpenId(String.valueOf(claims.get("openId")));
        } catch (Exception e) {
            userDetail = null;
        }
        return userDetail;
    }

    /**
     * 根据token 获取用户ID
     * 和获取用户信息一致 此处新定义一个方法是用着方便
     * @param token token
     * @return 返回用户id
     */
    private Long getUserIdFromToken(String token) {
        Long userId;
        try {
            final Claims claims = getClaimsFromToken(token);
            userId = Long.parseLong(claims.get("id").toString());
        } catch (Exception e) {
            userId = 0L;
        }
        return userId;
    }
    /**
     * 根据token 获取用户名
     * 和获取用户信息一致 此处新定义一个方法是用着方便
     * @param token token字符串
     * @return 用户昵称
     */
    public static String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 刷新token
     *
     * @param token 原token
     * @return 新token
     */
    public static String refreshToken(String token,Integer tokenExpiration) {
        String refreshedToken;
        try {
            final Claims claims = getClaimsFromToken(token);
            refreshedToken = generateToken(claims.getSubject(), claims,tokenExpiration);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 根据token 获取生成时间
     * @param token token字符串
     * @return 时间
     */
    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = getClaimsFromToken(token);
            created = claims.getIssuedAt();
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    /**
     * 根据token 获取过期时间
     * @param token token
     * @return 时间
     */
    public static Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }


    /**
     * 验证token 是否合法
     * @param token  token
     * @param userDetail  用户信息
     * @return true 或 false
     */
    public boolean validateToken(String token, UserLoginBO userDetail) {
        final long userId = getUserIdFromToken(token);
        final String username = getUsernameFromToken(token);
        return (userId == userDetail.getId()
                && username.equals(userDetail.getUserName())
                && !isTokenExpired(token)
        );
    }

    /**
     * 判断令牌是否过期
     * @param token 令牌
     * @return 是否过期
     */
    public static Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }



}

配置拦截器:

@Slf4j
public class LoginInterceptor extends HandlerInterceptorAdapter {

    /**
     * token剩余过期时间
     * 此处设定剩余到期时间自动刷新token
     * 根据需要手动编辑刷新token接口
     */
    private final Long TOKEN_DATE = 5*60*1000L;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //token约定放置在请求头中以access_token的方式发送
        String token = request.getHeader("access_token");
        log.debug("用户请求鉴权token = {}", token);
        //判断token是否存在 不存在无权限 直接返回
        if(StringUtils.isNotEmpty(token))   {
            //工具类 直接获取用户信息
            UserLoginBO userFromToken = JwtTokenUtil.getUserFromToken(token);
            //获取不到 用户授权过期 直接返回
            if (userFromToken != null && !JwtTokenUtil.isTokenExpired(token)) {
                //TODO 能正常获取用户信息 可判断用户信息是否一致 一致则鉴权成功 这里我懒省事
              
                //判断token剩余过期时间 将过期自动签发新token 以response方式返回
                if ( JwtTokenUtil.getExpirationDateFromToken(token).getTime() - System.currentTimeMillis() < TOKEN_DATE ){
                    String refreshToken = JwtTokenUtil.refreshToken(token,JwtTokenUtil.tokenExpiration);
                    Object value = CacheUtil.getInstance().getValue(token);
                    CacheUtil.getInstance().putValue(refreshToken,value, CacheConstant.PERMS_INFO);
                    response.setContentType("text/html; charset=UTF-8");
                    Map<String,String> map = new HashMap<>(16);
                    map.put("accessToken",refreshToken);
                    response.setHeader("access_token",refreshToken);
                }
                return true;
            }
        }
        response.setContentType("text/html; charset=UTF-8");
        ResponseBO responseBO = new ResponseBO();
        responseBO.setCode(ResponseCode.L001.getCode());
        responseBO.setMsg(ResponseCode.L001.getTitle());
        response.getWriter().write(JSONObject.toJSONString(responseBO));
        return false;
    }
}

注册拦截器,定义拦截规则

@Configuration
public class AuthConfigurer extends WebMvcConfigurationSupport {
    /**
     * 拦截器注入
     */
    @Bean
    public LoginInterceptor loginInterceptor() {
        return new LoginInterceptor();
    }

    /**
     * 添加拦截
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry
         		//注册拦截器 定义拦截接口
        		.addInterceptor(loginInterceptor()).addPathPatterns("/**")
                // 排除登录接口拦截
                .excludePathPatterns("/user/login")
        super.addInterceptors(registry);
    }


    /**
     * 替换Spring默认JSON转换器为fastjson
     * @param converters
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
        fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        converters.add(fastConverter);

    }

   
    /**
     * 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。 需要重新指定静态资源
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        //将所有/static/** 访问都映射到classpath:/static/ 目录下
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");

    }


    /**
     * 配置servlet处理
     */
    @Override
    public void configureDefaultServletHandling(
            DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

生成JWT秘钥工具类:

/**
 * @author : ljt
 * @version V1.0
 * @Description: 生成JWT秘钥
 * @date Date : 2021年11月25日 15:34
 */
@Slf4j
public class CreateJWTKeyUtil {
	//生成60位秘钥串
    public static String createJwtKey(){
        String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+<>?:,|./;'";
        Random random=new Random();
        StringBuilder sb=new StringBuilder();
        for(int i=0;i<60;i++){
            int number=random.nextInt(84);
            sb.append(str.charAt(number));
        }
        log.info("key====" + sb.toString());
        return sb.toString();
    }
}

定时修改JWT秘钥:

/**
 * @author : ljt
 * @version V1.0
 * @Description: 定时修改jwtkey
 * @date Date : 2021年11月25日 15:43
 */
@Component
public class CreateJWTKey {

    /**
     * jwt秘钥存储,可选择mysql、redis
     */
   @Autowired
   private IJwtKeyService jwtKeyService;

    /**
     * 可自定义更换周期
     * 注意:秘钥更换后,已签发token将无法解密,返回结果为token失效,需要重新签发
     */
    @Scheduled(cron = "0 0 0 1/7 * ?")
    public void createJtw(){
        //获取新秘钥
        String jwtKey = CreateJWTKeyUtil.createJwtKey();
        //秘钥存储修改
        jwtKeyService.updateJwtKey(jwtKey);
        //秘钥静态变量修改 可减少实现层查询次数 直接使用静态数据
        JwtTokenUtil.TOKEN_SECRET = jwtKey;
    }
}

项目启动时获取秘钥:

@Component
public class ApplicationRunnerImplConfig implements ApplicationRunner {

    @Autowired
    private IJwtKeyService jwtKeyService;

    /**
     * 项目启动获取jwt秘钥
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
    	//从存储库中获取秘钥
        JwtTokenUtil.TOKEN_SECRET = jwtKeyService.findJwtKey();
    }
}

使用示例:登录成功后返回token

Map<String, Object> map = new HashMap<>(16);
map.put("id",login.getId());
map.put("userName",login.getUserName());
map.put("nickName",login.getNickName());
map.put("realName",login.getRealName());
String token = JwtTokenUtil.generateToken("user", map,JwtTokenUtil.userTokenExpiration);
login.setAccessToken(token);

return login;

总结

使用流程就是:

前端发起登录请求->后端签发token->前端每次请求都携带此token->后端经过拦截器校验->后端逻辑处理->返回前端

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • SpringMVC框架和SpringBoot项目中控制器的响应结果深入分析

    SpringMVC框架和SpringBoot项目中控制器的响应结果深入分析

    这篇文章主要介绍了SpringMVC框架和SpringBoot项目中控制器的响应结果,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-12-12
  • Java匹配正则表达式汇总

    Java匹配正则表达式汇总

    java匹配字符串表达式在我们数据处理方面是及其重要的,现在就把我这几天数据处理比较常用的向大家介绍一下,常规的一些匹配方式就不介绍了,我们来学习一些特殊的,感兴趣的朋友跟随小编一起看看吧
    2023-03-03
  • MyBatis实现CRUD的示例代码

    MyBatis实现CRUD的示例代码

    本文主要介绍了MyBatis实现CRUD的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-12-12
  • springboot实现在工具类(util)中调用注入service层方法

    springboot实现在工具类(util)中调用注入service层方法

    这篇文章主要介绍了springboot实现在工具类(util)中调用注入service层方法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • SpringBoot之logback-spring.xml不生效的解决方法

    SpringBoot之logback-spring.xml不生效的解决方法

    这篇文章主要介绍了SpringBoot之logback-spring.xml不生效的解决方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • SpringMVC基于注解的Controller详解

    SpringMVC基于注解的Controller详解

    这篇文章主要介绍了SpringMVC基于注解的Controller详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • Springboot如何操作redis数据

    Springboot如何操作redis数据

    这篇文章主要介绍了Springboot如何操作redis数据,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • Spring框架设值注入操作实战案例分析

    Spring框架设值注入操作实战案例分析

    这篇文章主要介绍了Spring框架设值注入操作,结合具体实例形式分析了spring框架设值注入相关实现与使用方法,需要的朋友可以参考下
    2019-11-11
  • Java 如何读取Excel格式xls、xlsx数据工具类

    Java 如何读取Excel格式xls、xlsx数据工具类

    这篇文章主要介绍了Java 如何读取Excel格式xls、xlsx数据工具类的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • 基于Java实现空间滤波完整代码

    基于Java实现空间滤波完整代码

    空间滤波是一种采用滤波处理的影像增强方法。其理论基础是空间卷积和空间相关。这篇文章主要介绍了基于Java的空间滤波代码实现,需要的朋友可以参考下
    2021-08-08

最新评论