API授權認證原理
通常我們在調用第三方API的時候都需要一個Token作爲憑證,調用方自行根據第三方Token生成規則來生成Token,並作爲參數傳入。現在作爲API提供方,我們需要一套認證機制來確保API和數據的安全,僅被授權的調用方所使用。
JWT(Json Web Token)
Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519)。該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
JWT是由.
分割的如下三部分組成:
-
頭部(Header)
Header 一般由兩個部分組成:
- alg
- typ
alg
是是所使用的hash算法,如:HMAC SHA256或RSA,typ
是Token的類型,在這裏就是:JWT。
{
"alg": "HS256",
"typ": "JWT"
}
然後使用Base64Url編碼成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>
- 載荷(Payload)
這一部分是JWT主要的信息存儲部分,其中包含了許多種的聲明(claims)。
Claims的實體一般包含用戶和一些元數據,這些claims分成三種類型:
-
reserved claims:預定義的 一些聲明,並不是強制的但是推薦,它們包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(這裏都使用三個字母的原因是保證 JWT 的緊湊)。
-
public claims: 公有聲明,這個部分可以隨便定義,但是要注意和 IANA JSON Web Token 衝突。
-
private claims: 私有聲明,這個部分是共享被認定信息中自定義部分。
一個簡單的Pyload可以是這樣子的:
{
"sub": "1234567890",
"name": "Jonny Yan",
"admin": true
}
這部分同樣使用Base64Url編碼成第二部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>
- 簽名(Signature)
Signature是用來驗證發送者的JWT的同時也能確保在期間不被篡改。
在創建該部分時候你應該已經有了編碼後的Header和Payload,然後使用保存在服務端的祕鑰對其簽名,一個完整的JWT如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
因此使用JWT具有如下好處:
-
通用:因爲json的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
-
緊湊:JWT的構成非常簡單,字節佔用很小,可以通過 GET、POST 等放在 HTTP 的 header 中,非常便於傳輸。
-
擴展:JWT是自我包涵的,包含了必要的所有信息,不需要在服務端保存會話信息, 非常易於應用的擴展。
關於更多JWT的介紹,網上非常多,這裏就不再多做介紹。下面,演示一下 ASP.NET Core 中 JwtBearer 認證的使用方式。
JwtBearer認證實現
根據前面授權認證的原理,大致包括兩部分:發放Token和驗證Token,先來看總體的框架搭建,後續會解釋每個類的作用。
JwtHelper.cs:主要用於生成Token和驗證Token
TokenAuthMiddleware.cs:Token驗證中間件,用於截獲請求
TokenModelJwt.cs:Token實體類,定義一些自己想放置在token中的屬性
- 發放Token
在JwtHelper類中,實現發放token方法,該方法輸入參數爲Token實體類
/// <summary>
/// 生成AccessToken
/// </summary>
/// <param name="tokenModel"></param>
/// <returns></returns>
public static string IssueJWT(TokenModelJwt tokenModel)
{
DateTime UTC = DateTime.UtcNow;
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub,tokenModel.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),//JWT ID,JWT的唯一標識
new Claim(JwtRegisteredClaimNames.Iat, UTC.ToString(), ClaimValueTypes.Integer64),//Issued At,JWT頒發的時間,採用標準unix時間,用於驗證過期
};
claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s)));
JwtSecurityToken jwt = new JwtSecurityToken(
issuer: ConfigHelper.GetConfig("Token:Issuer"),
audience: tokenModel.Name,
claims: claims,//聲明集合
expires: UTC.AddMinutes(int.Parse(ConfigHelper.GetConfig("Token:Expires"))),//指定token的生命週期,unix時間戳格式,非必須
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(ConfigHelper.GetConfig("Token:SecurityKey"))), SecurityAlgorithms.HmacSha256)
);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);//生成最後的JWT字符串
return encodedJwt;
}
Token實體類
public class TokenModelJwt
{
public int Id { get; set; }
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Role
/// </summary>
public string Role { get; set; }
}
在控制器中,新建一個Action用於給外部調用來生成Token
/// <summary>
/// 獲取Access Token
/// </summary>
/// <param name="userName"></param>
/// <param name="passWord"></param>
/// <returns></returns>
[HttpGet]
public object GetAccessToken([FromQuery]string userName, [FromQuery] string passWord)
{
string jwtStr = string.Empty;
bool suc = false;
//查詢數據庫,檢驗用戶是否存在
if (true)
{
TokenModelJwt tokenModel = new TokenModelJwt
{
Id = 1,
Name = "Jonny Yan",
Role = "Admin"
};
jwtStr = JwtHelper.IssueJWT(tokenModel);//登錄,獲取到一定規則的 Token 令牌
suc = true;
}
else
{
jwtStr = "InValid User!";
Logger.Info("InValid User");
}
return new { Token = jwtStr, Success = suc };
}
至此已經完成Token發送的功能
- 驗證Token
我們發放了Token,調用方將Token至於請求header中傳遞過來,我們要驗證Token的有效性,主要驗證如下部分
- Token的格式是否正確
- Token的頒發者,過期時間等
- 解析Token中的用戶信息(發放Token時,我們傳入的token實體)
編輯Jwthelper.cs類,實現驗證Token的方法
/// <summary>
/// 驗證Token是否有效
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public static SecurityToken VerifyToken(string token)
{
var validationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true, //驗證私鑰
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(ConfigHelper.GetConfig("Token:SecurityKey"))),
ValidateLifetime = true,//是否驗證Token有效期
ClockSkew = TimeSpan.Zero, // 允許的服務器時間偏移量
RequireExpirationTime = true,//否要求Token的Claims中必須包含Expires
ValidateAudience = false, //是否驗證訂閱者
ValidateIssuer = true,//是否驗證提供者
ValidIssuers = new List<string> { ConfigHelper.GetConfig("Token:Issuer") } //合法的token提供者
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken validatedToken = null;
try
{
tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
}
catch (SecurityTokenException ex)
{
//log ex.message
Logger.Error(ex.Message, ex);
}
catch (Exception ex)
{
//log ex.message
Logger.Error(ex.Message, ex);
}
return validatedToken;
}
編輯TokenAuthMiddleware.cs中間件類,截獲請求,實現Token驗證
public class TokenAuthMiddleware
{
/// <summary>
/// http委託
/// </summary>
private readonly RequestDelegate _next;
/// <summary>
/// 構造函數
/// </summary>
/// <param name="next"></param>
public TokenAuthMiddleware(RequestDelegate next)
{
_next = next;
}
/// <summary>
/// 驗證授權
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public Task Invoke(HttpContext httpContext)
{
var headers = httpContext.Request.Headers;
//檢測是否包含'Authorization'請求頭,如果不包含返回context進行下一個中間件,用於訪問不需要認證的API
if (!headers.ContainsKey("Authorization"))
{
return _next(httpContext);
}
var tokenStr = headers["Authorization"];
string jwtStr = tokenStr.ToString().Replace("Bearer ", "");
try
{
JwtSecurityToken access_token = JwtHelper.VerifyToken(jwtStr) as JwtSecurityToken;
if (access_token != null)
{
object role;
access_token.Payload.TryGetValue(ClaimTypes.Role, out role);
TokenModelJwt tm = new TokenModelJwt
{
Id = int.Parse(access_token.Payload.Sub),
Name = access_token.Payload.Aud[0]
};
var claimList = new List<Claim>();
if (role.GetType().Equals(typeof(JArray)))
{
IEnumerable enumerable = role as IEnumerable;
foreach (object element in enumerable)
{
claimList.Add(new Claim(ClaimTypes.Role, element.ToString()));
}
}
else
{
claimList.Add(new Claim(ClaimTypes.Role, role.ToString()));
}
claimList.Add(new Claim(ClaimTypes.Name, tm.Name));
claimList.Add(new Claim(ClaimTypes.PrimarySid, tm.Id.ToString()));
var identity = new ClaimsIdentity(claimList);
var principal = new ClaimsPrincipal(identity);
httpContext.User = principal;
}
return _next(httpContext);
}
catch (Exception ex)
{
Logger.Info(ex.Message, ex);
return httpContext.Response.WriteAsync("Invalid Access Token");
}
}
}
注意事項:在解析Token後,如果用戶由多個角色,我們需要循環將每個Role都加入到Claim List中
至此已經完成Token驗證的功能
- 配置自定義的中間件
打開iStartup.cs類,編輯ConfigureServices方法,插入以下代碼
//自定義認證
services.AddAuthorization(options =>
{
options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());
options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build());
options.AddPolicy("ClientOrAdmin", policy => policy.RequireRole("Admin", "Client").Build());
});
//微軟自帶認證
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true, //驗證私鑰
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["Token:SecurityKey"])),
ValidateLifetime = true,//是否驗證Token有效期
ClockSkew = TimeSpan.Zero, // 允許的服務器時間偏移量
RequireExpirationTime = true,//否要求Token的Claims中必須包含Expires
ValidateAudience = false, //是否驗證訂閱者
ValidateIssuer = true,//是否驗證提供者
ValidIssuers = new List<string> { Configuration["Token:Issuer"] } //合法的token提供者
};
});
編輯Config方法,啓用中間件
至此已經完成自定義中間件的配置
- 測試授權
在需要授權的Action中,加上Authorize特性,並且指定可以訪問的角色(此處爲策略,因爲一個Action可能會被允許多個role訪問,所以我們用策略來對role分組)
如果沒由輸入Token,訪問API直接返回401錯誤
至此基於JWT授權認證的整個過程已經完成