JWT簡介
JSON Web Token (JWT)是一個開放标準(RFC 7519),它定義了一種緊湊的、自包含的方式,用于作為JSON對象在各方之間安全地傳輸資訊。該資訊可以被驗證和信任,因為它是數字簽名的。
JWT的實作流程

1. 使用者輸入賬号和密碼發出POST請求;
2. 驗證通過後伺服器應用使用私鑰建立一個JWT;
3. 伺服器應用傳回JWT;
4. 浏覽器将JWT添加在請求頭中向伺服器發送請求;
5. 伺服器應用驗證JWT的正确性以及權限;
6. 驗證通過後伺服器應用傳回響應的資源。
JWT的結構
JWT标準的Token需要包含三個部分:
第一部分:Header(頭部),頭部主要包括參數的類型以及簽名的算法。
第二部分:Payload(有效載荷),有效載荷中包含部分資訊,例如使用者名、登入狀态、有效時間等,在非加密情況下,不要在有效載荷中存放敏感資訊,例如使用者ID、登陸密碼等。
第三部分:Signature(簽名),簽名由加密後的頭部以及加密後的負荷通過“.”拼接而成,伺服器應用在收到JWT資料之後,會首先對頭部和有效載荷用同一算法再次加密,驗證簽名的正确性,是以簽名可以在消息傳遞的過程中判斷資料是否被篡改,在生成簽名的過程中使用非對稱加密還可以判斷JWT發送方的正确性。
JWT的使用場景
第一種情況:授權(Authorization),授權是JWT的最常見的使用場景,一旦使用者輸入正确的驗證資訊,成功登陸之後,使用者送出的每個請求都将包含JWT,進而允許使用者通路指定的服務和資源。JWT也用于實作單點登陸,它占用的開銷很小,而且它可以在跨域時使用。
第二種情況:資訊交換(Information Exchange),JWT本身可以攜帶一些資訊,而且可以被簽名,伺服器可以通過簽名驗證内容是否被篡改。
JWT的特點
- 簡潔:JWT可以通過URL,POST參數或添加在請求頭中發送,資料量小,傳輸速度快。
- 自包含:負載中包含了所有使用者所需要的資訊,避免了多次查詢資料庫。
- 跨語言:由于Token是以JSON加密的形式儲存在用戶端,是以JWT是跨語言的,原則上任何web形式都支援。
- 适用于分布式:不需要在服務端儲存會話資訊,特别适用于分布式微服務。
JWT的代碼實作過程
第一步,在伺服器應用的maven依賴中添加對JWT的依賴
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
第二步,在登入接口驗證登陸資訊正确後,生成JWT
//生成JWT的工具類
public class JWTUtil {
public static String createToken(String userName) {
try {
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS256");//alg為header和payload的加密方式
header.put("typ","JWT");
// 設定過期時間為目前時間後倆小時
Date nowDate = new Date();
Date expireDate = DateUtils.getAfterDate(new Date(),0,0,0,2,0,0);
String token = JWT.create()
// 設定JWT中的header
.withHeader(header)
// 設定使用者名
.withClaim("userName",userName)
// 設定目前時間
.withIssuedAt(nowDate)
// 設定到期時間
.withExpiresAt(expireDate)
// 生成簽名的加密方式
.sign(Algorithm.HMAC256(Constant.SECRETKEY));
return token;
} catch (JWTCreationException e) {
e.printStackTrace();
return null;
}
}
// 重新整理令牌中的目前時間與到期時間
public static String reFreshToken(String token){
// 首先擷取到token中的userName資訊,再生成新的token
try{
Claims claims = Jwts.parser()
.setSigningKey(Constant.SECRETKEY)
.parseClaimsJws(token).getBody();
return createToken(claims.get("userName").toString());
}catch (Exception e){
throw new RuntimeException("解密失敗");
}
}
}
第三步,在伺服器應用中添加驗證JWT的攔截器
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果請求的不是action,就直接通過
if (!(handler instanceof HandlerMethod)) {
return true;
} else {
String token = request.getHeader("Authorization");
// 如果請求中存在token,則驗證token,若token驗證通過,則請求通過,本項目未涉及權限管理,隻存在使用者登入狀态以及登陸逾時的檢測
if (token != null&&token.length()>0) {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(Constant.SECRETKEY)).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("token未通過驗證");
}
} else {
// 若不存在token,則是前台發來的請求或者是背景第一次發來的登陸請求,隻允許請求部分接口
String uri = request.getRequestURI();
if (uri.indexOf("getList") != -1 || uri.indexOf("subscribe") != -1 || uri.indexOf("contact") != -1||uri.indexOf("login")!=-1) {
} else {
throw new RuntimeException("無權限通路");
}
}
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
第四步,注冊攔截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注冊JWT攔截器,攔截所有請問
registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor(){
return new AuthenticationInterceptor();
}
}
第五步,實作登陸邏輯的控制器
@RestController
@RequestMapping("loginapi")
public class AdminLoginController {
// 登陸驗證,使用者名密碼應從資料庫中查詢驗證,目前設定為常量僅作展示
@PostMapping("login")
public ResultBean login(String username, String password, HttpServletRequest request){
String ip = request.getRemoteAddr();
if(Constant.USERNAME.equals(username)&&Constant.PASSWORD.equals(password)){
return ResultUtil.setOK("登陸成功", JWTUtil.createToken(username));
}else {
return ResultUtil.setError(100,"使用者名或密碼錯誤",null);
}
}
}
第六步,前端實作發送登入請求并記錄的代碼
$.ajax({
type: 'post',
url: geturl('/loginapi/login'),
async: true,
data: data.field,
success: function(data) {
layer.msg(data.msg);
if (data.code == 0) {
//登陸成功後會收到token資料,将token存儲到localStorage中
localStorage['token']= data.data;
window.location.href='index.html';
} else {}
}
})
第七步,在下次進行發起操作請求時,在Header中添加token
$.ajax({
type: 'post',
url: url,
async: true,
data: {
type: articleTypeDetail,
_method: "PATCH",
//你的資料
},
beforeSend: function(request) {
request.setRequestHeader("Authorization", localStorage['token'] != null ? localStorage['token'] : null);
},
success: function(data) {
if (data.code == 0) {
layer.msg('修改成功');
} else {
layer.msg('修改失敗');
}
},
error: function(XMLHttpResponse, textStatus, errorThrown) {
layer.msg('登陸逾時,請重新登入');
}
})