使用JWT進行跨域身份驗證
使用JWT進行跨域身份驗證
1、傳統使用者身份驗證
Internet服務無法與使用者身份驗證分開。一般過程如下:
- 使用者向伺服器傳送使用者名稱和密碼。
- 驗證伺服器後,相關資料(如使用者角色,登入時間等)將儲存在當前會話中。
- 伺服器向用戶返回session_id,session資訊都會寫入到使用者的Cookie。
- 使用者的每個後續請求都將通過在Cookie中取出session_id傳給伺服器。
- 伺服器收到session_id並對比之前儲存的資料,確認使用者的身份。
這種模式最大的問題是,沒有分散式架構,無法支援橫向擴充套件。
2、解決方案
- session廣播
- 將透明令牌存入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"));
}*/
}