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

举报
愚公搬代码 发表于 2023/02/28 22:48:56 2023/02/28
【摘要】 前言JWT(Json Web Token)是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准( RFC 7519 ),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。JWT一般由三段构成,用.号分隔开,第一段是head...

前言

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
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。