前言
定義:JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案
由於HTTP協議是無狀態的,這意味着如果我們想判定一個接口是否被認證后訪問,就需要借助cookie或者session會話機制進行判定,但是由於現在的系統架構大部分都不止一台服務器,此時又要借助數據庫或者全局緩存 做存儲,這種方案顯然受限太多。
那么我們可不可以讓認證令牌的發布者自己去識別這個令牌是不是我曾經發布的令牌呢(JWT核心思想),這是JWT最大的優點也是最大的缺點,優點是簡單快捷、不需要依賴任何第三方操作就能實現身份認證,缺點就是對於任何擁有用戶發布令牌的請求都會認證通過。
JWT的數據結構
正常的JWT數據結構應該如下
它是一個很長的字符串,中間用點(.
)分隔成三個部分
JWT的三個部分依次: Header - 頭部 、Payload - 負載 、Signature(簽名)
即:Header.Payload.Signature
Header
Header 部分是一個 JSON 對象,描述 JWT 的元數據,通常是下面的樣子。
{
"alg": "HS256",
"typ": "JWT"
}
alg
屬性表示簽名的算法(algorithm),默認是 HMAC SHA256(寫成 HS256);typ
屬性表示這個令牌(token)的類型(type),JWT 令牌統一寫為JWT
Payload
Payload 部分也是一個 JSON 對象,用來存放實際需要傳遞的數據。JWT 規定了7個官方字段,供選用。
iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號
除了官方字段,你還可以在這個部分定義私有字段
{
"sub": "1234567890",
"name": "John Doe",
"age": "19"
}
注意:JWT默認是明文展示,任何人都可以讀取到,所以此處不要放私密信息
這個 JSON 對象也要使用 Base64URL 算法轉成字符串。
Signature
Signature 部分是對前兩部分的簽名,防止數據篡改。
首先,需要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。然后,使用 Header 里面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出簽名以后,把 Header、Payload、Signature 三個部分拼成一個字符串,每個部分之間用"點"(.
)分隔,就可以返回給用戶。
Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。這個算法跟 Base64 算法基本類似,但有一些小的不同。
JWT 作為一個令牌(token),有些場合可能會放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個字符+
、/
和=
,在 URL 里面有特殊含義,所以要被替換掉:=
被省略、+
替換成-
,/
替換成_
。這就是 Base64URL 算法
JWT的實現
Maven依賴
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.5.0</version>
</dependency>
JWT簽名發布和驗證代碼
public class TokenUtil {
//Token的過期時間
private static final long EXPIRE_TIME = 30 * 60 * 1000;
//Token的私鑰
private static final String TOKEN_SECRET = "jytoken_secret";
/**
* 生成簽名,30分鍾過期
* @param **userInfo** 用戶信息 用戶姓名
* @param **other** 用戶其他信息 用戶id
* @return
*/
public static String sign(String userInfo, String other) {
try {
// 設置過期時間
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//私鑰和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 設置頭部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HS256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("userInfo", userInfo)
.withClaim("other", other)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 生成簽名,30分鍾過期
* @param **userInfo** 用戶信息 用戶姓名
* @param **other** 用戶其他信息 用戶id
* @return
*/
public static String sign(String userInfo, String other,long expire) {
try {
// 設置過期時間
Date date = new Date(System.currentTimeMillis() + expire);
//私鑰和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 設置頭部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HS256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("userInfo", userInfo)
.withClaim("other", other)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 檢驗token是否正確
* @param **token**
* @return
*/
public static boolean verify(String token){
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);//未驗證通過會拋出異常
return true;
} catch (Exception e){
return false;
}
}
/**
* 從token中獲取info信息
* @param **token**
* @return
*/
public static String getUserName(String token,String info){
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(info).asString();
} catch (JWTDecodeException e){
e.printStackTrace();
}
return null;
}
}
攔截器配置無需認證的請求
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Autowired
private TokenHandler tokenHandler;
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> excludePath = new ArrayList<>();
String checkLogin = "/pushlogin/checkIsCanLogin";
String login = "/pushlogin/login";
String getVerifyCode = "/common/send";
String verfifyMethod = "/common/validationCode";
excludePath.add(checkLogin);
excludePath.add(login);
excludePath.add(getVerifyCode);
excludePath.add(verfifyMethod);
registry.addInterceptor(tokenHandler).excludePathPatterns(excludePath);
}
}
Token統一攔截器代碼
@Component
@Slf4j
public class TokenHandler implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authentication");
if (token != null){
boolean result = TokenUtil.verify(token);
if(result){
log.info("通過攔截器");
return true;
}
}
log.info("認證失敗");
return false;
}
}
用戶登錄時驗證用戶信息后,返回Token信息
@Override
public UserDTO selectIsExistUserInfo(String phone) {
//TODO 偽代碼 驗證用戶信息
UserDTO info = 查詢用戶信息
if (info != null) {
String token = TokenUtil.sign(info.getUsername(), info.getUserId(), 6 * 60 * 60 * 1000);
info.setToken(token);
}
return info;
}