JWT(json web tokens)是目前比較流行的跨域認證解決方案;說通俗點就是比較流行的token生成和校驗的方案。碰巧公司有個app的項目的token采用了jwt方案,因此記錄下后端項目集成jwt的過程,方便后續查閱。
一、jwt的簡單介紹
jwt生成的token是一種無狀態的token,服務端不需要對該token進行保存;它一般由客戶端保存。客戶端訪問請求服務時,服務端對token進行校驗,然后進行各種控制。下面直接拿一個生成好的token來講解

通過上圖我們可以發現jwt生成的token是非常長的字符串,並且字符串中有2個小點("."),通過這2個小點我們可以把這token分成3部分。
- header:頭部,是用來描述這個token是什么類型,采用了何種加密算法;token中header是經過base64編碼的
- payload:荷載,用來存放需要傳遞的數據。官方提供的幾個標准字段,同時也可以自己往里面加自定義的字段和內容,用來存放一些不敏感的用戶信息。可以簡單的把它想像成一個Map集合;token中payload也是經過base64編碼的
- signature:簽名,主要是將header和payload的base64編碼后內容用點拼接在一起然后進行加密生成簽名。服務端需要利用這簽名來校驗token是否被篡改(驗簽)
所以通俗的來講,token = base64(header) + "." + base64(payload) + "." + 簽名
網上很多博文對jwt的介紹都比較詳細,因此本文就不再詳細的介紹jwt相關細節,重點放在java代碼該怎么寫。jwt相關詳細介紹可以參考如下鏈接:
下面直接上代碼
二、Maven依賴版本說明
pom中部分重要jar包依賴版本如下:
<!-- SpringBoot 版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--jwt 依賴-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
14
1
<!-- SpringBoot 版本 -->
2
<parent>
3
<groupId>org.springframework.boot</groupId>
4
<artifactId>spring-boot-starter-parent</artifactId>
5
<version>2.1.4.RELEASE</version>
6
<relativePath/> <!-- lookup parent from repository -->
7
</parent>
8
9
<!--jwt 依賴-->
10
<dependency>
11
<groupId>io.jsonwebtoken</groupId>
12
<artifactId>jjwt</artifactId>
13
<version>0.9.1</version>
14
</dependency>
三、token生成和解析工具類及token認證攔截器的編寫
(1)token生成和解析工具類編寫
該工具類需要具有如下功能
- 生成jwt標准的token;生成token時支持把不敏感的用戶信息放在token里面,后續解析token后可以直接使用這些用戶信息
- 解析token,校驗token是否過期和篡改
直接看下面代碼
package com.psx.gqxy.web.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* JwtUtils
* @author ZENG.XIAO.YAN
* @blog https://www.cnblogs.com/zeng1994/
* @version 1.0
*/
@Slf4j
public final class JwtUtils {
/** 存放token的請求頭對應的key的名字 */
private static String headerKey = "token";
/** 加密的secret */
private static String secret = "zxyTestSecret";
/** 過期時間,單位為秒 */
private static long expire = 1800L;
static {
// TODO 上面變量的值應該從配置文件中讀取,方便測試這里就不從配置文件中讀取
// 利用配置文件中的值覆蓋靜態變量初始化的值
}
/**
* 生成jwt token
*/
public static String generateToken(Map<String, Object> userInfoMap) {
if (Objects.isNull(userInfoMap)) {
userInfoMap = new HashMap<>();
}
// 過期時間
Date expireDate = new Date(System.currentTimeMillis() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT") // 設置頭部信息
.setClaims(userInfoMap) // 裝入自定義的用戶信息
.setExpiration(expireDate) // token過期時間
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 校驗token並解析token
* @param token
* @return Claims:它繼承了Map,而且里面存放了生成token時放入的用戶信息
*/
public static Claims verifyAndGetClaimsByToken(String token) {
try {
/* 如果過期或者是被篡改,則會拋異常.
注意點:只有在生成token設置了過期時間(setExpiration(expireDate))才會校驗是否過期,
可以參考源碼io.jsonwebtoken.impl.DefaultJwtParser的299行。
拓展:利用不設置過期時間就不校驗token是否過期的這一特性,我們不設置Expiration;
而采用自定義的字段來存放過期時間放在Claims(可以簡單的理解為map)中;
通過token獲取到Claims后自己寫代碼校驗是否過期。
通過這思路,可以去實現對過期token的手動刷新
*/
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
log.debug("verify token error:[{}] ", ExceptionUtils.getStackTrace(e));
return null;
}
}
public static String getHeaderKey() {
return headerKey;
}
}
83
1
package com.psx.gqxy.web.jwt;
2
import io.jsonwebtoken.Claims;
3
import io.jsonwebtoken.Jwts;
4
import io.jsonwebtoken.SignatureAlgorithm;
5
import lombok.extern.slf4j.Slf4j;
6
import org.apache.commons.lang3.exception.ExceptionUtils;
7
import java.util.Date;
8
import java.util.HashMap;
9
import java.util.Map;
10
import java.util.Objects;
11
12
/**
13
* JwtUtils
14
* @author ZENG.XIAO.YAN
15
* @blog https://www.cnblogs.com/zeng1994/
16
* @version 1.0
17
*/
18
19
public final class JwtUtils {
20
21
/** 存放token的請求頭對應的key的名字 */
22
private static String headerKey = "token";
23
/** 加密的secret */
24
private static String secret = "zxyTestSecret";
25
/** 過期時間,單位為秒 */
26
private static long expire = 1800L;
27
28
static {
29
// TODO 上面變量的值應該從配置文件中讀取,方便測試這里就不從配置文件中讀取
30
// 利用配置文件中的值覆蓋靜態變量初始化的值
31
}
32
33
34
/**
35
* 生成jwt token
36
*/
37
public static String generateToken(Map<String, Object> userInfoMap) {
38
if (Objects.isNull(userInfoMap)) {
39
userInfoMap = new HashMap<>();
40
}
41
// 過期時間
42
Date expireDate = new Date(System.currentTimeMillis() + expire * 1000);
43
return Jwts.builder()
44
.setHeaderParam("typ", "JWT") // 設置頭部信息
45
.setClaims(userInfoMap) // 裝入自定義的用戶信息
46
.setExpiration(expireDate) // token過期時間
47
.signWith(SignatureAlgorithm.HS512, secret)
48
.compact();
49
}
50
51
/**
52
* 校驗token並解析token
53
* @param token
54
* @return Claims:它繼承了Map,而且里面存放了生成token時放入的用戶信息
55
*/
56
public static Claims verifyAndGetClaimsByToken(String token) {
57
try {
58
/* 如果過期或者是被篡改,則會拋異常.
59
注意點:只有在生成token設置了過期時間(setExpiration(expireDate))才會校驗是否過期,
60
可以參考源碼io.jsonwebtoken.impl.DefaultJwtParser的299行。
61
拓展:利用不設置過期時間就不校驗token是否過期的這一特性,我們不設置Expiration;
62
而采用自定義的字段來存放過期時間放在Claims(可以簡單的理解為map)中;
63
通過token獲取到Claims后自己寫代碼校驗是否過期。
64
通過這思路,可以去實現對過期token的手動刷新
65
*/
66
return Jwts.parser()
67
.setSigningKey(secret)
68
.parseClaimsJws(token)
69
.getBody();
70
}catch (Exception e){
71
log.debug("verify token error:[{}] ", ExceptionUtils.getStackTrace(e));
72
return null;
73
}
74
}
75
76
public static String getHeaderKey() {
77
return headerKey;
78
}
79
80
81
}
82
83
(2)token身份認證攔截器的編寫
攔截器主要作用如下:
- 1)攔截器攔截到請求后,拿請求頭中的token,如果不存在只直接response輸出token不能為空
- 2)拿到token后,進行token的解析,校驗是否篡改或者過期。如果被篡改或者過期只直接response輸出token已失效
- 3)如果校驗都通過了,則把token中解析出的用戶信息放在request請求域中,方便后續Controller方法取用戶信息
直接看參考下面代碼
package com.psx.gqxy.web.jwt;
import com.alibaba.fastjson.JSON;
import com.psx.gqxy.common.base.CommonConstant;
import com.psx.gqxy.common.base.ModelResult;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* jwtToken校驗攔截器
* @author ZENG.XIAO.YAN
* @blog https://www.cnblogs.com/zeng1994/
* @version 1.0
*/
public class JwtInterceptor extends HandlerInterceptorAdapter {
public static final String USER_INFO_KEY = "user_info_key";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 獲取用戶 token
String token = request.getHeader(JwtUtils.getHeaderKey());
if (StringUtils.isBlank(token)) {
token = request.getParameter(JwtUtils.getHeaderKey());
}
// token為空
if(StringUtils.isBlank(token)) {
this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
JwtUtils.getHeaderKey() + " can not be blank",
response);
return false;
}
// 校驗並解析token,如果token過期或者篡改,則會返回null
Claims claims = JwtUtils.verifyAndGetClaimsByToken(token);
if(null == claims){
this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
JwtUtils.getHeaderKey() + "失效,請重新登錄",
response);
return false;
}
// 校驗通過后,設置用戶信息到request里,在Controller中從Request域中獲取用戶信息
request.setAttribute(USER_INFO_KEY, claims);
return true;
}
/**
* 利用response直接輸出錯誤信息
* @param code
* @param msg
* @param response
* @throws IOException
*/
private void writerErrorMsg (String code, String msg, HttpServletResponse response) throws IOException {
ModelResult<Void> result = new ModelResult<>();
result.setCode(code);
result.setMsg(msg);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
65
1
package com.psx.gqxy.web.jwt;
2
import com.alibaba.fastjson.JSON;
3
import com.psx.gqxy.common.base.CommonConstant;
4
import com.psx.gqxy.common.base.ModelResult;
5
import io.jsonwebtoken.Claims;
6
import org.apache.commons.lang3.StringUtils;
7
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
8
import javax.servlet.http.HttpServletRequest;
9
import javax.servlet.http.HttpServletResponse;
10
import java.io.IOException;
11
12
/**
13
* jwtToken校驗攔截器
14
* @author ZENG.XIAO.YAN
15
* @blog https://www.cnblogs.com/zeng1994/
16
* @version 1.0
17
*/
18
public class JwtInterceptor extends HandlerInterceptorAdapter {
19
20
public static final String USER_INFO_KEY = "user_info_key";
21
22
23
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
24
// 獲取用戶 token
25
String token = request.getHeader(JwtUtils.getHeaderKey());
26
if (StringUtils.isBlank(token)) {
27
token = request.getParameter(JwtUtils.getHeaderKey());
28
}
29
// token為空
30
if(StringUtils.isBlank(token)) {
31
this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
32
JwtUtils.getHeaderKey() + " can not be blank",
33
response);
34
return false;
35
}
36
// 校驗並解析token,如果token過期或者篡改,則會返回null
37
Claims claims = JwtUtils.verifyAndGetClaimsByToken(token);
38
if(null == claims){
39
this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
40
JwtUtils.getHeaderKey() + "失效,請重新登錄",
41
response);
42
return false;
43
}
44
// 校驗通過后,設置用戶信息到request里,在Controller中從Request域中獲取用戶信息
45
request.setAttribute(USER_INFO_KEY, claims);
46
return true;
47
}
48
49
/**
50
* 利用response直接輸出錯誤信息
51
* @param code
52
* @param msg
53
* @param response
54
* @throws IOException
55
*/
56
private void writerErrorMsg (String code, String msg, HttpServletResponse response) throws IOException {
57
ModelResult<Void> result = new ModelResult<>();
58
result.setCode(code);
59
result.setMsg(msg);
60
response.setContentType("application/json;charset=UTF-8");
61
response.getWriter().write(JSON.toJSONString(result));
62
}
63
64
}
65
四、攔截器的配置和功能測試
(1)編寫一個Controller
寫一個Controller,里面包含一登錄方法和一個test方法
- 登錄方法用來實現登錄,登錄成功后返回token
- test方法,主要通過攔截器攔截該方法的請求,當用戶帶有效的token訪問時才允許訪問該方法
代碼如下:
package com.psx.gqxy.web.controller;
import com.psx.gqxy.common.base.CommonConstant;
import com.psx.gqxy.common.base.ModelResult;
import com.psx.gqxy.domain.dto.UserLoginDTO;
import com.psx.gqxy.web.jwt.JwtInterceptor;
import com.psx.gqxy.web.jwt.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* TestJwtController
* @author ZENG.XIAO.YAN
* @blog https://www.cnblogs.com/zeng1994/
* @Date 2019-07-14
* @version 1.0
*/
@RestController
@RequestMapping("jwt")
public class TestJwtController {
@PostMapping("login")
public ModelResult<String> login(@RequestBody UserLoginDTO dto) {
ModelResult<String> result = new ModelResult<>();
// 這里登錄就簡單的模擬下
if ("root".equals(dto.getUserName()) && "123456".equals(dto.getPassword())) {
Map<String, Object> userInfoMap = new HashMap<>();
userInfoMap.put("userName", "隔壁老王");
String token = JwtUtils.generateToken(userInfoMap);
result.setData(token);
} else {
result.setCode(CommonConstant.FAIL);
result.setMsg("用戶名或密碼錯誤");
}
return result;
}
@GetMapping("test")
public String test(HttpServletRequest request) {
// 登錄成功后,從request中獲取用戶信息
Claims claims = (Claims) request.getAttribute(JwtInterceptor.USER_INFO_KEY);
if (null != claims) {
return (String) claims.get("userName");
} else {
return "fail";
}
}
}
53
1
package com.psx.gqxy.web.controller;
2
import com.psx.gqxy.common.base.CommonConstant;
3
import com.psx.gqxy.common.base.ModelResult;
4
import com.psx.gqxy.domain.dto.UserLoginDTO;
5
import com.psx.gqxy.web.jwt.JwtInterceptor;
6
import com.psx.gqxy.web.jwt.JwtUtils;
7
import io.jsonwebtoken.Claims;
8
import org.springframework.web.bind.annotation.*;
9
import javax.servlet.http.HttpServletRequest;
10
import java.util.HashMap;
11
import java.util.Map;
12
13
/**
14
* TestJwtController
15
* @author ZENG.XIAO.YAN
16
* @blog https://www.cnblogs.com/zeng1994/
17
* @Date 2019-07-14
18
* @version 1.0
19
*/
20
21
22
"jwt") (
23
public class TestJwtController {
24
25
"login") (
26
public ModelResult<String> login( UserLoginDTO dto) {
27
ModelResult<String> result = new ModelResult<>();
28
// 這里登錄就簡單的模擬下
29
if ("root".equals(dto.getUserName()) && "123456".equals(dto.getPassword())) {
30
Map<String, Object> userInfoMap = new HashMap<>();
31
userInfoMap.put("userName", "隔壁老王");
32
String token = JwtUtils.generateToken(userInfoMap);
33
result.setData(token);
34
} else {
35
result.setCode(CommonConstant.FAIL);
36
result.setMsg("用戶名或密碼錯誤");
37
}
38
return result;
39
}
40
41
"test") (
42
public String test(HttpServletRequest request) {
43
// 登錄成功后,從request中獲取用戶信息
44
Claims claims = (Claims) request.getAttribute(JwtInterceptor.USER_INFO_KEY);
45
if (null != claims) {
46
return (String) claims.get("userName");
47
} else {
48
return "fail";
49
}
50
}
51
52
}
53
(2)攔截器的配置
攔截器攔截需要身份認證的請求,同時放行登錄接口
代碼如下:
/**
* web相關的定制化配置
* @author ZENG.XIAO.YAN
* @version 1.0
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
// WebMvcConfigurerAdapter 這個類在SpringBoot2.0已過時,官方推薦直接實現WebMvcConfigurer 這個接口
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration jwtInterceptorRegistration = registry.addInterceptor(jwtInterceptor());
// 配置攔截器的攔截規則和放行規則
jwtInterceptorRegistration.addPathPatterns("/jwt/**")
.excludePathPatterns("/jwt/login");
}
}
x
1
/**
2
* web相關的定制化配置
3
* @author ZENG.XIAO.YAN
4
* @version 1.0
5
*/
6
7
public class WebConfig implements WebMvcConfigurer {
8
// WebMvcConfigurerAdapter 這個類在SpringBoot2.0已過時,官方推薦直接實現WebMvcConfigurer 這個接口
9
10
11
public JwtInterceptor jwtInterceptor() {
12
return new JwtInterceptor();
13
}
14
15
16
public void addInterceptors(InterceptorRegistry registry) {
17
InterceptorRegistration jwtInterceptorRegistration = registry.addInterceptor(jwtInterceptor());
18
// 配置攔截器的攔截規則和放行規則
19
jwtInterceptorRegistration.addPathPatterns("/jwt/**")
20
.excludePathPatterns("/jwt/login");
21
}
22
}
(3)相關測試
- 不帶token訪問 /jwt/test接口,被攔截器攔截;返回token不能為空; 效果如下圖

- 訪問登錄接口,進行登錄;登錄成功,同時返回生成的token;效果如下圖

- 帶上登錄成功返回的token訪問/jwt/test接口,攔截器放行了請求,成功請求到了test方法;效果如下圖

- 當token被篡改或者已過期時,訪問/jwt/test接口,攔截器攔截了該請求,返回token已失效;效果如下圖

進行完上述測試后,說明jwt的集成已經大功告成了。
五、小結
jwt集成不麻煩,但是也有很多不完善的地方,后續再想辦法把它完善。
在我的JwtUtils的verifyAndGetClaimsByToken方法里提到了相關擴展的思路,可以通過該思路來實現token的刷新及其他的騷操作。
當然,jwt也有很多缺點,這里就不在贅述了。