文章目录
- 前言
- 一、Jwt的配置
- 1.安装包
- 2.注入
- 2.1 JWT服务的注入
- 2.2 appsetting.json的配置
- 2.3 JWT服务的封装
- 2.3.1 AddAuthentication
- 2.3.2 AddJwtBearer
- 2.3.4 TokenManager
- 2.4 使用
- 备注
前言
JWT(Json Web Token)
是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准( RFC 7519 ),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
JWT一般由三段构成,用.号分隔开,第一段是header,第二段是payload,第三段是signature,
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
1、header
header是一段base64Url编码的字符串。通常包含两部分内容。
- 签名算法,一般可选的有HS256(HMAC-SHA256) 、RS256(RSA-SHA256) 、ES256(ECDSA-SHA256)
- 固定的类型,直接就是"JWT"
{
'typ': 'JWT',
'alg': 'HS256'
}
2、payload 同样是一段base64Url编码的字符串,一般是用来包含实际传输数据的。payload段官方提供了7个字段可以选择。
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
3、signature signature是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
完整的JWT token=header + “.” + paylod + “.” + signatrue
一、Jwt的配置
1.安装包
Microsoft.AspNetCore.Authentication
Microsoft.AspNetCore.Authentication.JwtBearer
2.注入
2.1 JWT服务的注入
#region JWT services.AddTokenGeneratorService(configuration); #endregion
#region JWT
app.UseTokenGeneratorConfigure(configuration);
app.UseAuthorization();
#endregion
2.2 appsetting.json的配置
"TokenSettings": {
"Audience": "ModernWMS",
"Issuer": "ModernWMS",
"SigningKey": "ModernWMS_SigningKey",
"ExpireMinute": 60
}
2.3 JWT服务的封装
#region JWT
/// <summary>
/// register JWT
/// </summary>
/// <param name="services">services</param>
/// <param name="configuration">configuration</param>
private static void AddTokenGeneratorService(this IServiceCollection services, IConfiguration configuration)
{if (services == null) { throw new ArgumentNullException(nameof(services)); } var tokenSettings = configuration.GetSection("TokenSettings"); services.Configure<TokenSettings>(tokenSettings); services.AddTransient<ITokenManager, TokenManager>(); services.AddAuthentication(options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = nameof(ApiResponseHandler); options.DefaultForbidScheme = nameof(ApiResponseHandler); } ) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidAudience = tokenSettings["Audience"], ValidateIssuer = true, ValidIssuer = tokenSettings["Issuer"], ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenSettings["SigningKey"])), ClockSkew = TimeSpan.Zero }; }) .AddScheme<AuthenticationSchemeOptions, ApiResponseHandler>(nameof(ApiResponseHandler), o => { });
}
private static void UseTokenGeneratorConfigure(this IApplicationBuilder app, IConfiguration configuration)
{
app.UseAuthentication();
}
#endregion
2.3.1 AddAuthentication
AddAuthentication参数说明
- DefaultScheme:就是Bearer认证
- DefaultAuthenticateScheme:就是权限Bearer认证
- DefaultChallengeScheme:401登录报错
- DefaultForbidScheme:403资源报错
Bearer认证的好处:
- CORS: cookies + CORS 并不能跨不同的域名。而Bearer验证在任何域名下都可以使用HTTP
header头部来传输用户信息。 - 对移动端友好: 当你在一个原生平台(iOS, Android,WindowsPhone等)时,使用Cookie验证并不是一个好主意,因为你得和Cookie容器打交道,而使用Bearer验证则简单的多。
- CSRF: 因为Bearer验证不再依赖于cookies, 也就避免了跨站请求攻击。
- 标准:在Cookie认证中,用户未登录时,返回一个302到登录页面,这在非浏览器情况下很难处理,而Bearer验证则返回的是标准的401
challeng
/// <summary>
/// Custom Processing Unit
/// </summary>
public class ApiResponseHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{/// <summary> /// token manager /// </summary> private readonly ITokenManager _tokenManager; /// <summary> /// cache manager /// </summary> private readonly CacheManager _cacheManager; /// <summary> /// constructor /// </summary> /// <param name="options">options</param> /// <param name="logger">logger</param> /// <param name="encoder">encoder</param> /// <param name="clock"></param> /// <param name="tokenManager">tokenManager</param> /// <param name="cacheManager">cacheManagerparam> public ApiResponseHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock , ITokenManager tokenManager , CacheManager cacheManager) : base(options, logger, encoder, clock) { this._tokenManager = tokenManager; _cacheManager = cacheManager; } /// <summary> /// handle authority /// </summary> /// <returns></returns> protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { var token = Request.Headers["Authorization"].ObjToString().Replace("Bearer ", ""); var currentUser = this._tokenManager.GetCurrentUser(token); var flag = _cacheManager.Is_Token_Exist<string>(currentUser.user_id, "WebToken", _tokenManager.GetRefreshTokenExpireMinute()); if (!flag) { return AuthenticateResult.Fail("Sorry, you don't have the authority required!"); } throw new NotImplementedException(); } /// <summary> /// authentication /// </summary> /// <param name="properties">参数</param> /// <returns></returns> protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { Response.ContentType = "application/json"; Response.StatusCode = StatusCodes.Status401Unauthorized; await Response.WriteAsync(JsonHelper.SerializeObject(ResultModel<object>.Error("Sorry, please sign in first!", 401))); } /// <summary> /// access denied /// </summary> /// <param name="properties"></param> /// <returns></returns> protected override async Task HandleForbiddenAsync(AuthenticationProperties properties) { Response.ContentType = "application/json"; Response.StatusCode = StatusCodes.Status403Forbidden; await Response.WriteAsync(JsonHelper.SerializeObject(ResultModel<object>.Error("Sorry, you don't have the authority required!", 403))); }
}
2.3.2 AddJwtBearer
TokenValidationParameters的参数默认值:
- ValidateAudience = true, ----- 如果设置为false,则不验证Audience受众人
- ValidateIssuer = true , ----- 如果设置为false,则不验证Issuer发布人,但建议不建议这样设置
ValidateIssuerSigningKey = false, - ValidateLifetime = true, -----是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
- RequireExpirationTime = true, ----- 是否要求Token的Claims中必须包含Expires
- ClockSkew = TimeSpan.FromSeconds(300), ----- 允许服务器时间偏移量300秒,即我们配置的过期时间加上这个允许偏移的时间值,才是真正过期的时间(过期时间+偏移值)你也可以设置为0,ClockSkew = TimeSpan.Zero
2.3.4 TokenManager
TokenManager主要是负责token生成和校验的封装
/// <summary>
/// token manager
/// </summary>
public class TokenManager : ITokenManager
{
private readonly IOptions<TokenSettings> _tokenSettings;//token setting
private readonly IHttpContextAccessor _accessor; // Inject IHttpContextAccessor
/// <summary>
/// Constructor
/// </summary>
/// <param name="tokenSettings">token setting s</param>
/// <param name="accessor">Inject IHttpContextAccessor</param>
public TokenManager(IOptions<TokenSettings> tokenSettings
, IHttpContextAccessor accessor)
{
this._tokenSettings = tokenSettings;
this._accessor = accessor;
}
/// <summary>
/// Method of refreshing token
/// </summary>
/// <returns></returns>
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } } /// <summary> /// Method of generating AccessToken /// </summary> /// <param name="userClaims">自定义信息</param> /// <returns>(token,有效分钟数)</returns> public (string token, int expire) GenerateToken(CurrentUser userClaims) { string token = new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken( issuer: _tokenSettings.Value.Issuer, audience: _tokenSettings.Value.Audience, claims: SetClaims(userClaims), expires: DateTime.Now.AddMinutes(_tokenSettings.Value.ExpireMinute), signingCredentials: new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GlobalConsts.SigningKey)), SecurityAlgorithms.HmacSha256) )); return (token, _tokenSettings.Value.ExpireMinute); } /// <summary> /// Get the current user information in the token /// </summary> /// <returns></returns> public CurrentUser GetCurrentUser() { if (_accessor.HttpContext == null) { return new CurrentUser(); } var token = _accessor.HttpContext.Request.Headers["Authorization"].ObjToString(); if (!token.StartsWith("Bearer")) { return new CurrentUser(); } token = token.Replace("Bearer ", ""); if (token.Length > 0) { var principal = new JwtSecurityTokenHandler().ValidateToken(token, new TokenValidationParameters { ValidateAudience = false, ValidateIssuer = false, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GlobalConsts.SigningKey)), ValidateLifetime = false }, out var securityToken); if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) { return new CurrentUser(); } var user = JsonHelper.DeserializeObject<CurrentUser>(principal.Claims.First(claim => claim.Type == ClaimValueTypes.Json).Value); if (user != null) { return user; } else { return new CurrentUser(); } } else { return new CurrentUser(); } } /// <summary> /// Get the current user information in the token /// </summary> /// <returns></returns> public CurrentUser GetCurrentUser(string token) { if (token.Length > 0) { var principal = new JwtSecurityTokenHandler().ValidateToken(token, new TokenValidationParameters { ValidateAudience = false, ValidateIssuer = false, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GlobalConsts.SigningKey)), ValidateLifetime = false }, out var securityToken); if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) { return new CurrentUser(); } var user = JsonHelper.DeserializeObject<CurrentUser>(principal.Claims.First(claim => claim.Type == ClaimValueTypes.Json).Value); if (user != null) { return user; } else { return new CurrentUser(); } } else { return new CurrentUser(); } } /// <summary> /// Method of refreshing token /// </summary> /// <returns></returns> public int GetRefreshTokenExpireMinute() { return _tokenSettings.Value.ExpireMinute + 1; } /// <summary> /// Setting Custom Information /// </summary> /// <param name="userClaims">Custom Information</param> /// <returns></returns> private static IEnumerable<Claim> SetClaims(CurrentUser userClaims) { return new List<Claim> { new Claim(ClaimTypes.Sid, Guid.NewGuid().ToString()), new Claim(ClaimValueTypes.Json,JsonHelper.SerializeObject(userClaims), ClaimValueTypes.Json) }; }
}
2.4 使用
[Authorize]
只需要在控制器方法上加Authorize特性就行,但是前面已经通过ApiResponseHandler自定义校验所以不需要加Authorize特性就可以控制全局控制器的权限校验。
备注
登录和刷新token完整逻辑
/// <summary>
/// account
/// </summary>
[Route("[controller]")]
[ApiController]
[ApiExplorerSettings(GroupName = "Base")]
public class AccountController : BaseController
{
/// <summary>
/// token manger
/// </summary>
private readonly ITokenManager _tokenManager;
/// <summary>
/// Log helper
/// </summary>
private readonly ILogger<AccountController> _logger;/// <summary> /// cache helper /// </summary> private readonly CacheManager _cacheManager; /// <summary> /// account service class /// </summary> private readonly IAccountService _accountService; /// <summary> /// Localizer /// </summary> private readonly IStringLocalizer _stringLocalizer; /// <summary> /// Structure /// </summary> /// <param name="logger">logger helper</param> /// <param name="tokenManager">token manger</param> /// <param name="cacheManager">cache helper</param> /// <param name="accountService">account service class</param> /// <param name="stringLocalizer">Localizer</param> public AccountController(ILogger<AccountController> logger , ITokenManager tokenManager , CacheManager cacheManager , IAccountService accountService , IStringLocalizer stringLocalizer ) { this._tokenManager = tokenManager; this._logger = logger; this._cacheManager = cacheManager; this._accountService = accountService; this._stringLocalizer = stringLocalizer; } #region Login /// <summary> /// login /// </summary> /// <param name="loginAccount">user's account infomation</param> /// <returns></returns> [AllowAnonymous] [HttpPost("/login")] public async Task<ResultModel<LoginOutputViewModel>> LoginAsync(LoginInputViewModel loginAccount) { var user = await _accountService.Login(loginAccount,CurrentUser); if (user != null) { var result = _tokenManager.GenerateToken( new CurrentUser { user_id = user.user_id, user_name = user.user_name, user_num = user.user_num, user_role = user.user_role, tenant_id = user.tenant_id } ); string rt = this._tokenManager.GenerateRefreshToken(); user.access_token = result.token; user.expire = result.expire; user.refresh_token = rt; await _cacheManager.TokenSet(user.user_id, "WebRefreshToken", rt, _tokenManager.GetRefreshTokenExpireMinute()); return ResultModel<LoginOutputViewModel>.Success(user); } else { return ResultModel<LoginOutputViewModel>.Error(_stringLocalizer["login_failed"]); } } /// <summary> /// get a new token /// </summary> /// <param name="inPutViewModel">old access token and refreshtoken key</param> /// <returns></returns> [AllowAnonymous] [HttpPost("/refresh-token")] public async Task<ResultModel<string>> RefreshToken([FromBody] RefreshTokenInPutViewModel inPutViewModel) { var currentUser = this._tokenManager.GetCurrentUser(inPutViewModel.AccessToken); var flag = _cacheManager.Is_Token_Exist<string>(currentUser.user_id, "WebRefreshToken", _tokenManager.GetRefreshTokenExpireMinute()); if (!flag) { return ResultModel<string>.Error("refreshtoken_failure"); } else { var result = _tokenManager.GenerateToken(currentUser); return ResultModel<string>.Success(result.token); } } #endregion #region hello world [AllowAnonymous] [HttpPost("/hello-world")] public ResultModel<string> hello_world() { return ResultModel<string>.Success(_accountService.HelloWorld()); } #endregion
}