在文章之前,我們先介紹幾個概念
OAuth2、JWT,Spring Security、Spring Security OAuth2
OAuth2:Open Authorization,是一種授權協議,是規範,不是技術實現。
JWT:JSON Web Token,是一種具體的Token實現框架。
Spring Security:前身是 Acegi Security ,能夠爲 Spring企業應用系統提供聲明式的安全訪問控制。該框架老古董了。
Spring Security OAuth2:Spring 對 OAuth2 開源實現(與Spring Cloud技術棧無縫集成)。
目前用的最多是JWT,因此本文也是圍繞JWT來實現。
1.什麼是JWT
JWT的原則是在服務器身份驗證之後,將生成一個JSON對象並將其發送回用戶。當用戶與服務器通信時,客戶在請求中發回JSON對象。服務器僅依賴於這個JSON對象來標識用戶。
2.JWT的組成
JWT TOKEN分爲三部分:header.payload.signature,因此JWT通常如下表示xxx.yyyyy.zz
2.1header
頭部包含了兩部分,token 類型和採用的加密算法。alg字段指定了生成signature的算法,默認值爲HS256,typ默認值爲JWT。
如果你使用Node.js,可以用Node.js的包base64url來得到這個字符串。Base64是一種編碼,也就是說,它是可以被翻譯回原來的樣子來的。它並不是一種加密過程
{
"alg": "HS256",
"typ": "JWT"
}
2.2payload
這部分就是我們存放信息的地方了,你可以把用戶 ID 等信息放在這裏,JWT 規範裏面對這部分有進行了比較詳細的介紹,常用的由 iss(簽發者),exp(過期時間),sub(面向的用戶),aud(接收方),iat(簽發時間)。同樣的,它會使用 Base64 編碼組成 JWT 結構的第二部分。
{
"iss": "admin", //該JWT的簽發者
"iat": 1535967430, //簽發時間
"exp": 1535974630, //過期時間
"nbf": 1535967430, //該時間之前不接收處理該Token
"sub": "www.admin.com", //面向的用戶
"jti": "9f10e796726e332cec401c569969e13e" //該Token唯一標識
}
2.3Signature
String secret = "123456";//祕鑰
HMACSHA256( base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
前面兩部分都是使用 Base64 進行編碼的,即前端可以解開知道里面的信息。Signature 需要使用編碼後的 header 和 payload 以及我們提供的一個密鑰,然後使用 header 中指定的簽名算法(HS256)進行簽名。簽名的作用是保證 JWT 沒有被篡改過。
2.4JWT的樣式
三個部分通過.
連接在一起就是我們的 JWT 了,它可能長這個樣子,長度貌似和你的加密算法和私鑰有關係。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ
.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s
其實到這一步可能就有人會想了,HTTP 請求總會帶上 token,這樣這個 token 傳來傳去佔用不必要的帶寬啊。如果你這麼想了,那你可以去了解下 HTTP2,HTTP2 對頭部進行了壓縮,相信也解決了這個問題。
2.5簽名的目的
最後一步簽名的過程,實際上是對頭部以及負載內容進行簽名,防止內容被竄改。如果有人對頭部以及負載的內容解碼之後進行修改,再進行編碼,最後加上之前的簽名組合形成新的JWT的話,那麼服務器端會判斷出新的頭部和負載形成的簽名和JWT附帶上的簽名是不一樣的。如果要對新的頭部和負載進行簽名,在不知道服務器加密時用的密鑰的話,得出來的簽名也是不一樣的。
2.6信息暴露
在這裏大家一定會問一個問題:Base64是一種編碼,是可逆的,那麼我的信息不就被暴露了嗎?
是的。所以,在JWT中,不應該在負載裏面加入任何敏感的數據。在上面的例子中,我們傳輸的是用戶的User ID。這個值實際上不是什麼敏感內容,一般情況下被知道也是安全的。但是像密碼這樣的內容就不能被放在JWT中了。如果將用戶的密碼放在了JWT中,那麼懷有惡意的第三方通過Base64解碼就能很快地知道你的密碼了。
因此JWT適合用於向Web應用傳遞一些非敏感信息。JWT還經常用於設計用戶認證和授權系統,甚至實現Web應用的單點登錄。
流程參考如下:資料引用於https://www.cnblogs.com/wenqiangit/p/9592132.html
- 首先,前端通過Web表單將自己的用戶名和密碼發送到後端的接口。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感信息被嗅探。
- 後端覈對用戶名和密碼成功後,將用戶的id等其他信息作爲JWT Payload(負載),將其與頭部分別進行Base64編碼拼接後簽名,形成一個JWT。形成的JWT就是一個形同lll.zzz.xxx的字符串。
- 後端將JWT字符串作爲登錄成功的返回結果返回給前端。前端可以將返回的結果保存在localStorage或sessionStorage上,退出登錄時前端刪除保存的JWT即可。
- 前端在每次請求時將JWT放入HTTP Header中的Authorization位。(解決XSS和XSRF問題)
- 後端檢查是否存在,如存在驗證JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期;檢查Token的接收方是否是自己(可選)。
- 驗證通過後後端使用JWT中包含的用戶信息進行其他邏輯操作,返回相應結果。
3JWT TOKEN的實現
3.1引入依賴
<!--JWT Token-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
3.2Token工具類
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Description: Token工具類
* @author hutao
* @mail [email protected]
* @date 2020年1月11日
*/
public class TokenUtil {
/**
* 簽名祕鑰
*/
public static final String SECRET = "123456";
/**
* 簽發地
*/
public static final String ISSUER = "hutao.com";
/**
* 過期時間
*/
public static final long TTLMILLIS = 3600*1000*60;
/**
* @Description: 生成Token令牌
* @author hutao
* @mail [email protected]
* @date 2020年1月11日
* @param claims 私有聲明
* @param id 編號
* @param issuer 該JWT的簽發者,是否使用是可選的
* @param subject 該JWT所面向的用戶,是否使用是可選的;
* @param ttlMillis 有效時間
* @return token String
*/
public static String generateJwtToken(Map<String,Object> claims,String id, String issuer, String subject, long ttlMillis) {
//指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的時間
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
/* Map<String,Object> claims = new ConcurrentHashMap<String,Object>();
claims.put("aaaa", "aaaa");
claims.put("bbbb", "bbbb");
claims.put("cccc", "cccc");*/
// 通過祕鑰簽名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// 讓我們設置JWT聲明
JwtBuilder builder = Jwts.builder();
if(claims!=null) {
builder.setClaims(claims);//如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之後,就是覆蓋了那些標準的聲明的
}
builder.setId(id);//jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置爲一個不重複的值,主要用來作爲一次性token,從而回避重放攻擊。
builder.setIssuedAt(now);////iat: jwt的簽發時間
builder.setSubject(subject);//sub(Subject):代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什麼userid,roldid之類的,作爲什麼用戶的唯一標誌。
builder.setIssuer(issuer);//iss(issuer):簽發地
builder.signWith(signatureAlgorithm, signingKey); //設置簽名使用的簽名算法和簽名使用的祕鑰
// 如果已指定,則添加到期時間
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//設置過期時間
builder.setExpiration(exp);
}
// 構建JWT並將其序列化爲一個緊湊的url安全字符串
return builder.compact();
}
/**
* @Description: 解析Token
* @author hutao
* @mail [email protected]
* @date 2020年1月11日
*/
public static Claims parseJWT(String jwt) {
// 如果這行代碼不是簽名的JWS(如預期),那麼它將拋出異常
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
.parseClaimsJws(jwt).getBody();
return claims;
}
public static void main(String[] args) {
String token = TokenUtil.generateJwtToken(null,"100",ISSUER,"hutao",TTLMILLIS);
System.out.println(token);
Claims claims = TokenUtil.parseJWT(token);
System.out.println(claims);
}
}