通常我们在调用第三方API的时候都需要一个Token作为凭证,调用方自行根据第三方Token生成规则来生成Token,并作为参数传入。现在作为API提供方,我们需要一套认证机制来确保API和数据的安全,仅被授权的调用方所使用。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT是由.
分割的如下三部分组成:
头部(Header)
Header 一般由两个部分组成:
alg
是是所使用的hash算法,如:HMAC SHA256或RSA,typ
是Token的类型,在这里就是:JWT。
{
"alg": "HS256",
"typ": "JWT"
}
然后使用Base64Url编码成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..
这一部分是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.
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 认证的使用方式。
根据前面授权认证的原理,大致包括两部分:发放Token和验证Token,先来看总体的框架搭建,后续会解释每个类的作用。
JwtHelper.cs:主要用于生成Token和验证Token
TokenAuthMiddleware.cs:Token验证中间件,用于截获请求
TokenModelJwt.cs:Token实体类,定义一些自己想放置在token中的属性
在JwtHelper类中,实现发放token方法,该方法输入参数为Token实体类
///
/// 生成AccessToken
///
///
///
public static string IssueJWT(TokenModelJwt tokenModel)
{
DateTime UTC = DateTime.UtcNow;
var claims = new List
{
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; }
///
/// Name
///
public string Name { get; set; }
///
/// Role
///
public string Role { get; set; }
}
在控制器中,新建一个Action用于给外部调用来生成Token
///
/// 获取Access Token
///
///
///
///
[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至于请求header中传递过来,我们要验证Token的有效性,主要验证如下部分
编辑Jwthelper.cs类,实现验证Token的方法
///
/// 验证Token是否有效
///
///
///
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 { 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
{
///
/// http委托
///
private readonly RequestDelegate _next;
///
/// 构造函数
///
///
public TokenAuthMiddleware(RequestDelegate next)
{
_next = next;
}
///
/// 验证授权
///
///
///
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();
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 { Configuration["Token:Issuer"] } //合法的token提供者
};
});
编辑Config方法,启用中间件
至此已经完成自定义中间件的配置
在需要授权的Action中,加上Authorize特性,并且指定可以访问的角色(此处为策略,因为一个Action可能会被允许多个role访问,所以我们用策略来对role分组)
如果没由输入Token,访问API直接返回401错误
至此基于JWT授权认证的整个过程已经完成