首页 > 技术文章 > .Net Core 基于JWT签发Token

roubaozidd 2022-03-27 14:07 原文

 

如果不了解JWT可以先了解这篇文章 。 这里主要是来记录一下怎样使用Jwt 自己来签发和刷新Token,很多地方不符合实际使用,只是为了在这里测试达到效果,正式使用根据实际情况修改代码

1. 添加Nuget引用

Microsoft.AspNetCore.Authentication.JwtBeare
System.IdentityModel.Tokens.Jwt

2. 添加简单封装的工具类

public class JwtHelper
    { 
        public IConfiguration _configuration { get; set; }
        public JwtHelper(IConfiguration configuration)
        {
            _configuration = configuration;
        }
        /// <summary>
        /// 生成AccessToken
        /// </summary>
        /// <param name="username">这里测试用的是用户信息,可以传入其他信息</param>
        /// <returns></returns>
        public string GenerateAccessToken(string username)
        {
            var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
            string issuer = _configuration.GetSection("JwtConfig:Issuer").Value;
            // 获取SecurityKey
            string securityKey = _configuration.GetSection("JwtConfig:SecurityKey").Value;
            //------------生成AccessToken----------------------------------
            // token中的claims用于储存自定义信息,如登录之后的用户id等
            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub,username),
                new Claim(ClaimTypes.Role,"admin")
            };
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
            //生成Token两种方式
            //方式一
            //var tokenDescriptor = new SecurityTokenDescriptor
            //{
            //    Issuer = issuer,
            //    Audience = "testClient", 
            //    NotBefore = DateTime.Now, // 预设值就是 DateTime.Now
            //    IssuedAt = DateTime.Now, // 预设值就是 DateTime.Now
            //    Subject = new ClaimsIdentity(claims),
            //    Expires = DateTime.Now.AddMinutes(30),
            //    SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
            //};
            //var securityToken = jwtSecurityTokenHandler.CreateToken(tokenDescriptor);
            //var serializeToken = jwtSecurityTokenHandler.WriteToken(securityToken);
            //方式二
            var token = new JwtSecurityToken(
                issuer: issuer,                    // 发布者
                audience: "testClient",                // 接收者
                notBefore: DateTime.Now,                                                          // token签发时间
                expires: DateTime.Now.AddMinutes(30),                                             // token过期时间
                claims: claims,                                                                   // 该token内存储的自定义字段信息
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)    // 用于签发token的秘钥算法
            );
            return jwtSecurityTokenHandler.WriteToken(token);
        }
        /// <summary>
        /// 生成RefreshToken
        /// </summary>
        /// <returns></returns>
        public string GenerateRefreshToken()
        {
            string issuer = _configuration.GetSection("JwtConfig:Issuer").Value;
            // 获取SecurityKey
            string securityKey = _configuration.GetSection("JwtConfig:SecurityKey").Value;
            var refClaims = new[]
            {
               new Claim("role","refresh")
            };
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
            var refreshToken = new JwtSecurityToken(
                issuer: issuer,                    // 发布者
                audience: "testClient",                // 接收者
                notBefore: DateTime.Now,                                                          // token签发时间
                expires: DateTime.Now.AddDays(7),                                             // token过期时间
                claims: refClaims,                                                                   // 该token内存储的自定义字段信息
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)    // 用于签发token的秘钥算法
            );

            // 返回成功信息,写出token
            return new JwtSecurityTokenHandler().WriteToken(refreshToken);
        }
        /// <summary>
        /// 刷新accessToken
        /// </summary>
        /// <param name="accessToken">过期的accessToken</param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public string RefreshToken(string accessToken)
        {
            string issuer = _configuration.GetSection("JwtConfig:Issuer").Value;
            // 获取SecurityKey
            string securityKey = _configuration.GetSection("JwtConfig:SecurityKey").Value;
            var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
            bool isCan = jwtSecurityTokenHandler.CanReadToken(accessToken);//验证Token格式
            if (!isCan)
                throw new Exception("传入访问令牌格式错误");
            //var jwtToken = jwtSecurityTokenHandler.ReadJwtToken(refreshtoken);//转换类型为token,不用这一行
            var validateParameter = new TokenValidationParameters()//验证参数
            {
                ValidateAudience = true,
                // 验证发布者
                ValidateIssuer = true,
                // 验证过期时间
                ValidateLifetime = false,
                // 验证秘钥
                ValidateIssuerSigningKey = true,
                // 读配置Issure
                ValidIssuer = issuer,
                // 读配置Audience
                ValidAudience = "testClient",
                // 设置生成token的秘钥
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey))
            };

            //验证传入的过期的AccessToken
            SecurityToken validatedToken = null;
            try
            {
                jwtSecurityTokenHandler.ValidateToken(accessToken, validateParameter, out validatedToken);//微软提供的验证方法。那个out传出的参数,类型是是个抽象类,记得转换
            }
            catch (SecurityTokenException)
            {
                throw new Exception("传入AccessToken被修改");
            }
            // 获取SecurityKey
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
            var jwtToken = validatedToken as JwtSecurityToken;//转换一下
            var accClaims = jwtToken.Claims;
            var access_Token = new JwtSecurityToken(
                    issuer: "fcb",                    // 发布者
                                                      //audience: "myClient",                // 接收者
                    notBefore: DateTime.Now,                                                          // token签发时间
                    expires: DateTime.Now.AddMinutes(30),                                             // token过期时间
                    claims: accClaims,                                                                   // 该token内存储的自定义字段信息
                    signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)    // 用于签发token的秘钥算法
                );
            // 返回成功信息,写出token
            return new JwtSecurityTokenHandler().WriteToken(access_Token);
        }
    }

3.  修改Program.cs 

(这里多设置了swagger,方便测试,如果实际情况不需要swagger进行测试可以去掉 AddSwaggerGen 中参数配置)

builder.Services.AddSwaggerGen(c => {
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
    {
        Description = "在下框中输入请求头中需要添加Jwt授权Token:Bearer Token",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] { }
        }
    });

});
builder.Services.AddScoped<JwtHelper>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        // 当验证失败时,表头WWW-Authenticate会返回失败原因 
        options.IncludeErrorDetails = true; 
     //配置Token的验证 options.TokenValidationParameters = new TokenValidationParameters { // 可以从 "sub" 取值并设定給 User.Identity.Name NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", // 可以从 "roles" 取值,并可以从 [Authorize] 设置角色 RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", // 一般我们都要验证 Issuer ValidateIssuer = true, ValidIssuer = builder.Configuration.GetValue<string>("JwtConfig:Issuer"), // 通常不太需要验证 Audience ValidateAudience = false, //ValidAudience = "JwtAuthDemo", // 不验证就不需要 // 一般我们都会验证 Token 的有效期 ValidateLifetime = true, // 如果 Token 中包含 key 才需要验证,一般都只有前面而已 ValidateIssuerSigningKey = false, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetValue<string>("JwtConfig:SecurityKey"))) }; });

添加认证管道,系统里面已经存在  UseAuthorization  ,这里 在 UseAuthorization   之前添加  UseAuthentication

app.UseAuthentication();
app.UseAuthorization();

 

4.  添加控制器

这里刷新Token的接口限制了  [Authorize(Roles = "refresh")]  ,只有 refreshToken 才有相应的角色,所以 需要换成  refreshToken ,并且传参之前过期的accessToken,目的主要是拿取token中的claim信息,方便生成新的accessToken重新写入进去, 当前也可以特别处理refreashToken,而取消传入失效的accessToken,我这里没有试过,理论上是可以的。这里还存在一个问题就是可以通过refreshToken 去请求 其他 需要的 accessToken 验证的接口 ,所以可以给相应的接口新增一些限制,只能通过 accessToken 去请求,比如下面的接口 Test2 限制了  [Authorize(Roles = "admin")]   ,accessToken 才有admin的角色权限,只能通过accessToken 去请求 

    [Route("api/[controller]")]
    [ApiController]
    public class AccessController : ControllerBase
    {
        private JwtHelper _jwtHelper;
        public AccessController(JwtHelper jwtHelper)
        {
            _jwtHelper = jwtHelper;
        }
        [Authorize]
        [HttpGet("test")]
        public ActionResult Test()
        {
            return Ok(HttpContext.User.Claims.Count());
        }
        [Authorize(Roles = "admin")]
        [HttpGet("test2")]
        public ActionResult Test2()
        {
            return Ok(HttpContext.User.Claims.Count());
        }
        [HttpGet("login")]
        public ActionResult Login(string username, string password)
        {
            //校验账号密码,这里省略
            if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
            {
                var access_token= _jwtHelper.GenerateAccessToken(username);
                var refresh_token = _jwtHelper.GenerateRefreshToken();
                // 返回成功信息,写出token
                return Ok(new { code = 200, message = "登录成功", accessToken = access_token, refreshToken = refresh_token });
            }
            // 返回错误请求信息
            return BadRequest(new { code = 400, message = "登录失败,用户名或密码为空" });
        }
        //此方法用来刷新令牌,逻辑是验证refToken才能进入方法,进入后验证accessToken除了过期时间项的其他所有项,目的是防止用户修改权限等
        [HttpGet("refresh")]
        [Authorize(Roles = "refresh")]//验证权限
        public ActionResult Refresh(string accessToken)
        {
            var newAccessToken = _jwtHelper.RefreshToken(accessToken);
            //重新生成refeashToken
            var refresh_token = _jwtHelper.GenerateRefreshToken();

            // 返回成功信息,写出token
            return Ok(new
            {
                code = 200,
                message = "令牌刷新成功",
                refreshToken = refresh_token,
                accessToken = newAccessToken
            });
        }
    }

 

5. 运行一下

 

 

 

获取到accessToken之后授权swagger (Bearer+" " + accessToken )

 

 

 

 

 

再请求一下刷新Token的接口

 

文章参考文档:

https://docs.microsoft.com/en-us/dotnet/api/system.identitymodel.tokens.jwt.jwtsecuritytokenhandler?view=azure-dotnet(官网)

https://www.cnblogs.com/zxy001126/p/15530864.html

https://www.cnblogs.com/hot-tofu-curd/p/15115844.html

推荐阅读