轉自:https://blog.csdn.net/XlxfyzsFdblj/article/details/81705882
從網上摘錄了一些關於 JWT 的優點
1. 相比於session,它無需保存在服務器,不佔用服務器內存開銷。
2. 無狀態、可拓展性強:比如有3臺機器(A、B、C)組成服務器集羣,若session存在機器A上,session只能保存在其中一臺服務器,此時你便不能訪問機器B、C,因爲B、C上沒有存放該Session,而使用token就能夠驗證用戶請求合法性,並且我再加幾臺機器也沒事,所以可拓展性好就是這個意思。
JWT的大致思路就是:(感覺不太嚴謹,僅當參考即可)
後臺收到前端的登錄驗證請求,賬號驗證成功後,後臺創建token並把token返回給前端,前端獲取到token後,把token存入cookie中,之後獲取數據的請求都要在請求頭中加入token。後臺會從請求頭中解析token,來驗證請求的安全性。token不保存在服務端,只保存在前端。後臺解析token不需要匹對服務端的數據庫或者本地文件等存儲介質。所以jwt是相對獨立的。
附上一個流程圖
JWT詳細說明,參考博客
https://blog.csdn.net/why15732625998/article/details/78534711
下面就是運用實例基於SpringBoot
Maven配置
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>5.14</version>
</dependency>
JWT核心代碼
參考博客https://blog.csdn.net/baidu_38532123/article/details/79211042
import com.aekc.mmall.enums.TokenState;
import com.google.common.collect.Maps;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import net.minidev.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
/**
* JWT組成
* 第一部分我們稱它爲頭部(header),第二部分我們稱其爲載荷(payload),第三部分是簽證(signature)。
*/
public class JwtUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtil.class);
/**
* 公共祕鑰-保存在服務端,客戶端是不知道該祕鑰的,防止被攻擊。(signature)
*/
private static final byte[] SECRET = "1234567890qwertyuiopasdfghjklzxcvbnm".getBytes();
/**
* 初始化head部分的數據爲(第一部分)
* {
* "alg":"HS256",
* "type":"JWT"
* }
*/
private static final JWSHeader HEADER = new JWSHeader(JWSAlgorithm.HS256, JOSEObjectType.JWT, null, null, null, null, null, null, null, null, null, null, null);
/**
* 生成token,該方法只在用戶登錄成功後調用
* @param payload Map集合,可以存儲用戶id,token生成時間,token過期時間等自定義字段
* @return token字符串,若失敗則返回null
*/
public static String createToken(Map<String, Object> payload) {
String tokenString = null;
// 創建一個JWS Object(第二部分)
JWSObject jwsObject = new JWSObject(HEADER, new Payload(new JSONObject(payload)));
try {
// 將jwsObject進行HMAC簽名,相當於加密(第三部分)
jwsObject.sign(new MACSigner(SECRET));
tokenString = jwsObject.serialize();
} catch (JOSEException e) {
LOGGER.error("簽名失敗: {}", e.getMessage());
e.printStackTrace();
}
return tokenString;
}
/**
* 校驗token是否合法,返回Map集合,集合中主要包含 state狀態碼 data鑑權成功後從token中提取的數據
* 該方法在過濾器中調用,每次請求API時都校驗
* @param token token
* @return Map<String, Object>
*/
public static Map<String, Object> validToken(String token) {
Map<String, Object> resultMap = Maps.newHashMap();
try {
JWSObject jwsObject = JWSObject.parse(token);
// palload就是JWT構成的第二部分不過這裏自定義的是私有聲明(標準中註冊的聲明, 公共的聲明)
Payload payload = jwsObject.getPayload();
JWSVerifier verifier = new MACVerifier(SECRET);
if(jwsObject.verify(verifier)) {
JSONObject jsonObject = payload.toJSONObject();
// token檢驗成功(此時沒有檢驗是否過期)
resultMap.put("state", TokenState.VALID.toString());
// 若payload包含ext字段,則校驗是否過期
if(jsonObject.containsKey("ext")) {
long extTime = Long.valueOf(jsonObject.get("ext").toString());
long curTime = System.currentTimeMillis();
// 過期了
if(curTime > extTime) {
resultMap.clear();
resultMap.put("state", TokenState.EXPIRED.toString());
}
}
resultMap.put("data", jsonObject);
} else {
// 檢驗失敗
resultMap.put("state", TokenState.INVALID.toString());
}
} catch (Exception e) {
e.printStackTrace();
// token格式不合法導致的異常
resultMap.clear();
resultMap.put("state", TokenState.INVALID.toString());
}
return resultMap;
}
}
token的枚舉信息
public enum TokenState {
/** 過期 */
EXPIRED("EXPIRED"),
/** 無效(token不合法) */
INVALID("INVALID"),
/** 有效的 */
VALID("VALID");
private String state;
TokenState(String state) {
this.state = state;
}
/**
* 根據狀態字符串獲取token狀態枚舉對象
* @param tokenState
* @return TokenState
*/
public static TokenState getTokenState(String tokenState) {
TokenState[] states = TokenState.values();
TokenState ts = null;
for(TokenState state : states) {
if(state.toString().equals(tokenState)) {
ts = state;
break;
}
}
return ts;
}
@Override
public String toString() {
return this.state;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
配置兩個攔截器interceptor
一個是攔截所有請求的HttpInterceptor用來設定返回頭信息。另一個JwtInterceptor用來攔截除了登錄外的請求。
@Component
public class HttpInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 允許跨域
response.setHeader("Access-Control-Allow-Origin", "*");
// 允許自定義請求頭token(允許head跨域)
response.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
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 {
}
}
@Component
public class JwtInterceptor implements HandlerInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtInterceptor.class);
private void output(JsonData jsonData, HttpServletResponse response) throws IOException {
response.setContentType("text/html;charset=UTF-8;");
PrintWriter out = response.getWriter();
out.write(Objects.requireNonNull(JsonUtil.objectToJson(jsonData)));
out.flush();
out.close();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 前段ajax自定義headers字段,會出現了option請求,在GET請求之前。
// 所以應該把他過濾掉,以免影響服務。但是不能返回false,如果返回false會導致後續請求不會繼續。
if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
//從請求頭中獲取token
String token = request.getHeader("token");
Map<String, Object> resultMap = JwtUtil.validToken(token);
TokenState state = TokenState.getTokenState((String) resultMap.get("state"));
switch(state) {
case VALID:
// 取出payload中數據,放到request作用域中
request.setAttribute("data", resultMap.get("data"));
return true;
case EXPIRED:
case INVALID:
LOGGER.warn("無效token");
//JsonData是返回給前端的json格式(不重要)
JsonData jsonData = new JsonData(false);
jsonData.setMsg("您的token不合法或者過期了,請重新登陸");
output(jsonData, response);
break;
default:
break;
}
return false;
}
}
把interceptor註冊到spring容器中,並設置攔截的url
import com.aekc.mmall.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootConfiguration
public class InterceptorConfiguration implements WebMvcConfigurer {
@Autowired
private HttpInterceptor httpInterceptor;
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(httpInterceptor).addPathPatterns("/**");
registry.addInterceptor(jwtInterceptor).addPathPatterns("/sys/**");
}
}
Controller
在登錄的時候創建token
@GetMapping(value = "/login")
public JsonData login(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
//這個步驟就是獲取user的全部信息不重要,直接忽略
SysUser sysUser = sysUserService.findByKeyword(username);
sysUser.setPassword(null);
String token = createPayLoad(sysUser.getId());
return JsonData.success(token);
}
/**
* JWT的組成:Header + payload + signature
* Payload(載荷)的組成信息,私有聲明(標準中註冊的聲明和公共的聲明並未使用)
* @param userId 用戶id
* @return token
*/
private String createPayLoad(Integer userId) {
Map<String, Object> payload = Maps.newHashMap();
Date date = new Date();
// 用戶id
payload.put("uid", String.valueOf(userId));
// 生成時間:當前
payload.put("iat", date.getTime());
// 過期時間10分鐘(單位毫秒)
payload.put("ext", date.getTime() + 1000*60*10);
return JwtUtil.createToken(payload);
}
下面是前端ajax代碼,在羅列代碼前說下preflighted request
自定義header字段會導致一種叫做preflighted request的請求。
preflighted request在發送真正的請求前, 會先發送一個方法爲OPTIONS的預請求(preflight request), 用於試探服務端是否能接受真正的請求,如果options獲得的迴應是拒絕性質的,比如404\403\500等http狀態,就會停止post、put等請求的發出。
那麼, 什麼情況下請求會變成preflighted request呢?
1、請求方法不是GET/HEAD/POST
2、POST請求的Content-Type並非application/x-www-form-urlencoded, multipart/form-data, 或text/plain
3、請求設置了自定義的header字段
參考博客https://blog.csdn.net/cc1314_/article/details/78272329
//登錄ajax,登錄成功後獲取後臺返回的token,並把token保存到cookie中
function signIn() {
let username = $("input[name='username']").val();
let password = $("input[name='password']").val();
$.ajax({
url: urlHead + "/user/login",
type: "GET",
dataType: "json",
data: {username: username, password: password},
success: function (result) {
//保存token用來判斷用戶是否登錄,和身份是否屬實
$.cookie('token', result.data);
}
})
}
//請求數據的ajax,需要從cookie讀取token放入head傳給後臺。
function loadDeptTree() {
$.ajax({
// 自定義的headers字段,會出現option請求,在GET請求之前,後臺要記得做檢驗。
headers: {
token: $.cookie('token')
},
url: urlHead + "/sys/dept/tree",
type: 'GET',
dataType: 'json',
success : function (result) {
}
})
}
註銷賬戶或退出登錄時就把所有cookie清除,不需要向後臺驗證。
// 註銷,清空所有cookie(或者只清空保存着token的Cookie就行)
function logout() {
var keys = document.cookie.match(/[^ =;]+(?=\=)/g);
if(keys) {
for(var i = keys.length; i--;)
document.cookie = keys[i] + '=0;expires=' + new Date(0).toUTCString()
}
//返回登錄頁面或者主頁
window.location.href = "signin.html";
}