SpringBoot 使用 JWT
登錄方式對比
傳統登錄方式 Session + Cookie
- 客戶端向服務器發送用戶名和密碼
- 服務器驗證通過,並把相關數據保存在 Session 中,例如登錄時間之類的
- 服務器返回給用戶一個 SessionId ,客戶端把這個 SessionId 寫入 Cookie
- 用戶每次請求都會通過 Cookie 提交 SessionId 到服務器
- 服務器收到 SessionId 后查找數據,就可以知道用戶身份
特點:
- 數據存儲在服務器,安全性較強,但是占用服務器資源
- 因為使用到了 Cookie ,所以會被偽造
- 如果服務器較多,或者跨域訪問之類的操作,就要求共享 Session 資源,否則就需要用戶和服務器重復登錄驗證操作,或者記錄用戶登錄的服務器,對服務器和用戶體驗都不好。
JWT 方式登錄
- 客戶端向服務器發送用戶名和密碼
- 服務器驗證通過,對用戶數據進行加密,生成 Token 返回給客戶端
- 瀏覽器(客戶端)接收到 Token 后,將 Token 存儲在 Local Storage,需要使用 JavaScript 代碼獲取,而 Cookie 是自動攜帶
- 用戶每次請求都把 Token 提交到服務器
- 服務器對傳來的 Token 進行解密,再去查詢用戶數據,一次知道用戶身份
特點:
- 存儲在客戶端,不占用服務器資源,但是同樣會被偽造
- 前后端分離,帶上 Token 進行請求,不需要考慮用戶是在哪個服務器上登錄的,多服務器和跨域請求都沒有問題
建議:對數據庫的增刪改,必須加上 Token 驗證,查詢不加 Token ,這樣效率會比較高,同時查詢操作也無法獲取 Token ,更安全
如何強制token失效?
在數據庫里保存一份 Token ,驗證時再拿出來校驗,重新登錄就刷新覆蓋這個值
JWT
JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。
先看概念
官網:https://jwt.io/
這張圖來自官網
JWT 結構
JWT 分為三個部分
- Header,算法和令牌類型
{
"alg": "HS256",
"typ": "JWT"
}
- Payload:數據,實際需要傳遞的 JSON 對象
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
- Verify Signature:簽名,用於防偽驗證
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
加密之后的結果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
終於可以上代碼了
代碼
新建一個 SpringBoot 項目
application.properties ,我只配置了最基本的
#配置程序端口,默認為8080
server.port= 8080
# 配置默認訪問路徑,默認為/
server.servlet.context-path=/jwt_demo
# 配置 Tomcat
# 配置Tomcat編碼,默認為UTF-8
server.tomcat.uri-encoding=UTF-8
# 配置最大線程數
server.tomcat.max-threads=1000
JWT 依賴庫
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
模型類
因為我要用 RESTful 所以可以用一個模型類
public class UserModel
{
private String _username;
private String _password;
public String get_username()
{
return _username;
}
public void set_username(String _username)
{
this._username = _username;
}
public String get_password()
{
return _password;
}
public void set_password(String _password)
{
this._password = _password;
}
}
控制器
DemoController ,用於測試訪問數據,Foo_01()不需要Token,Foo_02()需要 Token
@RestController
@RequestMapping("/v1/Demo")
public class DemoController
{
@RequestMapping(value = "/Get1",method = RequestMethod.GET,produces = "application/json")
public List<String> Foo_01()
{
List<String> list=new ArrayList<>();
list.add("Foo_01");
list.add("Test");
return list;
}
@RequestMapping("/Get2")
public List<String> Foo_02()
{
List<String> list=new ArrayList<>();
list.add("Foo_02");
list.add("Test");
return list;
}
}
LoginController ,用於登錄並獲取 Token
@RestController
@RequestMapping("/v1/Login")
public class LoginController
{
@RequestMapping(value = "/Login", method = RequestMethod.POST, produces = "application/json")
public String Login(@RequestBody UserModel user)
{
if (user.get_username().equals("abc") && user.get_password().equals("123456"))
{
Map<String, String> claimMap = new HashMap<>();
claimMap.put("username", "abc");
return TokenUtli.GenerateToken(claimMap);
}
return "登錄失敗";
}
}
封裝 JWT 工具類
其實我封裝的很隨便啦,僅用於本案例
public class TokenUtli
{
//Issuer
public static final String ISSUER = "Test.com";
//Audience
public static final String AUDIENCE = "Client";
//密鑰
public static final String KEY = "ThisIsMySecretKey";
//算法
public static final Algorithm ALGORITHM = Algorithm.HMAC256(TokenUtli.KEY);
//Header
public static final Map<String, Object> HEADER_MAP = new HashMap<>()
{
{
put("alg", "HS256");
put("typ", "JWT");
}
};
/**
* 生成 Token 字符串
*
* @param claimMap claim 數據
* @return Token 字符串
*/
public static String GenerateToken(Map<String, String> claimMap)
{
Date nowDate = new Date();
//120 分鍾過期
Date expireDate = TokenUtli.AddDate(nowDate, 2 * 60);
//Token 建造器
JWTCreator.Builder tokenBuilder = JWT.create();
for (Map.Entry<String, String> entry : claimMap.entrySet())
{
//Payload 部分,根據需求添加
tokenBuilder.withClaim(entry.getKey(), entry.getValue());
}
//token 字符串
String token = tokenBuilder.withHeader(TokenUtli.HEADER_MAP)//Header 部分
.withIssuer(TokenUtli.ISSUER)//issuer
.withAudience(TokenUtli.AUDIENCE)//audience
.withIssuedAt(nowDate)//生效時間
.withExpiresAt(expireDate)//過期時間
.sign(TokenUtli.ALGORITHM);//簽名,算法加密
return token;
}
/**
* 時間加法
*
* @param date 當前時間
* @param minute 持續時間(分鍾)
* @return 時間加法結果
*/
private static Date AddDate(Date date, Integer minute)
{
if (null == date)
{
date = new Date();
}
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.add(Calendar.MINUTE, minute);
return calendar.getTime();
}
}
在此示例中,我們指定了必須考慮哪些參數才能將 JWT 視為有效。根據我們的代碼,以下項目認為令牌有效:
- 驗證生成令牌的服務器 Issuer
- 驗證令牌的接收者被授權接收 Audience
- 檢查令牌是否未過期以及頒發者的簽名密鑰是否有效 Lifetime
- 驗證令牌的簽名 IssuerSigningKey
- 此外,我們指定Issuer、Audience、SigningKey的值。在本例中,我將這些值存儲在常量中。
驗證 Token ,Token 錯誤或者過期就拋出異常,這里我就只判斷了一個空字符串,其它驗證規則可以自己寫啦
/**
* 驗證 Token
*
* @param webToken 前端傳遞的 Token 字符串
* @return Token 字符串是否正確
* @throws Exception 異常信息
*/
public static boolean VerifyJWTToken(String webToken) throws Exception
{
String[] token = webToken.split(" ");
if (token[1].equals(""))
{
throw new Exception("token錯誤");
}
//JWT驗證器
JWTVerifier verifier = JWT.require(TokenUtli.ALGORITHM).withIssuer(TokenUtli.ISSUER).build();
//解碼
DecodedJWT jwt = verifier.verify(token[1]);
//Audience
List<String> audienceList = jwt.getAudience();
String audience = audienceList.get(0);
//Payload
Map<String, Claim> claimMap = jwt.getClaims();
for (Map.Entry<String, Claim> entry : claimMap.entrySet())
{
}
//生效時間
Date issueTime = jwt.getIssuedAt();
//過期時間
Date expiresTime = jwt.getExpiresAt();
return true;
}
注冊攔截器
JWTInterceptor
public class JWTInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
//從請求頭內獲取token
String token = request.getHeader("authorization");
//驗證令牌,如果令牌不正確會出現異常會被全局異常處理
return TokenUtli.VerifyJWTToken(token);
}
}
注冊攔截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer
{
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(new JWTInterceptor()).addPathPatterns("/**")//全部路徑
.excludePathPatterns("/v1/Demo/Get1")//排除不需要Token的路徑
.excludePathPatterns("/v1/Login/Login");//開放登錄路徑
}
}
測試
因為懶得寫前端頁面,所以使用 postman 調試
首先,不登錄去訪問 DemoController 里的兩個函數
報了 500錯誤
登錄,以及生成的 Token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJDbGllbnQiLCJpc3MiOiJUZXN0LmNvbSIsImV4cCI6MTYyNjY4OTczMiwiaWF0IjoxNjI2NjgyNTMyLCJ1c2VybmFtZSI6ImFiYyJ9.ZtSyoqmUVKgGY3_wFQD24_mOot7qnMUKh8xMPKOW2JQ
使用 Token 再去測試需要 Token 的函數
這次就訪問到數據了
SpringBoot 使用 JWT 結束
項目結構