前言:當(dāng)今前后端分離時代,基于Token的會話保持機制比傳統(tǒng)的Session/Cookie機制更加方便,下面我會介紹SpringBoot快速集成JWT庫java-jwt以完成用戶登錄認(rèn)證。
一械蹋、JWT 簡介
1.1哗戈、 JWT的概念
JWT 是 JSON Web Token 的縮寫,是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于 JSON 的開放標(biāo)準(zhǔn)((RFC 7519)。定義了一種簡潔的瞎嬉,自包含的方法用于通信雙方之間以 JSON 對象的形式安全的傳遞信息氧枣。因為數(shù)字簽名的存在,這些信息是可信的茬贵,JWT 可以使用 HMAC 算法或者是 RSA 的公私秘鑰對進(jìn)行簽名。
1.2螟左、JWT請求流程
JWT 請求流程
用戶使用賬號和密碼發(fā)起 POST 請求;
服務(wù)器使用私鑰創(chuàng)建一個 JWT喘先;
服務(wù)器返回這個 JWT 給瀏覽器;
瀏覽器將該 JWT 串在請求頭中像服務(wù)器發(fā)送請求暇番;
服務(wù)器驗證該 JWT壁酬;
返回響應(yīng)的資源給瀏覽器岳服。
1.3斜纪、JWT 的主要應(yīng)用場景
身份認(rèn)證在這種場景下,一旦用戶完成了登錄,在接下來的每個請求中包含 JWT趾断,可以用來驗證用戶身份以及對路由,服務(wù)和資源的訪問權(quán)限進(jìn)行驗證。由于它的開銷非常小,可以輕松的在不同域名的系統(tǒng)中傳遞疚顷,所有目前在單點登錄(SSO)中比較廣泛的使用了該技術(shù)释液。 信息交換在通信的雙方之間使用 JWT 對數(shù)據(jù)進(jìn)行編碼是一種非常安全的方式,由于它的信息是經(jīng)過簽名的,可以確保發(fā)送者發(fā)送的信息是沒有經(jīng)過偽造的。
1.4耍鬓、JWT 數(shù)據(jù)結(jié)構(gòu)
JWT 是由三段信息構(gòu)成的绅这,將這三段信息文本用 . 連接一起就構(gòu)成了 JWT 字符串。JWT 的三個部分依次為頭部:Header寇窑,負(fù)載:Payload 和簽名:Signature疗认。
①Header:
Header 部分是一個 JSON 對象,描述 JWT 的元數(shù)據(jù)缎浇,通常是下面的樣子。
{
? "alg": "HS256",
? "typ": "JWT"
}
上面代碼中指厌,alg 屬性表示簽名的算法(algorithm)踩验,默認(rèn)是 HMAC SHA256(寫成 HS256)袭异;typ 屬性表示這個令牌(token)的類型(type),JWT 令牌統(tǒng)一寫為 JWT。
最后帝火,將上面的 JSON 對象使用 Base64URL 算法轉(zhuǎn)成字符串嗓违。
②Payload:
Payload 部分也是一個 JSON 對象,用來存放實際需要傳遞的有效信息。有效信息包含三個部分:
標(biāo)準(zhǔn)中注冊的聲明
公共的聲明
私有的聲明
標(biāo)準(zhǔn)中注冊的聲明 (建議但不強制使用) :
iss (issuer):簽發(fā)人
exp (expiration time):過期時間睬辐,必須要大于簽發(fā)時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發(fā)時間
jti (JWT ID):編號丰刊,JWT 的唯一身份標(biāo)識洪橘,主要用來作為一次性 token逗概,從而回避重放攻擊枚钓。
公共的聲明 :公共的聲明可以添加任何的信息星掰,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息。但不建議添加敏感信息蜀踏,因為該部分在客戶端可解密殖熟。
私有的聲明 :私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為 base64 是對稱解碼的昭卓,意味著該部分信息可以歸類為明文信息倒淫。這個 JSON 對象也要使用 Base64URL 算法轉(zhuǎn)成字符串。
③Signature:
Signature 部分是對前兩部分的簽名,防止數(shù)據(jù)篡改财剖。
首先瞳氓,需要指定一個密鑰(secret)。這個密鑰只有服務(wù)器才知道,不能泄露給用戶违霞。然后涧郊,使用 Header 里面指定的簽名算法(默認(rèn)是 HMAC SHA256),按照下面的公式產(chǎn)生簽名。
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 算法疾渣。
1.5吊圾、JWT 的使用方式
客戶端收到服務(wù)器返回的 JWT 之后需要在本地做保存。此后趁尼,客戶端每次與服務(wù)器通信啃憎,都要帶上這個 JWT。一般的的做法是放在 HTTP 請求的頭信息 Authorization 字段里面辉阶。
Authorization: Bearer <token>
這樣每個請求中垃僚,服務(wù)端就可以在請求頭中拿到 JWT? 進(jìn)行解析與認(rèn)證碍岔。
1.6亮元、JWT 的特性
JWT 默認(rèn)是不加密盗温,但也是可以加密的。生成原始 Token 以后丘逸,可以用密鑰再加密一次仲锄。
JWT 不加密的情況下涛舍,不能將秘密數(shù)據(jù)寫入 JWT肛搬。
JWT 不僅可以用于認(rèn)證鬼癣,也可以用于交換信息章郁。有效使用 JWT,可以降低服務(wù)器查詢數(shù)據(jù)庫的次數(shù)。
JWT 的最大缺點是象缀,由于服務(wù)器不保存 session 狀態(tài),因此無法在使用過程中廢止某個 token颓遏,或者更改 token 的權(quán)限。也就是說窒百,一旦 JWT 簽發(fā)了美旧,在到期之前就會始終有效,除非服務(wù)器部署額外的邏輯论咏。
JWT 本身包含了認(rèn)證信息,一旦泄露,任何人都可以獲得該令牌的所有權(quán)限。為了減少盜用,JWT 的有效期應(yīng)該設(shè)置得比較短。對于一些比較重要的權(quán)限,使用時應(yīng)該再次對用戶進(jìn)行認(rèn)證颈墅。
為了減少盜用毒坛,JWT 不應(yīng)該使用 HTTP 協(xié)議明碼傳輸腿箩,要使用 HTTPS 協(xié)議傳輸。
二暇韧、SpringBoot整合JWT
新建一個spring boot項目spring-boot-jwt涂乌,按照下面步驟操作巴席。
2.1赵刑、pom.xml引入jar包
<!-- 引入jwt-->
<dependency>
? ? <groupId>com.auth0</groupId>
? ? <artifactId>java-jwt</artifactId>
? ? <version>3.8.2</version>
</dependency>
順便貼一下下面要用到的User類:
package com.hs.demo.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel
public class User
{
? ? //實體類中铐懊,Integer類型的屬性加@ApiModelProperty時茅茂,必須要給example參數(shù)賦值,且值必須為數(shù)字類型逗噩。
? ? @ApiModelProperty(value = "用戶id",example = "1")
? ? private Integer id;
? ? @ApiModelProperty(value = "用戶名")
? ? private String userName;
? ? @ApiModelProperty(value = "用戶密碼")
? ? private String password;
? ? //getter/setter用@Data注解自動生成
}
2.2片迅、新建Jwt工具類
Jwt工具類進(jìn)行token的生成和認(rèn)證,工具類代碼如下:
package com.hs.demo.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.hs.demo.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @description: Jwt工具類扼菠,生成JWT和認(rèn)證
* @author: heshi
*/
public class JwtUtil {
? ? private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
? ? /**
? ? * 密鑰
? ? */
? ? private static final String SECRET = "my_secret";
? ? /**
? ? * 過期時間
? ? **/
? ? private static final long EXPIRATION = 1800L;//單位為秒
? ? /**
? ? * 生成用戶token,設(shè)置token超時時間
? ? */
? ? public static String createToken(User user) {
? ? ? ? //過期時間
? ? ? ? Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
? ? ? ? Map<String, Object> map = new HashMap<>();
? ? ? ? map.put("alg", "HS256");
? ? ? ? map.put("typ", "JWT");
? ? ? ? String token = JWT.create()
? ? ? ? ? ? ? ? .withHeader(map)// 添加頭部
? ? ? ? ? ? ? ? //可以將基本信息放到claims中
? ? ? ? ? ? ? ? .withClaim("id", user.getId())//userId
? ? ? ? ? ? ? ? .withClaim("userName", user.getUserName())//userName
? ? ? ? ? ? ? ? .withClaim("password", user.getPassword())//password
? ? ? ? ? ? ? ? .withExpiresAt(expireDate) //超時設(shè)置,設(shè)置過期的日期
? ? ? ? ? ? ? ? .withIssuedAt(new Date()) //簽發(fā)時間
? ? ? ? ? ? ? ? .sign(Algorithm.HMAC256(SECRET)); //SECRET加密
? ? ? ? return token;
? ? }
? ? /**
? ? * 校驗token并解析token
? ? */
? ? public static Map<String, Claim> verifyToken(String token) {
? ? ? ? DecodedJWT jwt = null;
? ? ? ? try {
? ? ? ? ? ? JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
? ? ? ? ? ? jwt = verifier.verify(token);
? ? ? ? ? ? //decodedJWT.getClaim("屬性").asString()? 獲取負(fù)載中的屬性值
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? logger.error(e.getMessage());
? ? ? ? ? ? logger.error("token解碼異常");
? ? ? ? ? ? //解碼異常則拋出異常
? ? ? ? ? ? return null;
? ? ? ? }
? ? ? ? return jwt.getClaims();
? ? }
}
2.3泽篮、添加JWT過濾器
JWT過濾器中進(jìn)行token的校驗和判斷,token不合法直接返回,合法則解密數(shù)據(jù)并把數(shù)據(jù)放到request中供后續(xù)使用专筷。
為了使過濾器生效味咳,需要在啟動類添加注解@ServletComponentScan(basePackages = "com.hs.demo.jwt")掂铐。
JWT過濾器代碼如下:
package com.hs.demo.jwt;
import com.auth0.jwt.interfaces.Claim;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* JWT過濾器辱姨,攔截 /secure的請求
*/
@Slf4j
@WebFilter(filterName = "JwtFilter", urlPatterns = "/secure/*")
public class JwtFilter implements Filter
{
? ? @Override
? ? public void init(FilterConfig filterConfig) throws ServletException {
? ? }
? ? @Override
? ? public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
? ? ? ? final HttpServletRequest request = (HttpServletRequest) req;
? ? ? ? final HttpServletResponse response = (HttpServletResponse) res;
? ? ? ? response.setCharacterEncoding("UTF-8");
? ? ? ? //獲取 header里的token
? ? ? ? final String token = request.getHeader("authorization");
? ? ? ? if ("OPTIONS".equals(request.getMethod())) {
? ? ? ? ? ? response.setStatus(HttpServletResponse.SC_OK);
? ? ? ? ? ? chain.doFilter(request, response);
? ? ? ? }
? ? ? ? // Except OPTIONS, other request should be checked by JWT
? ? ? ? else {
? ? ? ? ? ? if (token == null) {
? ? ? ? ? ? ? ? response.getWriter().write("沒有token医瘫!");
? ? ? ? ? ? ? ? return;
? ? ? ? ? ? }
? ? ? ? ? ? Map<String, Claim> userData = JwtUtil.verifyToken(token);
? ? ? ? ? ? if (userData == null) {
? ? ? ? ? ? ? ? response.getWriter().write("token不合法稼锅!");
? ? ? ? ? ? ? ? return;
? ? ? ? ? ? }
? ? ? ? ? ? Integer id = userData.get("id").asInt();
? ? ? ? ? ? String userName = userData.get("userName").asString();
? ? ? ? ? ? String password= userData.get("password").asString();
? ? ? ? ? ? //攔截器 拿到用戶信息锥债,放到request中
? ? ? ? ? ? request.setAttribute("id", id);
? ? ? ? ? ? request.setAttribute("userName", userName);
? ? ? ? ? ? request.setAttribute("password", password);
? ? ? ? ? ? chain.doFilter(req, res);
? ? ? ? }
? ? }
? ? @Override
? ? public void destroy() {
? ? }
}
2.4允趟、添加LoginController
LoginController進(jìn)行登錄操作分唾,登錄成功后生產(chǎn)token并返回折砸。
LoginController代碼如下:
package com.hs.demo.jwt;
import com.hs.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 登錄Controller
*/
@Slf4j
@RestController
public class LoginController
{
? ? static Map<Integer, User> userMap = new HashMap<>();
? ? static {
? ? ? ? //模擬數(shù)據(jù)庫
? ? ? ? User user1 = new User(1,"張三","123456");
? ? ? ? userMap.put(1, user1);
? ? ? ? User user2 = new User(2,"李四","123123");
? ? ? ? userMap.put(2, user2);
? ? }
? ? /**
? ? * 模擬用戶 登錄
? ? */
? ? @RequestMapping("/login")
? ? public String login(User user)
? ? {
? ? ? ? for (User dbUser : userMap.values()) {
? ? ? ? ? ? if (dbUser.getUserName().equals(user.getUserName()) && dbUser.getPassword().equals(user.getPassword())) {
? ? ? ? ? ? ? ? log.info("登錄成功盗扇!生成token翼闹!");
? ? ? ? ? ? ? ? String token = JwtUtil.createToken(dbUser);
? ? ? ? ? ? ? ? return token;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return "";
? ? }
}
2.5坚弱、添加SecureController
SecureController中的請求會被JWT過濾器攔截,合法后才能訪問。
SecureController代碼如下:
package com.hs.demo.jwt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 需要登錄后攜帶JWT才能訪問
*/
@Slf4j
@RestController
public class SecureController
{
? ? /**
? ? * 查詢 用戶信息鹅很,登錄后攜帶JWT才能訪問
? ? */
? ? @RequestMapping("/secure/getUserInfo")
? ? public String login(HttpServletRequest request) {
? ? ? ? Integer id = (Integer) request.getAttribute("id");
? ? ? ? String userName = request.getAttribute("userName").toString();
? ? ? ? String password= request.getAttribute("password").toString();
? ? ? ? return "當(dāng)前用戶信息id=" + id + ",userName=" + userName+ ",password=" + password;
? ? }
}
三污茵、接口測試
測試分兩步,首先訪問登錄接口盗飒,登錄成功后獲取token,然后拿著token在訪問查詢用戶信息接口痕囱。
3.1弦悉、訪問登錄接口
打開PostMan崇败,訪問http://localhost:8080/login?userName=zhangsan&password=123456,登錄成功后接口返回token,請求成功截圖如下:
3.2、訪問用戶信息接口
打開PostMan湖饱,訪問http://localhost:8080/secure/getUserInfo仅仆,header里需要攜帶token请契,請求成功截圖如下:
四、Token 認(rèn)證的優(yōu)勢
相比于 Session 認(rèn)證的方式來說,使用 token 進(jìn)行身份認(rèn)證主要有下面三個優(yōu)勢:
4.1奢啥、無狀態(tài)
JWT實現(xiàn)的Token 自身包含了身份驗證所需要的所有信息赌结,使得我們的服務(wù)器不需要存儲 Session 信息量承,這顯然增加了系統(tǒng)的可用性和伸縮性忧风,大大減輕了服務(wù)端的壓力弟蚀。但是,也正是由于 token 的無狀態(tài),也導(dǎo)致了它最大的缺點:當(dāng)后端在token 有效期內(nèi)廢棄一個 token 或者更改它的權(quán)限的話税灌,不會立即生效粘秆,一般需要等到有效期過后才可以陋气。另外淳附,當(dāng)用戶 Logout 的話洽糟,token 也還有效。除非,我們在后端增加額外的處理邏輯勺鸦。
4.2怀跛、有效避免了CSRF 攻擊
CSRF(Cross Site Request Forgery) 一般被翻譯為 跨站請求偽造漓拾,屬于網(wǎng)絡(luò)攻擊領(lǐng)域范圍低千。相比于 SQL 腳本注入瘫拣、XSS等等安全攻擊方式黔姜,CSRF 的知名度并沒有它們高。但是,它的確是每個系統(tǒng)都要考慮的安全隱患,就連技術(shù)帝國 Google 的 Gmail 在早些年也被曝出過存在? CSRF 漏洞,這給 Gmail 的用戶造成了很大的損失。
那么究竟什么是? 跨站請求偽造 呢漠烧?說簡單用你的身份去發(fā)送一些對你不友好的請求。舉個簡單的例子:
小壯登錄了某網(wǎng)上銀行,他來到了網(wǎng)上銀行的帖子區(qū),看到一個帖子下面有一個鏈接寫著“科學(xué)理財乍丈,年盈利率過萬”,小壯好奇的點開了這個鏈接采驻,結(jié)果發(fā)現(xiàn)自己的賬戶少了10000元痘系。這是這么回事呢悉默?原來黑客在鏈接中藏了一個請求,這個請求直接利用小壯的身份給銀行發(fā)送了一個轉(zhuǎn)賬請求,也就是通過你的 Cookie 向銀行發(fā)出請求型豁。
<a src=http://www.mybank.com/Transfer?bankId=11&money=10000>科學(xué)理財驼侠,年盈利率過萬</>
導(dǎo)致這個問題很大的原因就是:Session 認(rèn)證中 Cookie 中的 session_id 是由瀏覽器發(fā)送到服務(wù)端的,借助這個特性,攻擊者就可以通過讓用戶誤點攻擊鏈接,達(dá)到攻擊效果周拐。
那為什么 token 不會存在這種問題呢滩报?
我是這樣理解的:一般情況下我們使用 JWT 的話可训,在我們登錄成功獲得 token 之后蚜厉,一般會選擇存放在? local storage 中胞四。然后我們在前端通過某些方式會給每個發(fā)到后端的請求加上這個 token,這樣就不會出現(xiàn) CSRF 漏洞的問題约巷。因為氓癌,即使有個你點擊了非法鏈接發(fā)送了請求到服務(wù)端践剂,這個非法請求是不會攜帶 token 的军洼,所以這個請求將是非法的甘桑。
但是這樣會存在? XSS 攻擊中被盜的風(fēng)險愧驱,為了避免 XSS 攻擊,你可以選擇將 token 存儲在標(biāo)記為httpOnly? 的cookie 中。但是揍鸟,這樣又導(dǎo)致了你必須自己提供CSRF保護(hù)。
具體采用上面哪兩種方式存儲 token 呢矛纹,大部分情況下存放在? local storage 下都是最好的選擇,某些情況下可能需要存放在標(biāo)記為httpOnly 的cookie 中會更好。
4.3腻贰、適合移動端應(yīng)用
使用 Session 進(jìn)行身份認(rèn)證的話,需要保存一份信息在服務(wù)器端询微,而且這種方式會依賴到 Cookie(需要 Cookie 保存 SessionId)叁温,所以不適合移動端。
但是核畴,使用 token 進(jìn)行身份認(rèn)證就不會存在這種問題膝但,因為只要 token 可以被客戶端存儲就能夠使用,而且 token 還可以跨語言使用谤草。
4.4跟束、單點登錄友好
使用 Session 進(jìn)行身份認(rèn)證的話,實現(xiàn)單點登錄丑孩,需要我們把用戶的 Session 信息保存在一臺電腦上冀宴,并且還會遇到常見的 Cookie 跨域的問題。但是温学,使用 token 進(jìn)行認(rèn)證的話略贮, token 被保存在客戶端,不會存在這些問題。
五逃延、Token 認(rèn)證常見問題以及解決辦法
5.1览妖、注銷登錄等場景下 token 還有效
與之類似的具體相關(guān)場景有:
退出登錄;
修改密碼;
服務(wù)端修改了某個用戶具有的權(quán)限或者角色;
用戶的帳戶被刪除/暫停揽祥。
用戶由管理員注銷讽膏;
這個問題不存在于 Session? 認(rèn)證方式中,因為在? Session? 認(rèn)證方式中拄丰,遇到這種情況的話服務(wù)端刪除對應(yīng)的 Session 記錄即可府树。但是,使用 token 認(rèn)證的方式就不好解決了愈案。我們也說過了挺尾,token 一旦派發(fā)出去,如果后端不增加其他邏輯的話站绪,它在失效之前都是有效的遭铺。那么,我們?nèi)绾谓鉀Q這個問題呢恢准?查閱了很多資料魂挂,總結(jié)了下面幾種方案:
將 token 存入內(nèi)存數(shù)據(jù)庫:將 token 存入 DB 中,redis 內(nèi)存數(shù)據(jù)庫在這里是是不錯的選擇馁筐。如果需要讓某個 token 失效就直接從 redis 中刪除這個 token 即可涂召。但是,這樣會導(dǎo)致每次使用 token 發(fā)送請求都要先從 DB 中查詢 token 是否存在的步驟敏沉,而且違背了 JWT 的無狀態(tài)原則果正。
黑名單機制:和上面的方式類似,使用內(nèi)存數(shù)據(jù)庫比如 redis 維護(hù)一個黑名單盟迟,如果想讓某個 token 失效的話就直接將這個 token 加入到 黑名單 即可秋泳。然后,每次使用 token 進(jìn)行請求的話都會先判斷這個 token 是否存在于黑名單中攒菠。
修改密鑰 (Secret) : 我們?yōu)槊總€用戶都創(chuàng)建一個專屬密鑰迫皱,如果我們想讓某個 token 失效,我們直接修改對應(yīng)用戶的密鑰即可辖众。但是,這樣相比于前兩種引入內(nèi)存數(shù)據(jù)庫帶來了危害更大凹炸,比如:1??如果服務(wù)是分布式的戏阅,則每次發(fā)出新的 token 時都必須在多臺機器同步密鑰。為此啤它,你需要將必須將機密存儲在數(shù)據(jù)庫或其他外部服務(wù)中饲握,這樣和 Session 認(rèn)證就沒太大區(qū)別了救欧。2??? 如果用戶同時在兩個瀏覽器打開系統(tǒng)笆怠,或者在手機端也打開了系統(tǒng),如果它從一個地方將賬號退出,那么其他地方都要重新進(jìn)行登錄迂卢,這是不可取的。
保持令牌的有效期限短并經(jīng)常輪換 :很簡單的一種方式。但是拣度,會導(dǎo)致用戶登錄狀態(tài)不會被持久記錄幕随,而且需要用戶經(jīng)常登錄。
對于修改密碼后 token 還有效問題的解決還是比較容易的睦霎,說一種我覺得比較好的方式:使用用戶的密碼的哈希值對 token 進(jìn)行簽名戴陡。因此诀浪,如果密碼更改雷猪,則任何先前的令牌將自動無法驗證月帝。
5.2、token 的續(xù)簽問題
token 有效期一般都建議設(shè)置的不太長,那么 token 過期后如何認(rèn)證刑然,如何實現(xiàn)動態(tài)刷新 token挡逼,避免用戶經(jīng)常需要重新登錄订框?
我們先來看看在 Session 認(rèn)證中一般的做法:假如 session 的有效期30分鐘穿扳,如果 30 分鐘內(nèi)用戶有訪問,就把 session 有效期被延長30分鐘国旷。
類似于 Session 認(rèn)證中的做法:這種方案滿足于大部分場景矛物。假設(shè)服務(wù)端給的 token 有效期設(shè)置為30分鐘,服務(wù)端每次進(jìn)行校驗時跪但,如果發(fā)現(xiàn) token 的有效期馬上快過期了履羞,服務(wù)端就重新生成 token 給客戶端÷啪茫客戶端每次請求都檢查新舊token忆首,如果不一致,則更新本地的token被环。這種做法的問題是僅僅在快過期的時候請求才會更新 token ,對客戶端不是很友好糙及。
每次請求都返回新 token :這種方案的的思路很簡單,但是筛欢,很明顯浸锨,開銷會比較大。
token 有效期設(shè)置到半夜 :這種方案是一種折衷的方案版姑,保證了大部分用戶白天可以正常登錄柱搜,適用于對安全性要求不高的系統(tǒng)。
用戶登錄返回兩個 token :第一個是 acessToken 剥险,它的過期時間 token 本身的過期時間比如半個小時聪蘸,另外一個是 refreshToken 它的過期時間更長一點比如為1天”碇疲客戶端登錄后健爬,將 accessToken和refreshToken 保存在本地,每次訪問將 accessToken 傳給服務(wù)端夫凸。服務(wù)端校驗 accessToken 的有效性浑劳,如果過期的話,就將 refreshToken 傳給服務(wù)端夭拌。如果有效魔熏,服務(wù)端就生成新的 accessToken 給客戶端衷咽。否則,客戶端就重新登錄即可蒜绽。該方案的不足是:1??需要客戶端來配合镶骗;2??用戶注銷的時候需要同時保證兩個? token 都無效;3??重新請求獲取 token? 的過程中會有短暫 token 不可用的情況(可以通過在客戶端設(shè)置定時器躲雅,當(dāng)accessToken 快過期的時候鼎姊,提前去通過 refreshToken 獲取新的accessToken)。
PHP體系:
通過Postman進(jìn)行post請求時傳遞X-XSRF-TOKEN相赁。Laravel預(yù)設(shè)要求post時需要HTTP header附帶XSRF token
PHP可通過JWT實現(xiàn)API接口認(rèn)證:
API接口JWT方式的Token認(rèn)證(上)相寇,服務(wù)器(Laravel)的實現(xiàn)
總結(jié)
JWT 最適合的場景是不需要服務(wù)端保存用戶狀態(tài)的場景,如果考慮到 token 注銷和 token 續(xù)簽的場景話钮科,沒有特別好的解決方案唤衫,大部分解決方案都給 token 加上了狀態(tài),這就有點類似 Session 認(rèn)證了绵脯。