SpringBoot 集成token实践详解

一、需求

  1. SpringBoot 集成 JWT(token),

  2. 拦截器自动验证验证 token 是否过期

  3. token 自动刷新(单个 token 刷新机制,保证活跃用户不会掉线)

  4. 标准统一的 RESTFul 返回体数据格式

  5. 异常统一拦截处理

单个 token 刷新机制(介绍):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ksAAtY2-1613013321715)(13.SpringBoot%20%E9%9B%86%E6%88%90token%E5%AE%9E%E8%B7%B5%E8%AF%A6%E8%A7%A3.assets/1611831269211-d13250d1-227d-48f3-bdb4-40d8bd643652.png)]

token 距离发布token 2 个小时内的token为新生token,2-3 个小时的token为老年token

每次请求,前端带上 token,

(1)如果 token 为新 token ,服务器返回原来的 token

(2)如果 token 为老年 token,服务器返回 刷新后的新生token ,

(3)如果 token 为过期 token,服务器返回token过期 状态码 401,,请求失败, 前端重新登录

二、代码

1. 导入依赖

jwt 依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

整个 SpringBoot 依赖

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
<!--        jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

2. 配置文件

server:
    port: 8081
spring:
    application:
        name: tokendemo
# token
token:
    privateKey: 'fdasfgdsagaxgsregdfdjyghjfhebfdgwe45ygrfbsdfshfdsag'
    yangToken: 1000000
    oldToken: 3000000000

3. 代码

代码结构如下

image-20210211101506755

  1. AuthWebMvcConfigurer
@Configuration
public class AuthWebMvcConfigurer implements WebMvcConfigurer {
    
    
    @Autowired
    AuthHandlerInterceptor authHandlerInterceptor;

    /**
     * 给除了 /login 的接口都配置拦截器,拦截转向到 authHandlerInterceptor
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(authHandlerInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}
  1. TokenTestController
@RestController
public class TokenTestController {
    
    
    @Autowired
    TokenUtil tokenUtil;
    /**
     * 使用 /login 请求获得 token, /login 不经过拦截器
     */
    @RequestMapping("/login")
    public String login(){
    
    
        return tokenUtil.getToken("靓仔","admin");
    }
    /**
     * 使用 /test-token 测试 token,进过拦截器
     */
    @RequestMapping("/test-token")
    public Map testToken(HttpServletRequest request){
    
    
        String token = request.getHeader("token");
        return tokenUtil.parseToken(token);
    }

}
  1. TokenAuthExpiredException
public class TokenAuthExpiredException extends RuntimeException{
    
    
}
  1. AuthHandlerInterceptor
@Slf4j
@Component
public class AuthHandlerInterceptor implements HandlerInterceptor {
    
    
    @Autowired
    TokenUtil tokenUtil;
    @Value("${token.privateKey}")
    private String privateKey;
    @Value("${token.yangToken}")
    private Long yangToken;
    @Value("${token.oldToken}")
    private Long oldToken;
    /**
     * 权限认证的拦截操作.
     */
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
    
    
        log.info("=======进入拦截器========");
        // 如果不是映射到方法直接通过,可以访问资源.
        if (!(object instanceof HandlerMethod)) {
    
    
            return true;
        }
        //为空就返回错误
        String token = httpServletRequest.getHeader("token");
        if (null == token || "".equals(token.trim())) {
    
    
            return false;
        }
        log.info("==============token:" + token);
        Map<String, String> map = tokenUtil.parseToken(token);
        String userId = map.get("userId");
        String userRole = map.get("userRole");
        long timeOfUse = System.currentTimeMillis() - Long.parseLong(map.get("timeStamp"));
        //1.判断 token 是否过期
        //年轻 token
        if (timeOfUse < yangToken) {
    
    
            log.info("年轻 token");
        }
        //老年 token 就刷新 token
        else if (timeOfUse >= yangToken && timeOfUse < oldToken) {
    
    
            httpServletResponse.setHeader("token",tokenUtil.getToken(userId,userRole));
        }
        //过期 token 就返回 token 无效.
        else {
    
    
            throw new TokenAuthExpiredException();
        }
        //2.角色匹配.
        if ("user".equals(userRole)) {
    
    
            log.info("========user账户============");
            return true;
        }
        if ("admin".equals(userRole)) {
    
    
            log.info("========admin账户============");
            return true;
        }
        return false;
    }

}
  1. GlobalExceptionHandler
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 用户 token 过期
     * @return
     */
    @ExceptionHandler(value = TokenAuthExpiredException.class)
    @ResponseBody
    public String tokenExpiredExceptionHandler(){
        log.warn("用户 token 过期");
        return "用户 token 过期";
    }
}
  1. TokenUtil
@Component
public class TokenUtil {
    
    
    @Value("${token.privateKey}")
    private String privateKey;

    /**
     * 加密token.
     */
    public String getToken(String userId, String userRole) {
    
    
        //这个是放到负载payLoad 里面,魔法值可以使用常量类进行封装.
        String token = JWT
                .create()
                .withClaim("userId" ,userId)
                .withClaim("userRole", userRole)
                .withClaim("timeStamp", System.currentTimeMillis())
                .sign(Algorithm.HMAC256(privateKey));
        return token;
    }

    /**
     * 解析token.
     * (优化可以用常量固定魔法值+使用DTO在 mvc 之前传输数据,而不是 map,这里因为篇幅原因就不做了)
     * {
     * "userId": "3412435312",
     * "userRole": "ROLE_USER",
     * "timeStamp": "134143214"
     * }
     */
    public Map<String, String> parseToken(String token) {
    
    
        HashMap<String, String> map = new HashMap<>();
        DecodedJWT decodedjwt = JWT.require(Algorithm.HMAC256(privateKey))
                .build().verify(token);
        Claim userId = decodedjwt.getClaim("userId");
        Claim userRole = decodedjwt.getClaim("userRole");
        Claim timeStamp = decodedjwt.getClaim("timeStamp");
        map.put("userId", userId.asString());
        map.put("userRole", userRole.asString());
        map.put("timeStamp", timeStamp.asLong().toString());
        return map;
    }
}

完整项目代码的地址

三、测试

1. 获得 token

访问

localhost:8081/login

效果:

image-20210211102129000

2. 测试 token 是否可用

将 1 测试得到的 token 放到 header 里面测试 token是否可用

访问

localhost:8081/test-token

image-20210211102448340

3. 测试 token 过期

测试全局异常拦截类拦截到 TokenAuthExpiredException 异常,然后返回提示。

将过期时间调小,修改 application.yaml 文件,3 秒钟就过期

server:
    port: 8081
spring:
    application:
        name: tokendemo
# token
token:
    privateKey: 'fdasfgdsagaxgsregdfdjyghjfhebfdgwe45ygrfbsdfshfdsag'
    yangToken: 1000
    oldToken: 3000

重启应用测试:

image-20210211102718111

完整项目代码的地址

猜你喜欢

转载自blog.csdn.net/jarvan5/article/details/113789133