【愚公系列】2023年02月 WMS智能仓储系统-008.Jwt的配置

文章目录

  • 前言
  • 一、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,

代码语言:javascript
复制
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

1、header

header是一段base64Url编码的字符串。通常包含两部分内容。

  • 签名算法,一般可选的有HS256(HMAC-SHA256) 、RS256(RSA-SHA256) 、ES256(ECDSA-SHA256)
  • 固定的类型,直接就是"JWT"
代码语言:javascript
复制
{
  'typ': 'JWT',
  'alg': 'HS256'
}

2、payload 同样是一段base64Url编码的字符串,一般是用来包含实际传输数据的。payload段官方提供了7个字段可以选择。

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
代码语言:javascript
复制
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

3、signature signature是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret
代码语言:javascript
复制
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

完整的JWT token=header + “.” + paylod + “.” + signatrue

在这里插入图片描述

一、Jwt的配置

1.安装包

代码语言:javascript
复制
Microsoft.AspNetCore.Authentication
Microsoft.AspNetCore.Authentication.JwtBearer
在这里插入图片描述

2.注入

2.1 JWT服务的注入

代码语言:javascript
复制
#region JWT
services.AddTokenGeneratorService(configuration);
#endregion

#region JWT
app.UseTokenGeneratorConfigure(configuration);
app.UseAuthorization();
#endregion

在这里插入图片描述

2.2 appsetting.json的配置

代码语言:javascript
复制
"TokenSettings": {
"Audience": "ModernWMS",
"Issuer": "ModernWMS",
"SigningKey": "ModernWMS_SigningKey",
"ExpireMinute": 60
}
在这里插入图片描述

2.3 JWT服务的封装

代码语言:javascript
复制
#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(&#34;TokenSettings&#34;);
services.Configure&lt;TokenSettings&gt;(tokenSettings);
services.AddTransient&lt;ITokenManager, TokenManager&gt;();

services.AddAuthentication(options =&gt;
{
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = nameof(ApiResponseHandler); 
    options.DefaultForbidScheme = nameof(ApiResponseHandler);
}
)
.AddJwtBearer(options =&gt;
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = true,
        ValidAudience = tokenSettings[&#34;Audience&#34;],
        ValidateIssuer = true,
        ValidIssuer = tokenSettings[&#34;Issuer&#34;],
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenSettings[&#34;SigningKey&#34;])),
        ClockSkew = TimeSpan.Zero
    };
})
.AddScheme&lt;AuthenticationSchemeOptions, ApiResponseHandler&gt;(nameof(ApiResponseHandler), o =&gt; { });

}

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
代码语言:javascript
复制
/// <summary>
/// Custom Processing Unit
/// </summary>
public class ApiResponseHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{

/// &lt;summary&gt;
/// token manager
/// &lt;/summary&gt;
private readonly ITokenManager _tokenManager;
/// &lt;summary&gt;
/// cache manager
/// &lt;/summary&gt;
private readonly CacheManager _cacheManager;

/// &lt;summary&gt;
/// constructor 
/// &lt;/summary&gt;
/// &lt;param name=&#34;options&#34;&gt;options&lt;/param&gt;
/// &lt;param name=&#34;logger&#34;&gt;logger&lt;/param&gt;
/// &lt;param name=&#34;encoder&#34;&gt;encoder&lt;/param&gt;
/// &lt;param name=&#34;clock&#34;&gt;&lt;/param&gt;
/// &lt;param name=&#34;tokenManager&#34;&gt;tokenManager&lt;/param&gt;
/// &lt;param name=&#34;cacheManager&#34;&gt;cacheManagerparam&gt;
public ApiResponseHandler(IOptionsMonitor&lt;AuthenticationSchemeOptions&gt; options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock
    , ITokenManager tokenManager
    , CacheManager cacheManager)
    : base(options, logger, encoder, clock)
{
    this._tokenManager = tokenManager;
    _cacheManager = cacheManager;
}
/// &lt;summary&gt;
/// handle authority
/// &lt;/summary&gt;
/// &lt;returns&gt;&lt;/returns&gt;
protected override async Task&lt;AuthenticateResult&gt; HandleAuthenticateAsync()
{
    var token = Request.Headers[&#34;Authorization&#34;].ObjToString().Replace(&#34;Bearer &#34;, &#34;&#34;);
    var currentUser = this._tokenManager.GetCurrentUser(token);
    var flag =  _cacheManager.Is_Token_Exist&lt;string&gt;(currentUser.user_id, &#34;WebToken&#34;, _tokenManager.GetRefreshTokenExpireMinute());
    if (!flag)
    {
        return AuthenticateResult.Fail(&#34;Sorry, you don&#39;t have the authority required!&#34;);
    }
        throw new NotImplementedException();
}
/// &lt;summary&gt;
/// authentication
/// &lt;/summary&gt;
/// &lt;param name=&#34;properties&#34;&gt;参数&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
    Response.ContentType = &#34;application/json&#34;;
    Response.StatusCode = StatusCodes.Status401Unauthorized;
    await Response.WriteAsync(JsonHelper.SerializeObject(ResultModel&lt;object&gt;.Error(&#34;Sorry, please sign in first!&#34;, 401)));
}
/// &lt;summary&gt;
/// access denied
/// &lt;/summary&gt;
/// &lt;param name=&#34;properties&#34;&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
    Response.ContentType = &#34;application/json&#34;;
    Response.StatusCode = StatusCodes.Status403Forbidden;
    await Response.WriteAsync(JsonHelper.SerializeObject(ResultModel&lt;object&gt;.Error(&#34;Sorry, you don&#39;t have the authority required!&#34;, 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生成和校验的封装

代码语言:javascript
复制
/// <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);
    }
}
/// &lt;summary&gt;
/// Method of generating AccessToken
/// &lt;/summary&gt;
/// &lt;param name=&#34;userClaims&#34;&gt;自定义信息&lt;/param&gt;
/// &lt;returns&gt;(token,有效分钟数)&lt;/returns&gt;
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);
}
/// &lt;summary&gt;
/// Get the current user information in the token
/// &lt;/summary&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public CurrentUser GetCurrentUser()
{
    if (_accessor.HttpContext == null)
    {
        return new CurrentUser();
    }
    var token = _accessor.HttpContext.Request.Headers[&#34;Authorization&#34;].ObjToString();
    if (!token.StartsWith(&#34;Bearer&#34;))
    {
        return new CurrentUser();
    }
    token = token.Replace(&#34;Bearer &#34;, &#34;&#34;);
    if (token.Length &gt; 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&lt;CurrentUser&gt;(principal.Claims.First(claim =&gt; claim.Type == ClaimValueTypes.Json).Value);
        if (user != null)
        {
            return user;
        }
        else
        {
            return new CurrentUser();
        }
    }
    else
    {
        return new CurrentUser();
    }

}

/// &lt;summary&gt;
/// Get the current user information in the token
/// &lt;/summary&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public CurrentUser GetCurrentUser(string token)
{
    if (token.Length &gt; 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&lt;CurrentUser&gt;(principal.Claims.First(claim =&gt; claim.Type == ClaimValueTypes.Json).Value);
        if (user != null)
        {
            return user;
        }
        else
        {
            return new CurrentUser();
        }
    }
    else
    {
        return new CurrentUser();
    }

}
/// &lt;summary&gt;
/// Method of refreshing token
/// &lt;/summary&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public int GetRefreshTokenExpireMinute()
{
    return _tokenSettings.Value.ExpireMinute + 1;
}

/// &lt;summary&gt;
/// Setting Custom Information
/// &lt;/summary&gt;
/// &lt;param name=&#34;userClaims&#34;&gt;Custom Information&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
private static IEnumerable&lt;Claim&gt; SetClaims(CurrentUser userClaims)
{
    return new List&lt;Claim&gt;
    {
        new Claim(ClaimTypes.Sid, Guid.NewGuid().ToString()),
        new Claim(ClaimValueTypes.Json,JsonHelper.SerializeObject(userClaims), ClaimValueTypes.Json)
    };
}

}

在这里插入图片描述

2.4 使用

代码语言:javascript
复制
[Authorize]

只需要在控制器方法上加Authorize特性就行,但是前面已经通过ApiResponseHandler自定义校验所以不需要加Authorize特性就可以控制全局控制器的权限校验。

备注

登录和刷新token完整逻辑

代码语言:javascript
复制
/// <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;

/// &lt;summary&gt;
/// cache helper
/// &lt;/summary&gt;
private readonly CacheManager _cacheManager;

/// &lt;summary&gt;
/// account service class
/// &lt;/summary&gt;
private readonly IAccountService _accountService;

/// &lt;summary&gt;
/// Localizer
/// &lt;/summary&gt;
private readonly IStringLocalizer _stringLocalizer;
/// &lt;summary&gt;
/// Structure
/// &lt;/summary&gt;
/// &lt;param name=&#34;logger&#34;&gt;logger helper&lt;/param&gt;
/// &lt;param name=&#34;tokenManager&#34;&gt;token manger&lt;/param&gt;
/// &lt;param name=&#34;cacheManager&#34;&gt;cache helper&lt;/param&gt;
/// &lt;param name=&#34;accountService&#34;&gt;account service class&lt;/param&gt;
/// &lt;param name=&#34;stringLocalizer&#34;&gt;Localizer&lt;/param&gt;
public AccountController(ILogger&lt;AccountController&gt; 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

/// &lt;summary&gt;
/// login
/// &lt;/summary&gt;
/// &lt;param name=&#34;loginAccount&#34;&gt;user&#39;s account infomation&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
[AllowAnonymous]
[HttpPost(&#34;/login&#34;)]
public async Task&lt;ResultModel&lt;LoginOutputViewModel&gt;&gt; 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, &#34;WebRefreshToken&#34;, rt, _tokenManager.GetRefreshTokenExpireMinute());

        return ResultModel&lt;LoginOutputViewModel&gt;.Success(user);
    }
    else
    {
        return ResultModel&lt;LoginOutputViewModel&gt;.Error(_stringLocalizer[&#34;login_failed&#34;]);
    }
}
/// &lt;summary&gt;
/// get a new token
/// &lt;/summary&gt;
/// &lt;param name=&#34;inPutViewModel&#34;&gt;old access token and refreshtoken key&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
[AllowAnonymous]
[HttpPost(&#34;/refresh-token&#34;)]
public async Task&lt;ResultModel&lt;string&gt;&gt; RefreshToken([FromBody] RefreshTokenInPutViewModel inPutViewModel)
{
    var currentUser = this._tokenManager.GetCurrentUser(inPutViewModel.AccessToken);

    var flag = _cacheManager.Is_Token_Exist&lt;string&gt;(currentUser.user_id, &#34;WebRefreshToken&#34;, _tokenManager.GetRefreshTokenExpireMinute());
    if (!flag)
    {
        return ResultModel&lt;string&gt;.Error(&#34;refreshtoken_failure&#34;);
    }
    else
    {
        var result = _tokenManager.GenerateToken(currentUser);
        return ResultModel&lt;string&gt;.Success(result.token);
    }

}
#endregion
#region hello world
[AllowAnonymous]
[HttpPost(&#34;/hello-world&#34;)]
public ResultModel&lt;string&gt; hello_world()
{
    return ResultModel&lt;string&gt;.Success(_accountService.HelloWorld());
}
#endregion

}