1. 什么是JWT
Json web token (JWT) 是為了在網絡應用環境間傳遞聲明而執行的一種基于JSON的開放標準。簡答理解就是一個身份憑證,用于服務識別。
JWT本身是無狀態的,這點有別于傳統的session,不在服務端存儲憑證。這種特性使其在分布式場景,更便于擴展使用。
2. JWT組成部分
JWT有三部分組成,頭部(header),載荷(payload),是簽名(signature)。
- 頭部
頭部主要聲明了類型(jwt),以及使用的加密算法( HMAC SHA256)
- 載荷
載荷就是存放有自定義信息的地方,例如用戶標識,截止日期等
- 簽名
簽名進行對之前的數據添加一層防護,防止被篡改。
簽名生成過程: base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密。
1
2
3
4
|
// base64加密后的header和base64加密后的payload使用.連接組成的字符串 String str=base64(header).base64(payload); // 加鹽secret進行加密 String sign=HMACSHA256(encodedString, 'secret' ); |
3. JWT加密方式
jwt加密分為兩種對稱加密和非對稱加密。
- 對稱加密
對稱加密指使用同一秘鑰進行加密,解密的操作。加密解密的速度比較快,適合數據比較長時的使用。常見的算法為DES、3DES等
- 非對稱加密
非對稱指通過公鑰進行加密,通過私鑰進行解密。加密和解密花費的時間長、速度相對較慢,但安全性更高,只適合對少量數據的使用。常見的算法RSA、ECC等。
兩種加密方法沒有誰更好,只有哪種場景更合適。
4.實戰
本例采用了spring2.x,jwt使用了nimbus-jose-jwt版本,當然其他的jwt版本也都類似,封裝的都是不錯的。
1.maven關鍵配置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version> 9.12 . 1 </version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version> 1.2 . 72 </version> </dependency> |
2.jwt工具類
對于這里的秘鑰:采用了userId+salt+uuid的方式保證,即使是同一個用戶每次生成的serect都是不同的
對于校驗token有效性,包含三個過程:
- 格式是否合法
- token是否在有效期內
- token是否在刷新的有效期內
對于token超過有效期,但在刷新有效期內,返回特定的code,前端進行識別,發起請求刷新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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
public class JwtUtil { private static final Logger log = LoggerFactory.getLogger(JwtUtil. class ); private static final String BEARER_TYPE = "Bearer" ; private static final String PARAM_TOKEN = "token" ; /** * 秘鑰 */ private static final String SECRET = "dfg#fh!Fdh3443" ; /** * 有效期12小時 */ private static final long EXPIRE_TIME = 12 * 3600 * 1000 ; /** * 刷新時間7天 */ private static final long REFRESH_TIME = 7 * 24 * 3600 * 1000 ; public static String generate(PayloadDTO payloadDTO) { //創建JWS頭,設置簽名算法和類型 JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) .type(JOSEObjectType.JWT) .build(); //將負載信息封裝到Payload中 Payload payload = new Payload(JSON.toJSONString(payloadDTO)); //創建JWS對象 JWSObject jwsObject = new JWSObject(jwsHeader, payload); try { //創建HMAC簽名器 JWSSigner jwsSigner = new MACSigner(payloadDTO.getUserId() + SECRET+payloadDTO.getJti()); //簽名 jwsObject.sign(jwsSigner); return jwsObject.serialize(); } catch (JOSEException e) { log.error( "jwt生成器異常" ,e); throw new BizException(TOKEN_SIGNER); } } public static String freshToken(String token) { PayloadDTO payloadDTO; try { //從token中解析JWS對象 JWSObject jwsObject = JWSObject.parse(token); payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO. class ); // 校驗格式是否合適 verifyFormat(payloadDTO, jwsObject); } catch (ParseException e) { log.error( "jwt解析異常" ,e); throw new BizException(TOKEN_PARSE); } catch (JOSEException e) { log.error( "jwt生成器異常" ,e); throw new BizException(TOKEN_SIGNER); } // 校驗是否過期,未過期直接返回原token if (payloadDTO.getExp() >= System.currentTimeMillis()) { return token; } // 校驗是否處于刷新時間內,重新生成token if (payloadDTO.getRef() >= System.currentTimeMillis()) { getRefreshPayload(payloadDTO); return generate(payloadDTO); } throw new BizException(TOKEN_EXP); } private static void verifyFormat(PayloadDTO payloadDTO, JWSObject jwsObject) throws JOSEException { //創建HMAC驗證器 JWSVerifier jwsVerifier = new MACVerifier(payloadDTO.getUserId() + SECRET+payloadDTO.getJti()); if (!jwsObject.verify(jwsVerifier)) { throw new BizException(TOKEN_ERROR); } } public static String getTokenFromHeader(HttpServletRequest request) { // 先從header取值 String value = request.getHeader( "Authorization" ); if (!StringUtils.hasText(value)) { // header不存在從參數中獲取 value = request.getParameter(PARAM_TOKEN); if (!StringUtils.hasText(value)) { throw new BizException(TOKEN_MUST); } } if (value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase())) { return value.substring(BEARER_TYPE.length()).trim(); } return value; } public static PayloadDTO verify(String token) { PayloadDTO payloadDTO; try { //從token中解析JWS對象 JWSObject jwsObject = JWSObject.parse(token); payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO. class ); // 校驗格式是否合適 verifyFormat(payloadDTO, jwsObject); } catch (ParseException e) { log.error( "jwt解析異常" ,e); throw new BizException(TOKEN_PARSE); } catch (JOSEException e) { log.error( "jwt生成器異常" ,e); throw new BizException(TOKEN_SIGNER); } // 校驗是否過期 if (payloadDTO.getExp() < System.currentTimeMillis()) { // 校驗是否處于刷新時間內 if (payloadDTO.getRef() >= System.currentTimeMillis()) { throw new BizException(TOKEN_REFRESH); } throw new BizException(TOKEN_EXP); } return payloadDTO; } public static PayloadDTO getDefaultPayload(Long userId) { long currentTimeMillis = System.currentTimeMillis(); PayloadDTO payloadDTO = new PayloadDTO(); payloadDTO.setJti(UUID.randomUUID().toString()); payloadDTO.setExp(currentTimeMillis + EXPIRE_TIME); payloadDTO.setRef(currentTimeMillis + REFRESH_TIME); payloadDTO.setUserId(userId); return payloadDTO; } public static void getRefreshPayload(PayloadDTO payload) { long currentTimeMillis = System.currentTimeMillis(); payload.setJti(UUID.randomUUID().toString()); payload.setExp(currentTimeMillis + EXPIRE_TIME); payload.setRef(currentTimeMillis + REFRESH_TIME); } } |
3.權限攔截
本例中采用了自定義注解+切面的方式來實現token的校驗過程。
自定義Auth注解提供了是否開啟校驗token,sign的選項,實際操作中可以添加更多的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Target (value = ElementType.METHOD) @Documented @Retention (value = RetentionPolicy.RUNTIME) public @interface Auth { /** * 是否校驗token,默認開啟 */ boolean token() default true ; /** * 是否校驗sign,默認關閉 */ boolean sign() default false ; } |
切面部分指定了對Auth進行切面,這種方法比采用攔截器方式更加靈活些。
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
|
@Component @Aspect public class AuthAspect { @Autowired private HttpServletRequest request; @Pointcut ( "@annotation(com.rain.jwt.config.Auth)" ) private void authPointcut(){} @Around ( "authPointcut()" ) public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable { //獲取目標對象對應的字節碼對象 Class<?> targetCls=joinPoint.getTarget().getClass(); //獲取方法簽名信息從而獲取方法名和參數類型 MethodSignature ms= (MethodSignature) joinPoint.getSignature(); //獲取目標方法對象上注解中的屬性值 Auth auth=ms.getMethod().getAnnotation(Auth. class ); // 校驗簽名 if (auth.token()) { String token = JwtUtil.getTokenFromHeader(request); JwtUtil.verify(token); } // 校驗簽名 if (auth.sign()) { // todo } return joinPoint.proceed(); } } |
4.測試接口
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
|
@RestController @RequestMapping (value= "/user" ) @Api (tags = "用戶" ) public class UserController { @PostMapping (value = "/login" ) @Auth (token = false ) @ApiOperation ( "登錄" ) public Result<String> login(String username,String password) { // 用戶常規校驗 Long userId = 100L; // 用戶信息存入緩存 // 生成token String token = JwtUtil.generate(JwtUtil.getDefaultPayload(userId)); return Result.success(token); } @GetMapping (value = "refreshToken" ) @Auth @ApiOperation ( "刷新token" ) public Result<String> refreshToken(String token) { String freshToken = JwtUtil.freshToken(token); return Result.success(freshToken); } @GetMapping (value = "test" ) @Auth @ApiOperation ( "測試" ) public Result<String> test() { return Result.success( "測試成功" ); } } |
5.總結
許多同學使用jwt經常將獲取到的token放在redis中,在服務器端控制其有效性。這是一種處理token的方式,但這種方式跟jwt的思路是背道而去的,jwt本身就提供了過期的信息,將token的生命周期放入服務器中,又何必采用jwt的方式呢?直接來個uuid不香么。
最后來個項目地址。
到此這篇關于SpringBoot JWT實現登錄刷新token的文章就介紹到這了,更多相關SpringBoot JWT實現token登錄內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.csdn.net/qq_34789577/article/details/120518212