JWT(Json Web Token)
是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准( RFC 7519 ),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
JWT一般由三段构成,用.号分隔开,第一段是header,第二段是payload,第三段是signature,
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
1、header
header是一段base64Url编码的字符串。通常包含两部分内容。
{
'typ': 'JWT',
'alg': 'HS256'
}
2、payload 同样是一段base64Url编码的字符串,一般是用来包含实际传输数据的。payload段官方提供了7个字段可以选择。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
3、signature signature是一个签证信息,这个签证信息由三部分组成:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
完整的JWT token=header + “.” + paylod + “.” + signatrue
Microsoft.AspNetCore.Authentication
Microsoft.AspNetCore.Authentication.JwtBearer
#region JWT
services.AddTokenGeneratorService(configuration);
#endregion
#region JWT
app.UseTokenGeneratorConfigure(configuration);
app.UseAuthorization();
#endregion
"TokenSettings": {
"Audience": "ModernWMS",
"Issuer": "ModernWMS",
"SigningKey": "ModernWMS_SigningKey",
"ExpireMinute": 60
}
#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
AddAuthentication参数说明
Bearer认证的好处:
/// <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)));
}
}
TokenValidationParameters的参数默认值:
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)
};
}
}
[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
}