📖 官方文档

🌐官方文档

JWT.IO

💰JWT概述

JSON Web Token是一个开放标准(RFC 7519),它定义了一种紧凑且包含的方式,用于在各方之间安全地传输信息作为JSON对象。由于此信息是经过数字签名的,用于在各方面之间安全地传输信息作为JSON对象。由于此信息是经过数字签名地,因此可以被验证和信任。可以使用秘密(或者HMAC算法)或使用RSAECDSA的公钥/私钥对对JWT进行签名。

尽管可以对JWT进行加密以提供双方之间的保密性,但我们将重点关注已签名的令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌则将这些声明隐藏在其他方的面前。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。


🔱应用场景

  • 授权认证:使用JWT的最常见方案。一旦用户登录,每个后续请求将包括令牌,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
  • 信息交换:JWT是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名,例如使用公钥/私钥对。此外。由于签名是使用标头和有效负载计算的,因此还可以验证内容是否遭到篡改。

🚀 认证流程

⛔️session认证

传统方式:用户第一次请求登录时候设置session,此后每次访问携带cookie。

问题:

  • 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着用户的增多,服务端的开销会明显增大。
  • 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力,这也意味着限制了应用的扩展能力。
  • 因为是基于cookie来进行用户识别的,cookie如何被截获,用户很容易受到跨站请求伪造的攻击。

🔰JWT认证


认证流程:

首先,前端通过web表单将自己的用户名和密码发送到后端的接口,这一过程一般是一个Http POST请求。

后端核对用户名和密码成功后,将用户的id等其他信息作为JWT payload(负载),将其与头部分别进行Based64编码拼接后签名,形成一个JWT。形成的JWT就形同xxx.yyy.zzz的字符串。

后端将JWT字符串作为登陆成功的返回结果返回给前端,前端可以将返回的结果保存在localStorage上,退出登录时前端删除保存的JWT即可。

前端在每次请求时将JWT放入HTTP Header的Authorization位。(解决XSS和XSRF问题)

后端检查是否存在,如存在验证JWT的有效性。(签名是否正确,Token是否过期等)

验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,并返回相应结果。

优势:

  • 简洁,可以通过URL,POST参数或者在HTTP Header发送,因为数据量小,传输速度也很快
  • 自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库
  • 因为Token是JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
  • 不需要再服务端保存会话信息,特别适合用于分布式微服务

⛓令牌结构

JSON Web Token以紧凑的形式由三部分组成,三部分由.分隔,即xxxxx.yyyyy.zzzzz

  • 标头(Header) : Base64编码,由令牌类型和签名算法组成

    1
    2
    3
    4
    {
    "alg": "HS256",
    "typ": "JWT"
    }
  • 负载(Payload) : Base64编码,用于放置携带信息,不能放敏感信息

    1
    2
    3
    4
    5
    {
    "name":"Khighness"
    "admin":true
    "gender":"male"
    }
  • 签名(Signature) : 对标头和负载进行签名,防止内容被篡改

    1
    HMACSHA256(baseUrlEncode(header)) + "." + base64UrlEncode(payload).secret)

💻 JWT-Demo

➕添加依赖

1
2
3
4
5
6
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>

📑Java代码

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
@Test
void generateToken() {
HashMap<String, Object> map = new HashMap<>();

Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 60);

String token = JWT.create()
.withHeader(map) // Header
.withClaim("userID", 1011) // Payload
.withClaim("username", "KHighness")
.withExpiresAt(calendar.getTime()) // 指定令牌过期时间: 60S
.sign(Algorithm.HMAC256("PARAK")); // Signature,设置密钥PARAK

System.out.println(token);
}

/**
* <p>验证令牌</p>
*/
@Test
void verifyToken() {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512("PARAK")).build();
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDU5NDU0ODQsInVzZXJJRCI6MTAxMSwidXNlcm5hbWUiOiJLSGlnaG5lc3MifQ.Gvwa3vu_LYogcEPFxKOgFgaH6WnTKoo-UDW977W1GAw";
DecodedJWT verify = jwtVerifier.verify(token);
System.out.println(verify.getClaim("userID").asInt());
System.out.println(verify.getClaim("username").asString());
}

❗️ 常见异常

异常 描述
TokenExpiredException 令牌过期异常
SignatureVerificationException 签名不一致异常
AlgorithmMismatchException 加密算法不匹配异常
InvalidClaimException 失效的负载异常

🍃 整合Springboot

💬说明

用户在第一次登陆的时候,后台生成token返回给前端存储在sessionStorage中,此后每次前端需要调用后端需要认证的接口时都把token取出来携带在http header中。后台设置拦截器,设置需要认证才能访问的接口,每次处理请求时,先从request的http header中取出token进行认证,通过后才进行相关接口处理。


🔧封装工具类

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
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Map;

/**
* @author KHighness
* @since 2020-11-22
*/
public class KKJWTUtil {

/**
* <p>签名的密钥[@NAME]</p>
*/
private static final String secret = "@KHIGHNESS";

/**
* <p>签名的过期时间[A Week]</p>
*/
private static final int accessTokenExpireTime = 604800;

/**
* <p>令牌颁布者身份标识[DOMAIN]</p>
*/
private static final String issuer = "parak.top";

/**
* <p>生成Token</p>
* @param chaims
* @return
*/
public static String generateToken(Map<String, String> chaims) {
JWTCreator.Builder builder = JWT.create();

/* 添加负载 */
chaims.forEach((k, v) -> {
builder.withClaim(k, v);
});

/* 设置过期时间 */
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, accessTokenExpireTime);
builder.withExpiresAt(calendar.getTime());

/* 设置颁发者身份 */
builder.withIssuer(issuer);

/* 设置加密算法以及密钥 */
String token = builder.sign(Algorithm.HMAC256(secret));

return token;
}


/**
* <p>解析token</p>
* @param token
* @return
*/
public static DecodedJWT verifyToken(String token) {
return JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
}

}

用户控制器

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
75
76
77
78
79
80
81
82
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.*;
import top.parak.common.KKCondition;
import top.parak.common.KKDataResponse;
import top.parak.common.KKJWTUtil;
import top.parak.entity.User;
import top.parak.service.UserService;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;

/**
* @author KHighness
* @since 2020-11-19
*/
@Slf4j
@CrossOrigin
@RestController
@RequestMapping("/api/user")
public class UserController {

@Autowired
private UserService userService;

/**
* 用户注册
*/
@PostMapping("/register")
public KKDataResponse register(@Valid @RequestBody User user) {
String username = user.getUsername();
String password = user.getPassword();
log.info("用户注册 => [{}]", username);
KKCondition condition = new KKCondition();
condition.setName(username);
if (!ObjectUtils.isEmpty(userService.queryUserInCondition(condition))) {
log.warn("用户名{}已被注册,不可重复注册", username);
return KKDataResponse.errorResponse("该用户名已被注册,不可重复注册");
}
return userService.saveUser(username, password) == 1 ? KKDataResponse.successResponse(true) : KKDataResponse.errorResponse(false);
}

/**
* 用户登录
*/
@PostMapping("/login")
public KKDataResponse login(@RequestBody String loginDataJson) {
JSONObject loginData = JSON.parseObject(loginDataJson);
String username = loginData.get("username").toString();
String password = loginData.get("password").toString();
log.info("用户登录 => [{}]", username);
Map<String, Object> response = new HashMap<>();
if (userService.authenticate(username, password)) {
response.put("loginState", true);
/* 生成令牌 */
Map<String, String> payload = new HashMap<>();
payload.put("username", username);
String token = KKJWTUtil.generateToken(payload);
response.put("kktoken", token);
return KKDataResponse.successResponse(response);
} else {
response.put("loginState", false);
response.put("kktoken", null);
return KKDataResponse.errorResponse(response);
}
}

@GetMapping("/test")
public KKDataResponse test(HttpServletRequest request) {
String kktoken = (String) request.getHeader("Authorization");
DecodedJWT verify = KKJWTUtil.verifyToken(kktoken);
String username = verify.getClaim("username").asString();
log.info("请求用户 => [{}]", username);
return KKDataResponse.successResponse("请求成功");
}

}

设置拦截器

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

import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import top.parak.common.JwtTokenUtil;

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

/**
* <p> Project: Springboot-JWT </P>
* <p> Package: top.parak.interceptor </p>
* <p> FileName: JWTInterceptor <p>
* <p> Description: 令牌拦截器 <p>
* <p> Created By IntelliJ IDEA </p>
*
* @author KHighness
* @since 2020/11/22
*/

@Slf4j
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String kktoken = request.getHeader("Authorization");
Map<String, Object> map = new HashMap<>();
try {
log.info("请求令牌 => [{}]", kktoken);
DecodedJWT decodedJWT = KKJWTUtil.verifyToken(kktoken);
log.info("验证结果 => [{}]", true);
return true;
} catch (TokenExpiredException e) {
log.error("发成异常 => [{}]", e.getMessage());
map.put("msg", "令牌过期");
} catch (SignatureVerificationException e) {
log.error("发成异常 => [{}]", e.getMessage());
map.put("msg", "签名错误");
} catch (AlgorithmMismatchException e) {
log.error("发成异常 => [{}]", e.getMessage());
map.put("msg", "加密算法不匹配");
} catch (InvalidClaimException e) {
log.error("发成异常 => [{}]", e.getMessage());
map.put("msg", "失效负载");
} catch (NullPointerException e) {
log.error("发成异常 => [{}]", e.getMessage());
map.put("msg", "令牌为空");
} catch (Exception e) {
log.error("发成异常 => [{}]", e.getMessage());
map.put("msg", e.getMessage());
}
log.error("验证结果 => [{}]", false);
map.put("state", false);
/* map转json */
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.println(json);
writer.close();
return false;
}
}

拦截器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.parak.intercepter.JWTIntercepter;

/**
* @author KHighness
* @since 2020-11-22
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/api/**/**")
.excludePathPatterns("/api/user/login");
}
}