1. 程式人生 > 實用技巧 >使用JWT進行跨域身份驗證

使用JWT進行跨域身份驗證

使用JWT進行跨域身份驗證

1、傳統使用者身份驗證

Internet服務無法與使用者身份驗證分開。一般過程如下:

  1. 使用者向伺服器傳送使用者名稱和密碼。
  2. 驗證伺服器後,相關資料(如使用者角色,登入時間等)將儲存在當前會話中。
  3. 伺服器向用戶返回session_id,session資訊都會寫入到使用者的Cookie。
  4. 使用者的每個後續請求都將通過在Cookie中取出session_id傳給伺服器。
  5. 伺服器收到session_id並對比之前儲存的資料,確認使用者的身份。

這種模式最大的問題是,沒有分散式架構,無法支援橫向擴充套件。

2、解決方案

  1. session廣播
  2. 將透明令牌存入cookie,將使用者身份資訊存入redis

另外一種靈活的解決方案:

使用自包含令牌,通過客戶端儲存資料,而伺服器不儲存會話資料。 JWT是這種解決方案的代表。

二、JWT令牌

1、訪問令牌的型別

2、JWT的組成

典型的,一個JWT看起來如下圖:

該物件為一個很長的字串,字元之間通過"."分隔符分為三個子串。

每一個子串表示了一個功能塊,總共有以下三個部分:JWT頭、有效載荷和簽名

JWT頭

JWT頭部分是一個描述JWT元資料的JSON物件,通常如下所示。

{
  "alg": "HS256",
  "typ": "JWT"
}

在上面的程式碼中,alg屬性表示簽名使用的演算法,預設為HMAC SHA256(寫為HS256);typ屬性表示令牌的型別,JWT令牌統一寫為JWT。最後,使用Base64 URL演算法將上述JSON物件轉換為字串儲存。

有效載荷

有效載荷部分,是JWT的主體內容部分,也是一個JSON物件,包含需要傳遞的資料。 JWT指定七個預設欄位供選擇。

iss:發行人
exp:到期時間
sub:主題
aud:使用者
nbf:在此之前不可用
iat:釋出時間
jti:JWT ID用於標識該JWT

除以上預設欄位外,我們還可以自定義私有欄位,如下例:

{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

請注意,預設情況下JWT是未加密的,任何人都可以解讀其內容,因此不要構建隱私資訊欄位,存放保密資訊,以防止資訊洩露。

JSON物件也使用Base64 URL演算法轉換為字串儲存。

簽名雜湊

簽名雜湊部分是對上面兩部分資料簽名,通過指定的演算法生成雜湊,以確保資料不會被篡改。

首先,需要指定一個密碼(secret)。該密碼僅僅為儲存在伺服器中,並且不能向用戶公開。然後,使用標頭中指定的簽名演算法(預設情況下為HMAC SHA256)根據以下公式生成簽名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)

在計算出簽名雜湊後,JWT頭,有效載荷和簽名雜湊的三個部分組合成一個字串,每個部分用"."分隔,就構成整個JWT物件。

Base64URL演算法

如前所述,JWT頭和有效載荷序列化的演算法都用到了Base64URL。該演算法和常見Base64演算法類似,稍有差別。

作為令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三個字元是"+","/"和"=",由於在URL中有特殊含義,因此Base64URL中對他們做了替換:"="去掉,"+"用"-"替換,"/"用"_"替換,這就是Base64URL演算法。

3、JWT的原則

JWT的原則是在伺服器身份驗證之後,將生成一個JSON物件並將其傳送回用戶,如下所示。

{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

之後,當用戶與伺服器通訊時,客戶在請求中發回JSON物件。伺服器僅依賴於這個JSON物件來標識使用者。為了防止使用者篡改資料,伺服器將在生成物件時添加簽名。

伺服器不儲存任何會話資料,即伺服器變為無狀態,使其更容易擴充套件。

4、JWT的用法

客戶端接收伺服器返回的JWT,將其儲存在Cookie或localStorage中。

此後,客戶端將在與伺服器互動中都會帶JWT。如果將它儲存在Cookie中,就可以自動傳送,但是不會跨域,因此一般是將它放入HTTP請求的Header Authorization欄位中。當跨域時,也可以將JWT被放置於POST請求的資料主體中。

5、JWT問題和趨勢

  • JWT不僅可用於認證,還可用於資訊交換。善用JWT有助於減少伺服器請求資料庫的次數。

  • 生產的token可以包含基本資訊,比如id、使用者暱稱、頭像等資訊,避免再次查庫

  • 儲存在客戶端,不佔用服務端的記憶體資源

  • JWT預設不加密,但可以加密。生成原始令牌後,可以再次對其進行加密。

  • 當JWT未加密時,一些私密資料無法通過JWT傳輸。

  • JWT的最大缺點是伺服器不儲存會話狀態,所以在使用期間不可能取消令牌或更改令牌的許可權。也就是說,一旦JWT簽發,在有效期內將會一直有效。

  • JWT本身包含認證資訊,token是經過base64編碼,所以可以解碼,因此token加密前的物件不應該包含敏感資訊,一旦資訊洩露,任何人都可以獲得令牌的所有許可權。為了減少盜用,JWT的有效期不宜設定太長。對於某些重要操作,使用者在使用時應該每次都進行進行身份驗證。

  • 為了減少盜用和竊取,JWT不建議使用HTTP協議來傳輸程式碼,而是使用加密的HTTPS協議進行傳輸。

三、整合JWT令牌

1、在online_edu_user模組中新增jwt工具依賴

在pom中新增

<dependencies>
    <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
</dependencies>

2、建立JWT工具類

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * @author
 */
public class JwtUtils {

    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    public static String getJwtToken(String id, String nickname){

        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("guli-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .claim("nickname", nickname)
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判斷token是否存在與有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判斷token是否存在與有效
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根據token獲取會員id
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
    
    /**
     * 根據物件生成jwt的token字串
     */
    public static String geneJsonWebToken(MemberCenter member) {

        if (member == null || StringUtils.isEmpty(member.getId())
                || StringUtils.isEmpty(member.getNickname())
                || StringUtils.isEmpty(member.getAvatar())) {
            return null;
        }
        String token = Jwts.builder().setSubject(SUBJECT)
                .claim("id", member.getId())
                .claim("nickname", member.getNickname())
                .claim("avatar", member.getAvatar())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .signWith(SignatureAlgorithm.HS256, APPSECRET).compact();
        return token;
    }

    /**
     * 校驗jwt token
     */
    public static Claims checkJWT(String token) {
        Claims claims = Jwts.parser().setSigningKey(APPSECRET).parseClaimsJws(token).getBody();
        return claims;
    }
    /*public static void main(String[] args) {
        MemberCenter memberCenter = new MemberCenter();
        memberCenter.setAvatar("xx");
        memberCenter.setNickname("zhangsan");
        memberCenter.setId("15");
        String token = JwtUtils.geneJsonWebToken(memberCenter);
        System.out.println();

        Claims claims = JwtUtils.checkJWT(token);
        System.out.println(claims.get("id"));
    }*/
}