詳解使用JWT實現(xiàn)單點登錄(完全跨域方案)
首先介紹一下什么是JSON Web Token(JWT)?
官方文檔是這樣解釋的:JSON Web Token(JWT)是一個開放標準(RFC 7519),它定義了一種緊湊且獨立的方式,可以在各方之間作為JSON對象安全地傳輸信息。此信息可以通過數(shù)字簽名進行驗證和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
雖然JWT可以加密以在各方之間提供保密,但只將專注于簽名令牌。簽名令牌可以驗證其中包含的聲明的完整性,而加密令牌則隱藏其他方的聲明。當使用公鑰/私鑰對簽署令牌時,簽名還證明只有持有私鑰的一方是簽署私鑰的一方。
通俗來講,JWT是一個含簽名并攜帶用戶相關(guān)信息的加密串,頁面請求校驗登錄接口時,請求頭中攜帶JWT串到后端服務(wù),后端通過簽名加密串匹配校驗,保證信息未被篡改。校驗通過則認為是可靠的請求,將正常返回數(shù)據(jù)。
什么情況下使用JWT比較適合?
- 授權(quán):這是最常見的使用場景,解決單點登錄問題。因為JWT使用起來輕便,開銷小,服務(wù)端不用記錄用戶狀態(tài)信息(無狀態(tài)),所以使用比較廣泛;
- 信息交換:JWT是在各個服務(wù)之間安全傳輸信息的好方法。因為JWT可以簽名,例如,使用公鑰/私鑰對兒 - 可以確定請求方是合法的。此外,由于使用標頭和有效負載計算簽名,還可以驗證內(nèi)容是否未被篡改。
JWT的結(jié)構(gòu)體是什么樣的?
JWT由三部分組成,分別是頭信息、有效載荷、簽名,中間以(.)分隔,如下格式:
xxx.yyy.zzz
header(頭信息)
由兩部分組成,令牌類型(即:JWT)、散列算法(HMAC、RSASSA、RSASSA-PSS等),例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,這個JSON被編碼為Base64Url,形成JWT的第一部分。
Payload(有效載荷)
JWT的第二部分是payload,其中包含claims。claims是關(guān)于實體(常用的是用戶信息)和其他數(shù)據(jù)的聲明,claims有三種類型: registered, public, and private claims。
Registered claims: 這些是一組預(yù)定義的claims,非強制性的,但是推薦使用, iss(發(fā)行人), exp(到期時間), sub(主題), aud(觀眾)等;
Public claims: 自定義claims,注意不要和JWT注冊表中屬性沖突,這里可以查看JWT注冊表
Private claims: 這些是自定義的claims,用于在同意使用這些claims的各方之間共享信息,它們既不是Registered claims,也不是Public claims。
以下是payload示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后,再經(jīng)過Base64Url編碼,形成JWT的第二部分;
注意:對于簽名令牌,此信息雖然可以防止篡改,但任何人都可以讀取。除非加密,否則不要將敏感信息放入到Payload或Header元素中。
Signature
要創(chuàng)建簽名部分,必須采用編碼的Header,編碼的Payload,秘鑰,Header中指定的算法,并對其進行簽名。
例如,如果要使用HMAC SHA256算法,將按以下方式創(chuàng)建簽名:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
簽名用于驗證消息在此過程中未被篡改,并且,在使用私鑰簽名令牌的情況下,它還可以驗證JWT的請求方是否是它所聲明的請求方。
輸出是三個由點分隔的Base64-URL字符串,可以在HTML和HTTP環(huán)境中輕松傳遞,與SAML等基于XML的標準相比更加緊湊。
例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT工作機制?
在身份驗證中,當用戶使用其憑據(jù)成功登錄時,將返回JSON Web Token(即:JWT)。由于令牌是憑證,因此必須非常小心以防止出現(xiàn)安全問題。一般情況下,不應(yīng)將令牌保留的時間超過要求。理論上超時時間越短越好。
每當用戶想要訪問受保護的路由或資源時,用戶代理應(yīng)該使用Bearer模式發(fā)送JWT,通常在Authorization header中。標題內(nèi)容應(yīng)如下所示:
Authorization: Bearer <token>
在某些情況下,這可以作為無狀態(tài)授權(quán)機制。服務(wù)器的受保護路由將檢查Authorization header中的有效JWT ,如果有效,則允許用戶訪問受保護資源。如果JWT包含必要的數(shù)據(jù),則可以減少查詢數(shù)據(jù)庫或緩存信息。
如果在Authorization header中發(fā)送令牌,則跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。
注意:使用簽名令牌,雖然他們無法更改,但是令牌中包含的所有信息都會向用戶或其他方公開。這意味著不應(yīng)該在令牌中放置敏感信息。
使用JWT的好處是什么?
相比Simple Web Tokens (SWT)(簡單Web令牌) and Security Assertion Markup Language Tokens (SAML)(安全斷言標記語言令牌);
- JWT比SAML更簡潔,在HTML和HTTP環(huán)境中傳遞更方便;
- 在安全方面,SWT只能使用HMAC算法通過共享密鑰對稱簽名。但是,JWT和SAML令牌可以使用X.509證書形式的公鑰/私鑰對進行簽名。與簽名JSON的簡單性相比,使用XML數(shù)字簽名可能會存在安全漏洞;
- JSON解析成對象相比XML更流行、方便。
以下是我實際項目中的應(yīng)用分析
首先看一下大致的架構(gòu)及流程圖:


主要有以下三步:
項目一開始我先封裝了一個JWTHelper工具包(GitHub下載),主要提供了生成JWT、解析JWT以及校驗JWT的方法,其他還有一些加密相關(guān)操作,稍后我會以代碼的形式介紹下代碼。工具包寫好后我將打包上傳到私服,能夠隨時依賴下載使用;
接下來,我在客戶端項目中依賴JWTHelper工具包,并添加Interceptor攔截器,攔截需要校驗登錄的接口。攔截器中校驗JWT有效性,并在response中重新設(shè)置JWT的新值;
最后在JWT服務(wù)端,依賴JWT工具包,在登錄方法中,需要在登錄校驗成功后調(diào)用生成JWT方法,生成一個JWT令牌并且設(shè)置到response的header中。
以下是部分代碼分享:
JwtHelper工具類:
/**
* @Author: Helon
* @Description: JWT工具類
* 參考官網(wǎng):https://jwt.io/
* JWT的數(shù)據(jù)結(jié)構(gòu)為:A.B.C三部分數(shù)據(jù),由字符點"."分割成三部分數(shù)據(jù)
* A-header頭信息
* B-payload 有效負荷 一般包括:已注冊信息(registered claims),公開數(shù)據(jù)(public claims),私有數(shù)據(jù)(private claims)
* C-signature 簽名信息 是將header和payload進行加密生成的
* @Data: Created in 2018/7/19 14:11
* @Modified By:
*/
public class JwtHelper {
private static Logger logger = LoggerFactory.getLogger(JwtHelper.class);
/**
* @Author: Helon
* @Description: 生成JWT字符串
* 格式:A.B.C
* A-header頭信息
* B-payload 有效負荷
* C-signature 簽名信息 是將header和payload進行加密生成的
* @param userId - 用戶編號
* @param userName - 用戶名
* @param identities - 客戶端信息(變長參數(shù)),目前包含瀏覽器信息,用于客戶端攔截器校驗,防止跨域非法訪問
* @Data: 2018/7/28 19:26
* @Modified By:
*/
public static String generateJWT(String userId, String userName, String ...identities) {
//簽名算法,選擇SHA-256
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//獲取當前系統(tǒng)時間
long nowTimeMillis = System.currentTimeMillis();
Date now = new Date(nowTimeMillis);
//將BASE64SECRET常量字符串使用base64解碼成字節(jié)數(shù)組
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET);
//使用HmacSHA256簽名算法生成一個HS256的簽名秘鑰Key
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//添加構(gòu)成JWT的參數(shù)
Map<String, Object> headMap = new HashMap<>();
/*
Header
{
"alg": "HS256",
"typ": "JWT"
}
*/
headMap.put("alg", SignatureAlgorithm.HS256.getValue());
headMap.put("typ", "JWT");
JwtBuilder builder = Jwts.builder().setHeader(headMap)
/*
Payload
{
"userId": "1234567890",
"userName": "John Doe",
}
*/
//加密后的客戶編號
.claim("userId", AESSecretUtil.encryptToStr(userId, SecretConstant.DATAKEY))
//客戶名稱
.claim("userName", userName)
//客戶端瀏覽器信息
.claim("userAgent", identities[0])
//Signature
.signWith(signatureAlgorithm, signingKey);
//添加Token過期時間
if (SecretConstant.EXPIRESSECOND >= 0) {
long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate).setNotBefore(now);
}
return builder.compact();
}
/**
* @Author: Helon
* @Description: 解析JWT
* 返回Claims對象
* @param jsonWebToken - JWT
* @Data: 2018/7/28 19:25
* @Modified By:
*/
public static Claims parseJWT(String jsonWebToken) {
Claims claims = null;
try {
if (StringUtils.isNotBlank(jsonWebToken)) {
//解析jwt
claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET))
.parseClaimsJws(jsonWebToken).getBody();
}else {
logger.warn("[JWTHelper]-json web token 為空");
}
} catch (Exception e) {
logger.error("[JWTHelper]-JWT解析異常:可能因為token已經(jīng)超時或非法token");
}
return claims;
}
/**
* @Author: Helon
* @Description: 校驗JWT是否有效
* 返回json字符串的demo:
* {"freshToken":"A.B.C","userName":"Judy","userId":"123", "userAgent":"xxxx"}
* freshToken-刷新后的jwt
* userName-客戶名稱
* userId-客戶編號
* userAgent-客戶端瀏覽器信息
* @param jsonWebToken - JWT
* @Data: 2018/7/24 15:28
* @Modified By:
*/
public static String validateLogin(String jsonWebToken) {
Map<String, Object> retMap = null;
Claims claims = parseJWT(jsonWebToken);
if (claims != null) {
//解密客戶編號
String decryptUserId = AESSecretUtil.decryptToStr((String)claims.get("userId"), SecretConstant.DATAKEY);
retMap = new HashMap<>();
//加密后的客戶編號
retMap.put("userId", decryptUserId);
//客戶名稱
retMap.put("userName", claims.get("userName"));
//客戶端瀏覽器信息
retMap.put("userAgent", claims.get("userAgent"));
//刷新JWT
retMap.put("freshToken", generateJWT(decryptUserId, (String)claims.get("userName"), (String)claims.get("userAgent"), (String)claims.get("domainName")));
}else {
logger.warn("[JWTHelper]-JWT解析出claims為空");
}
return retMap!=null?JSONObject.toJSONString(retMap):null;
}
public static void main(String[] args) {
String jsonWebKey = generateJWT("123", "Judy",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");
System.out.println(jsonWebKey);
Claims claims = parseJWT(jsonWebKey);
System.out.println(claims);
System.out.println(validateLogin(jsonWebKey));
}
AES加密工具類:
/**
* @Author: Helon
* @Description: AES加密工具類
* @Data: Created in 2018/7/28 18:38
* @Modified By:
*/
public class AESSecretUtil {
/**秘鑰的大小*/
private static final int KEYSIZE = 128;
/**
* @Author: Helon
* @Description: AES加密
* @param data - 待加密內(nèi)容
* @param key - 加密秘鑰
* @Data: 2018/7/28 18:42
* @Modified By:
*/
public static byte[] encrypt(String data, String key) {
if(StringUtils.isNotBlank(data)){
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
//選擇一種固定算法,為了避免不同java實現(xiàn)的不同算法,生成不同的密鑰,而導(dǎo)致解密失敗
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(key.getBytes());
keyGenerator.init(KEYSIZE, random);
SecretKey secretKey = keyGenerator.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 創(chuàng)建密碼器
byte[] byteContent = data.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);// 初始化
byte[] result = cipher.doFinal(byteContent);
return result; // 加密
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* @Author: Helon
* @Description: AES加密,返回String
* @param data - 待加密內(nèi)容
* @param key - 加密秘鑰
* @Data: 2018/7/28 18:59
* @Modified By:
*/
public static String encryptToStr(String data, String key){
return StringUtils.isNotBlank(data)?parseByte2HexStr(encrypt(data, key)):null;
}
/**
* @Author: Helon
* @Description: AES解密
* @param data - 待解密字節(jié)數(shù)組
* @param key - 秘鑰
* @Data: 2018/7/28 19:01
* @Modified By:
*/
public static byte[] decrypt(byte[] data, String key) {
if (ArrayUtils.isNotEmpty(data)) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
//選擇一種固定算法,為了避免不同java實現(xiàn)的不同算法,生成不同的密鑰,而導(dǎo)致解密失敗
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(key.getBytes());
keyGenerator.init(KEYSIZE, random);
SecretKey secretKey = keyGenerator.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 創(chuàng)建密碼器
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);// 初始化
byte[] result = cipher.doFinal(data);
return result; // 加密
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* @Author: Helon
* @Description: AES解密,返回String
* @param enCryptdata - 待解密字節(jié)數(shù)組
* @param key - 秘鑰
* @Data: 2018/7/28 19:01
* @Modified By:
*/
public static String decryptToStr(String enCryptdata, String key) {
return StringUtils.isNotBlank(enCryptdata)?new String(decrypt(parseHexStr2Byte(enCryptdata), key)):null;
}
/**
* @Author: Helon
* @Description: 將二進制轉(zhuǎn)換成16進制
* @param buf - 二進制數(shù)組
* @Data: 2018/7/28 19:12
* @Modified By:
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* @Author: Helon
* @Description: 將16進制轉(zhuǎn)換為二進制
* @param hexStr - 16進制字符串
* @Data: 2018/7/28 19:13
* @Modified By:
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length()/2];
for (int i = 0;i< hexStr.length()/2; i++) {
int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
public static void main(String[] args) {
String ss = encryptToStr("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMiLCJ1c2VyTmFtZSI6Ikp1ZHkiLCJleHAiOjE1MzI3Nzk2MjIsIm5iZiI6MTUzMjc3NzgyMn0.sIw_leDZwG0pJ8ty85Iecd_VXjObYutILNEwPUyeVSo", SecretConstant.DATAKEY);
System.out.println(ss);
System.out.println(decryptToStr(ss, SecretConstant.DATAKEY));
}
所使用的常量類:
/**
* @Author: Helon
* @Description: JWT使用常量值
* @Data: Created in 2018/7/27 14:37
* @Modified By:
*/
public class SecretConstant {
//簽名秘鑰 自定義
public static final String BASE64SECRET = "***********";
//超時毫秒數(shù)(默認30分鐘)
public static final int EXPIRESSECOND = 1800000;
//用于JWT加密的密匙 自定義
public static final String DATAKEY = "************";
}
客戶端pom依賴:
<!--jwt工具類-->
<dependency>
<groupId>com.chtwm.component</groupId>
<artifactId>jwt-helper</artifactId>
<version>xxx</version>
</dependency>
客戶端攔截器:
/**
* @Author: Helon
* @Description: 校驗是否登錄攔截器
* @Data: Created in 2018/7/30 14:30
* @Modified By:
*/
@Slf4j
public class ValidateLoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//首先從請求頭中獲取jwt串,與頁面約定好存放jwt值的請求頭屬性名為User-Token
String jwt = httpServletRequest.getHeader("User-Token");
log.info("[登錄校驗攔截器]-從header中獲取的jwt為:{}", jwt);
//判斷jwt是否有效
if(StringUtils.isNotBlank(jwt)){
//校驗jwt是否有效,有效則返回json信息,無效則返回空
String retJson = JwtHelper.validateLogin(jwt);
log.info("[登錄校驗攔截器]-校驗JWT有效性返回結(jié)果:{}", retJson);
//retJSON為空則說明jwt超時或非法
if(StringUtils.isNotBlank(retJson)){
JSONObject jsonObject = JSONObject.parseObject(retJson);
//校驗客戶端信息
String userAgent = httpServletRequest.getHeader("User-Agent");
if (userAgent.equals(jsonObject.getString("userAgent"))) {
//獲取刷新后的jwt值,設(shè)置到響應(yīng)頭中
httpServletResponse.setHeader("User-Token", jsonObject.getString("freshToken"));
//將客戶編號設(shè)置到session中
httpServletRequest.getSession().setAttribute(GlobalConstant.SESSION_CUSTOMER_NO_KEY, jsonObject.getString("userId"));
return true;
}else{
log.warn("[登錄校驗攔截器]-客戶端瀏覽器信息與JWT中存的瀏覽器信息不一致,重新登錄。當前瀏覽器信息:{}", userAgent);
}
}else {
log.warn("[登錄校驗攔截器]-JWT非法或已超時,重新登錄");
}
}
//輸出響應(yīng)流
JSONObject jsonObject = new JSONObject();
jsonObject.put("hmac", "");
jsonObject.put("status", "");
jsonObject.put("code", "4007");
jsonObject.put("msg", "未登錄");
jsonObject.put("data", "");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
httpServletResponse.getOutputStream().write(jsonObject.toJSONString().getBytes("UTF-8"));
return false;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
客戶端攔截器在XML文件中配置:
<!--攔截器配置-->
<mvc:interceptors>
<mvc:interceptor>
<!--需攔截url配置-->
<mvc:exclude-mapping path="/api/aa/bb/**" />
<mvc:exclude-mapping path="/api/aa/cc/test" />
<bean id="validateLoginInterceptor" class="com.xxx.ValidateLoginInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
到此,后臺服務(wù)的配置已經(jīng)完成,下一步就需要前端頁面將JWT令牌從response響應(yīng)頭中取出,然后存入localstorage或cookie中。但是遇到跨域場景,處理起來就會比較復(fù)雜,因為一旦在瀏覽器中跨域?qū)@取不到localstorage中的JWT令牌。例如www.a.com域下的JWT,在www.b.com域下是獲取不到的,所以我選擇了一種頁面跨域的方式進行處理,使用iframe+H5的postMessage(參考博文),具體我使用代碼分享的方式來分析。
前端頁面js代碼(服務(wù)端):
/**CURD本地存儲信息 start**/
(function(doc,win){
var fn=function(){};
fn.prototype={
/*本地數(shù)據(jù)存儲 t:cookie有效時間,單位s; domain:cookie存儲所屬的domain域*/
setLocalCookie: function (k, v, t,domain) {
//如果當前瀏覽器不支持localStorage將存儲在cookie中
typeof window.localStorage !== "undefined" ? localStorage.setItem(k, v) :
(function () {
t = t || 365 * 12 * 60 * 60;
domain=domain?domain:".jwtserver.com";
document.cookie = k + "=" + v + ";max-age=" + t+";domain="+domain+";path=/";
})()
},
/*獲取本地存儲數(shù)據(jù)*/
getLocalCookie: function (k) {
k = k || "localDataTemp";
return typeof window.localStorage !== "undefined" ? localStorage.getItem(k) :
(function () {
var all = document.cookie.split(";");
var cookieData = {};
for (var i = 0, l = all.length; i < l; i++) {
var p = all[i].indexOf("=");
var dataName = all[i].substring(0, p).replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");
cookieData[dataName] = all[i].substring(p + 1);
}
return cookieData[k]
})();
},
/*刪除本地存儲數(shù)據(jù)*/
clearLocalData: function (k) {
k = k || "localDataTemp";
typeof window.localStorage !== "undefined" ? localStorage.removeItem(k) :
(function () {
document.cookie = k + "=temp" + ";max-age=0";
})()
},
init:function(){
this.bindEvent();
},
//事件綁定
bindEvent:function(){
var _this=this;
win.addEventListener("message",function(evt){
if(win.parent!=evt.source){return}
var options=JSON.parse(evt.data);
if(options.type=="GET"){
var data=tools.getLocalCookie(options.key);
win.parent.postMessage(data, "*");
}
options.type=="SET"&&_this.setLocalCookie(options.key,options.value);
options.type=="REM"&&_this.clearLocalData(options.key);
},false)
}
};
var tools=new fn();
tools.init();
})(document,window);
/**CURD本地存儲信息 end**/
前端頁面js代碼(客戶端):
//頁面初始化向iframe域名發(fā)送消息
window.onload = function() {
console.log('get key value......................')
window.frames[0].postMessage(JSON.stringify({type:"GET",key:"User-Token"}),'*');
}
//監(jiān)聽message信息,接收從iframe域下獲取到的token信息,然后存儲到localstorage或cookie中
window.addEventListener('message', function(e) {
console.log('listen.....');
var data = e.data;
console.log(data);
if(data != null){
localStorage.setItem("User-Token", data);
}
}, false);
總結(jié):
優(yōu)點:在非跨域環(huán)境下使用JWT機制是一個非常不錯的選擇,實現(xiàn)方式簡單,操作方便,能夠快速實現(xiàn)。由于服務(wù)端不存儲用戶狀態(tài)信息,因此大用戶量,對后臺服務(wù)也不會造成壓力;
缺點:跨域?qū)崿F(xiàn)相對比較麻煩,安全性也有待探討。因為JWT令牌返回到頁面中,可以使用js獲取到,如果遇到XSS攻擊令牌可能會被盜取,在JWT還沒超時的情況下,就會被獲取到敏感數(shù)據(jù)信息。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
解決wx.onMenuShareTimeline出現(xiàn)的問題
本文主要介紹解決wx.onMenuShareTimeline出現(xiàn)的問題,這里提供了示例代碼作為參考,有需要的小伙伴可以參考下2016-08-08

