RESTful API認證方式
一般來講,對於RESTful API都會有認證(Authentication)和授權(Authorization)過程,保證API的安全性。
Authentication vs. Authorization
Authentication指的是確定這個用戶的身份,Authorization是確定該用戶擁有什么操作權限。
認證方式一般有三種
Basic Authentication
這種方式是直接將用戶名和密碼放到Header中,使用 Authorization: Basic Zm9vOmJhcg==
,使用最簡單但是最不安全。
TOKEN認證
這種方式也是再HTTP頭中,使用 Authorization: Bearer <token>
,使用最廣泛的TOKEN是JWT,通過簽名過的TOKEN。
OAuth2.0
這種方式安全等級最高,但是也是最復雜的。如果不是大型API平台或者需要給第三方APP使用的,沒必要整這么復雜。
一般項目中的RESTful API使用JWT來做認證就足夠了。
什么是JWT
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
JWT官網: https://jwt.io/
JWT是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串。就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的構成
第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature)。
header
jwt的頭部承載兩部分信息:
- 聲明類型,這里是jwt
- 聲明加密的算法 通常直接使用 HMAC SHA256
這里的加密算法是單向函數散列算法,常見的有MD5、SHA、HAMC。這里使用基於密鑰的Hash算法HMAC生成散列值。
- MD5 message-digest algorithm 5 (信息-摘要算法)縮寫,廣泛用於加密和解密技術,常用於文件校驗。校驗?不管文件多大,經過MD5后都能生成唯一的MD5值
- SHA (Secure Hash Algorithm,安全散列算法),數字簽名等密碼學應用中重要的工具,安全性高於MD5
- HMAC (Hash Message Authentication Code,散列消息鑒別碼,基於密鑰的Hash算法的認證協議。用公開函數和密鑰產生一個固定長度的值作為認證標識,用這個標識鑒別消息的完整性。常用於接口簽名驗證
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
載荷就是存放有效信息的地方,這些有效信息包含三個部分:
- 標准中注冊的聲明
- 公共的聲明
- 私有的聲明
標准中注冊的聲明 (建議但不強制使用) :
- iss: jwt簽發者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
公共的聲明:
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密
私有的聲明:
私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。
定義一個payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后將其進行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:
header (base64后的)
payload (base64后的)
secret
這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.連接成一個完整的字符串,構成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
如何應用
一般是在請求頭里加入Authorization,並加上Bearer標注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
服務器負責解析這個HTTP頭來做用戶認證和授權處理。大致流程如下:
安全相關
JWT協議本身不具備安全傳輸功能,所以必須借助於SSL/TLS的安全通道,所以建議如下:
- 不應該在jwt的payload部分存放敏感信息,因為該部分是客戶端可解密的部分。
- 保護好secret私鑰,該私鑰非常重要。
- 如果可以,請使用https協議
和SpringBoot集成
簡要的說明下我們為什么要用JWT,因為我們要實現完全的前后端分離,所以不可能使用session,cookie的方式進行鑒權,所以JWT就被派上了用場,可以通過一個加密密鑰來進行前后端的鑒權。
程序邏輯:
- 我們POST用戶名與密碼到/login進行登入,如果成功返回一個加密token,失敗的話直接返回401錯誤。
- 之后用戶訪問每一個需要權限的網址請求必須在header中添加Authorization字段,例如Authorization: token,token為密鑰。
- 后台會進行token的校驗,如果不通過直接返回401。
這里我講一下如何在SpringBoot中使用JWT來做接口權限認證,安全框架依舊使用Shiro,JWT的實現使用 jjwt
添加Maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
<!-- shiro 權限控制 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- shiro ehcache (shiro緩存)-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
創建用戶Service
這個在shiro一節講過如果創建角色權限表,添加用戶Service來執行查找用戶操作,這里就不多講具體實現了,只列出關鍵代碼:
/**
* 通過名稱查找用戶
*
* @param username
* @return
*/
public ManagerInfo findByUsername(String username) {
ManagerInfo managerInfo = managerInfoDao.findByUsername(username);
if (managerInfo == null) {
throw new UnknownAccountException();
}
return managerInfo;
}
用戶信息類:
public class ManagerInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主鍵ID
*/
private Integer id;
/**
* 賬號
*/
private String username;
/**
* 密碼
*/
private String password;
/**
* md5密碼鹽
*/
private String salt;
/**
* 一個管理員具有多個角色
*/
private List<SysRole> roles;
JWT工具類
我們寫一個簡單的JWT加密,校驗工具,並且使用用戶自己的密碼充當加密密鑰,這樣保證了token 即使被他人截獲也無法破解。並且我們在token中附帶了username信息,並且設置密鑰5分鍾就會過期。
public class JWTUtil {
// 過期時間5分鍾
private static final long EXPIRE_TIME = 5 * 60 * 1000;
/**
* 校驗token是否正確
*
* @param token 密鑰
* @param secret 用戶的密碼
* @return 是否正確
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 獲得token中的信息無需secret解密也能獲得
*
* @return token中包含的用戶名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成簽名,5min后過期
*
* @param username 用戶名
* @param secret 用戶的密碼
* @return 加密的token
*/
public static String sign(String username, String secret) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.