「SpringBoot-View」 JWT

Posted by Dawn-K's Blog on July 8, 2020

JWT令牌验证

[toc] 老师博客

验证流程

  1. 用户使用账号和面发出post请求;
  2. 服务器使用私钥创建一个jwt;
  3. 服务器返回这个jwt给浏览器;
  4. 浏览器将该jwt串在请求头中像服务器发送请求;
  5. 服务器验证该jwt;
  6. 返回响应的资源给浏览器.

结构

JWT包含了三部分:

  • Header 头部(标题包含了令牌的元数据, 并且包含签名和/或加密算法的类型)
  • Payload 负载 (类似于飞机上承载的物品)
  • Signature 签名/签证

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.1</version>
</dependency>

<!-- 这个不是必需,但是可以简化操作,在下文会用到 -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.10</version>
</dependency>

工具类

用于生成和验证的工具类, 这个类复制好以后就直接调用就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// src\main\java\com\neuedu\utils\JwtUtils.java
package com.neuedu.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

@Slf4j
public class JwtUtils {

    // 过期时间5分钟
    private static final long EXPIRE_TIME = 5 * 60 * 1000;

    /**
     * 1. 校验token是否正确
     *
     * @param token  密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
//            加密算法(在此规定用户名是唯一的)
//            secret 加盐
            Algorithm algorithm = Algorithm.HMAC256(secret);
//            通过定义的算法产生 jwt校验对象
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            // 解析jwt
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 2. 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 3. 生成签名,5min后过期
     *
     * @param username 用户名
     * @param secret   用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);
    }
}

登录校验过程

主要是为围绕对登录的Controller的修改, 修改的时候会用到拦截器和令牌生成的算法.

拦截器

拦截器都放在 neuedu\interceptor 下.

主要是对登录的前置拦截器进行操作. 主要操作如下

  1. 如果当前是登录的链接,那么就不做检查,直接通行
  2. 从header中取出token,如果没有,就返回false
  3. 如果token不合法(利用JwtUtils.verify进行校验),返回结果

注意正确的时候直接返回true即可, 在出错的时候还是要对response进行输入, 然后返回报错信息.

令牌一般放在请求头中, 因为不是所有请求都有请求体. 这也意味着在Postman中进行测试时, token要加到Header里面.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// src\main\java\com\neuedu\interceptor\LoginInterceptor.java
package com.neuedu.interceptor;

import com.alibaba.fastjson.JSON;
import com.neuedu.constrant.GlobalConstrant;
import com.neuedu.constrant.ResultConstrant;
import com.neuedu.core.Result;
import com.neuedu.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * PROJECT_NAME: neuedu-his
 * Created by DawnK on 2020/7/8
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    // 前置拦截器,在访问控制器之前,进行令牌的校验
//    参数是servlet对象
    public boolean preHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, java.lang.Object handler) throws java.lang.Exception {
//        获取路径(getServletPath 或者 getRequestURI),因为有的需要用token,有的不需要
        String path = request.getServletPath();
        log.info("getpath" + path);
        if (StringUtils.isNotEmpty(path) && path.contains("/user/login")) {
            return true;
        }
        // 1. 是否有令牌
        // 
        String token = request.getHeader("token");
        log.info("findtoken ", token);
        // 看是否为空
        if (StringUtils.isEmpty(token)) {
            Result result = Result.fail(ResultConstrant.TOKEN_ERROR, "token error");
            response.getWriter().write(JSON.toJSONString(result));
            return false;
        } else {
//            解析令牌,查看其是否合法
            String username = JwtUtils.getUsername(token);
            if (JwtUtils.verify(token, username, GlobalConstrant.TOKEN_SECRET)) {
                return true;
            } else {
                Result result = Result.fail(ResultConstrant.TOKEN_ERROR, "token invalid");
                response.getWriter().write(JSON.toJSONString(result));
                return false;
            }
        }
    }

    public void postHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, java.lang.Object handler, @org.springframework.lang.Nullable org.springframework.web.servlet.ModelAndView modelAndView) throws java.lang.Exception {
        log.info("响应拦截器");

    }

    public void afterCompletion(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, java.lang.Object handler, @org.springframework.lang.Nullable java.lang.Exception ex) throws java.lang.Exception {
        log.info("后置拦截器");

    }
}

配置类

配置所有的请求, 都要经过拦截器

1
2
3
4
5
6
7
@Configuration
public class WebConfig implements WebMvcConfigurer {
    public void addInterceptors(InterceptorRegistry registry) {
        // addPathPatterns() 就是配置哪些请求路过拦截器
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
    }
}

TokenVO封装

这一层封装没有太看出作用. 还要深入研究, 可能是让token的类型不要如此单薄.

1
2
3
4
5
// src\main\java\com\neuedu\entity\vo\TokenVO.java
@Data
public class TokenVO {
    private String token;
}

控制类

在之前的设计中, 登录成功之后, 就返回Result.success()就结束了, 但是在加入令牌之后, 必须在登录成功的时候生成token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 截取了一部分,主要是登录
// src\main\java\com\neuedu\controller\UserController.java
   @PostMapping("/login")
    public Result login(@RequestBody LoginParams loginParams) {
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("username", loginParams.getUsername());
        queryWrapper.eq("password", loginParams.getPassword());
        List<User> list = userMapper.selectList(queryWrapper);

        if (list.size() > 0) {
//            登录成功返回token
            String token = JwtUtils.sign(loginParams.getUsername(), GlobalConstrant.TOKEN_SECRET);
            TokenVO tokenVO = new TokenVO();
            tokenVO.setToken(token);
            return Result.success(tokenVO);
        } else {
            return Result.fail("登录失败");
        }
    }

测试方法

这个需要使用postman进行测试, 在没有令牌的情况下尝试访问非登录界面(理论上应该会拒绝访问). 然后登录, 登录成功后获得token, 然后使用这个token(切记要在postman上加到Header上)尝试访问其他的界面(此时应该成功).

至此后端的令牌验证已经结束了, 下面还需要前端的验证.