1、什么是JWT
官方文檔解釋:JSON Web Token(JWT)是一個開放標准(RFC 7519),它定義了一種緊湊且獨立的方式,可以在各方之間作為JSON對象安全地傳輸信息。此信息可以通過數字簽名進行驗證和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
官網地址: https://jwt.io/introduction/
通俗來講,JWT是一個含簽名並攜帶用戶相關信息的加密串,頁面請求校驗登錄接口時,客戶端請求頭中攜帶JWT串到后端服務,后端通過簽名加密串匹配校驗,保證信息未被篡改。校驗通過則認為是可靠的請求,將正常返回數據。
2、JWT解決了什么問題
- 授權:這是最常見的使用場景,解決單點登錄問題。因為JWT使用起來輕便,開銷小,服務端不用記錄用戶狀態信息(無狀態),所以使用比較廣泛;
- 信息交換:JWT是在各個服務之間安全傳輸信息的好方法。因為JWT可以簽名,例如,使用公鑰/私鑰是以對兒 - 可以確定請求方是合法的。此外,由於使用標頭和有效負載計算簽名,還可以驗證內容是否未被篡改
3、早期的SSO認證
我們知道,http協議本身是一種無狀態的協議,而這就意味着如果用戶向我們的應用提供了用戶名和密碼來進行用戶認證,那么下一次請求時,用戶還要再一次進行用戶認證才行,因為根據http協議,我們並不能知道是哪個用戶發出的請求,所以為了讓我們的應用能識別是哪個用戶發出的請求,我們只能在服務器存儲一份用戶登錄的信息,這份登錄信息會在響應時傳遞給瀏覽器,告訴其保存為cookie,以便下次請求時發送給我們的應用,這樣我們的應用就能識別請求來自哪個用戶了,這就是傳統的基於session認證。
4、JWT認證
首先,前端通過Web表單將自己的用戶名和密碼發送到后端的接口。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協議)
后端核對用戶名和密碼成功后,將用戶的id等其他信息作為JWT Payload(負載),將其與頭部分別進行Base64編碼拼接后簽名,形成一個JWT(Token)。形成的JWT就是一個形同aaaa.bbb.cc的字符串。 token head.payload.singurater
后端將JWT字符串作為登錄成功的返回結果返回給前端。前端可以將返回的結果保存在localStorage或sessionStorage上,退出登錄時前端刪除保存的JWT即可。
前端在每次請求時將JWT放入HTTP Header中的Authorization位。(解決XSS和XSRF問題) HEADER
后端檢查是否存在,如存在驗證JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期;檢查Token的接收方是否是自己(可選)。
驗證通過后后端使用JWT中包含的用戶信息進行其他邏輯操作,返回相應結果。
5、JWT優勢
JWT 是一個開放標准(RFC 7519),它定義了一種用於簡潔,自包含的用於通信雙方之間以 JSON 對象的形式安全傳遞信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公鑰密鑰對進行簽名。它具備兩個特點:
- 簡潔(Compact)
可以通過URL, POST 參數或者在 HTTP header 發送,因為數據量小,傳輸速度快
- 自包含(Self-contained)
負載中包含了所有用戶所需要的信息,避免了多次查詢數據庫
- 自校驗
對token可以自己校驗是否過期
6、JWT結構
令牌組成
- 標頭(Header)
- 有效載荷(Payload)
- 簽名(Signature)
Header 標頭
標頭通常由兩部分組成:令牌的類型(即JWT)和所使用的簽名算法。它會使用 Base64 對header做編碼,組成而來JWT結構的第一部分。
Base64是一種編碼,也就是說,它是可以被翻譯回原來的樣子來的。它並不是一種加密過程。
{
"alg": "HS256", # 簽名算法
"typ": "JWT" # 類型
}
Payload 負載
這部分就是我們存放信息的地方了,你可以把用戶 ID 等信息放在這里,JWT 規范里面對這部分有進行了比較詳細的介紹,常用的由 iss(簽發者),exp(過期時間),sub(面向的用戶),aud(接收方),iat(簽發時間)。同樣的,它也會使用 Base64 編碼組成 JWT 結構的第二部分
{
"iss": "demo JWT",
"iat": 1342513302,
"exp": 1342513302,
"name": "admin",
"sub": "dev"
}
Signature 簽名
前面兩部分都是使用 Base64 進行編碼的,即前端可以解開知道里面的信息。Signature 需要使用編碼后的 header 和 payload 以及我們提供的一個密鑰,然后使用 header 中指定的簽名算法(HS256)進行簽名。簽名的作用是保證 JWT 沒有被篡改過。
三個部分通過
.
連接在一起就是我們的 JWT 了
簽名的目的
最后一步簽名的過程,實際上是對頭部以及負載內容進行簽名,防止內容被竄改。如果有人對頭部以及負載的內容解碼之后進行修改,再進行編碼,最后加上之前的簽名組合形成新的JWT的話,那么服務器端會判斷出新的頭部和負載形成的簽名和JWT附帶上的簽名是不一樣的。如果要對新的頭部和負載進行簽名,在不知道服務器加密時用的密鑰的話,得出來的簽名也是不一樣的。
信息安全性
在這里大家一定會問一個問題:Base64是一種編碼,是可逆的,那么我的信息不就被暴露了嗎?
是的。所以,在JWT中,不應該在負載里面加入任何敏感的數據。在上面的例子中,我們傳輸的是用戶的User ID。這個值實際上不是什么敏感內容,一般情況下被知道也是安全的。但是像密碼這樣的內容就不能被放在JWT中了。如果將用戶的密碼放在了JWT中,那么懷有惡意的第三方通過Base64解碼就能很快地知道你的密碼了。
因此JWT適合用於向Web應用傳遞一些非敏感信息。JWT還經常用於設計用戶認證和授權系統,甚至實現Web應用的單點登錄。
7、Hello-Word
添加依賴
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
生成Token
@Test
void testCreateToken() {
// 1.設置超時時間
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND,30); // 超時時間是30s
// 2.創建JWTbuilder
JWTCreator.Builder builder = JWT.create();
// 3.設置頭,負載,簽名
String token = builder
// .withHeader(map) 設置頭信息,可以不設置有默認值
.withClaim("name", "admin")
.withClaim("id", 10) // 設置用戶自定義屬性
.withExpiresAt(calendar.getTime()) // 設置令牌超時時間
.sign(Algorithm.HMAC256("dalaoshi"));// 設置用戶簽名
// 4.輸出結果
System.out.println(token);
}
認證token
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWRtaW4iLCJpZCI6MTAsImV4cCI6MTU5OTQwNTQ2NH0.7YFYieOC-ChS32He7DqyVtECCvM4nFWmb7hKLiPAIXY\n";
// 1.根據用戶簽簽名獲取JTW校驗器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("dalaoshi")).build();
// 2.驗證token
DecodedJWT verify = jwtVerifier.verify(token);
// 3.獲取token的數據
System.out.println(verify.getClaim("name").asString()); // 字符串使用asString()
System.out.println(verify.getClaim("id").asInt()); // int使用asInt
System.out.println(verify.getExpiresAt()); // 獲取過期時間
認證常見的異常
- SignatureVerificationException: 簽名不一致異常
- TokenExpiredException: 令牌過期異常
- AlgorithmMismatchException: 算法不匹配異常
- InvalidClaimException: 失效的payload異常
- JWTDecodeException
8、工具類
public class JWTUtils {
private static String sign = "dalaoshi";
public static String createToken(Map<String, String> map) {
// 1.設置超時時間
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 7); // 7天
// 2.創建JWTbuilder
JWTCreator.Builder builder = JWT.create();
// 設置負載數據
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entrie : entries) {
builder.withClaim(entrie.getKey(), entrie.getValue());
}
// 3.設置簽名,過期時間
String token = builder
.withExpiresAt(calendar.getTime()) // 設置令牌超時時間
.sign(getSignature());// 設置用戶簽名
// 4.返回
return token;
}
// 獲取起簽名
public static Algorithm getSignature() {
return Algorithm.HMAC256(sign);
}
// 校驗
public static DecodedJWT require(String token) {
return JWT.require(getSignature()).build().verify(token);
}
// 獲取token中的數據
public static Claim getPayload(String token, String key) {
return require(token).getClaim(key);
}
}
9、JWT整合Web
@Autowired
private IUserService userService;
@RequestMapping("/login")
public ResultEntity login(String username,String password){
ResultEntity resultEntity = userService.login(username, password);
if(ResultEntity.SUCEESS.equals(resultEntity.getStatus())){
Map<String,String> map = new HashMap<>();
map.put("id","10");
map.put("username",username);
String token = JWTUtils.createToken(map);
return ResultEntity.success(token);
}else{
return ResultEntity.error("登錄失敗");
}
}
@RequestMapping("/require")
public ResultEntity require(String token){
try {
DecodedJWT require = JWTUtils.require(token);
return ResultEntity.response(require);
}catch (TokenExpiredException e){
return ResultEntity.error("token過期");
}catch (SignatureVerificationException e){
return ResultEntity.error("用戶簽名不一致");
} catch (InvalidClaimException e){
return ResultEntity.error("payload數據有誤");
}catch (Exception e){
return ResultEntity.error("校驗失敗");
}
}
@RequestMapping(value = "/getPayLoad")
public ResultEntity getPayLoad(String token){
DecodedJWT decodedJWT = JWTUtils.require(token);
Map<String, Claim> claims = decodedJWT.getClaims();
Map<String,String> map = new HashMap<>();
Set<Map.Entry<String, Claim>> entries = claims.entrySet();
for (Map.Entry<String, Claim> entrie:entries) {
map.put(entrie.getKey(),entrie.getValue().asString());
}
return ResultEntity.success(map);
}
10、攔截器校驗
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.獲取token
String token = request.getHeader("token");
Map<String,Object> map = new HashMap<>();
try {
// 2.校驗
JWTUtils.verify(token);
return true;
}catch (TokenExpiredException e){
return ResultEntity.error("token過期");
}catch (SignatureVerificationException e){
return ResultEntity.error("用戶簽名不一致");
} catch (InvalidClaimException e){
return ResultEntity.error("payload數據有誤");
}catch (Exception e){
return ResultEntity.error("校驗失敗");
}
// 3.校驗失敗響應數據
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
11、網關路由校驗
@Component
public class SSOFilter extends ZuulFilter{
@Autowired
private ISSOService ssoService;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER-1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
StringBuffer requestURL = request.getRequestURL();
System.out.println(requestURL);
// 1.該服務是否需要驗證
if("http://localhost/shop-back/user/getUserPage".equals(requestURL.toString())){
String token = request.getHeader("token");
// 2.驗證服務
ResultEntity resultEntity = ssoService.require(token);
System.out.println(resultEntity);
if(!ResultEntity.SUCEESS.equals(resultEntity.getStatus())){
requestContext.setSendZuulResponse(false); // 不能往下執行了
HttpServletResponse response = requestContext.getResponse();
response.setContentType("application/json;charset=utf-8"); // 設置響應數據類型
requestContext.setResponseBody(JSON.toJSONString(ResultEntity.error("校驗未通過"))); // 設置響應數據
}
}
return null;
}
}
12、解決多用戶登錄的問題
如果一個用戶登錄在多個設備登錄,就會出現一個用戶多個token在多個設備上同時登錄。如果要解決這個問題就要判斷用戶操作的token是否是最新的,只有是最新的token才能認證成功。
// 偽代碼
// login
public String login(String name,String password){
// 1.查詢數據庫認證
// 2.生成token
String token = "";
// 3.把用戶最新的token放入到reids中
redisTemp.set(username,token); // username作為key,多次登錄key會被覆蓋
}
// 路由校驗
// 1.獲取用戶token
// 2.根據用戶名查詢用戶最新的token
// 3.對比兩個token是否一致,如果不一致就說明用戶進行了第二次登陸,就不讓認證通過。
13、客戶端保存/攜帶token
// 登錄獲取token,保存到本地
function login(){
var username ="admin";
var password ="123";
var param = new Object();
param.username=username;
param.password=password;
$.post("http://localhost/shop-sso/sso/login",param,function (data) {
if(data.status ="success"){
// 獲取token
var token = data.data;
// 保存toke到客戶端
localStorage.setItem("login-token",token);
}
},"JSON");
}
// 發送請求是把token放到請求頭中保存
function sendRequest(){
$.ajax({
url: "http://localhost/shop-sso/addXxxxx",
type: "post",
dataType: 'json',
beforeSend: function (XMLHttpRequest) {
// 獲取本地儲存的token,添加到請求頭中
XMLHttpRequest.setRequestHeader("Authorization", localStorage.getItem("login-token"));
},
success: function (result) {
}
});
}
為什么要把token放在請求頭中的Authorization中?
a)保存在請求頭中方便和其他參數區分
b)保存在請求頭中可以解決跨域的問題,比如cookie是存在跨域的問題
c)Authorization header就是為用戶認證而生的。
d)解決XSS和XSRF問題
14、抽取ajax工具類
window.utils={
ajax:function(param){
$.ajax({
url: param,
type: "post",
dataType: 'json',
data:param.data,
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader("Authorization", localStorage.getItem("login-token"));
},
success: function (result) {
param.success(result);
}
});
}
}
// 調用
utils.ajax({
url:"http://localhost/shop-sso/sso/login",
data:param,
success:function(data){
if(data.status ="success"){
// 獲取token
var token = data.data;
// 保存toke到客戶端
localStorage.setItem("login-token",token);
}
}
})
15、a標簽跳轉如何傳遞token
token只針對api設計,和原生標簽的跳轉沒有直接的關系。如果請求跳轉可以在url后面攜帶token。