Asp.Net Core JWT

## 1.Asp.Net Core JWT

1.1 概念

(1)session

  • 对于分布式集群环境,Session数据保存在服务器内存中就不合适了,应该放到一个中心状态服务器上。ASP.NET Core支持Session采用Redis、Memcached。
  • 中心状态服务器有性能问题。

(2)JWT(Json Web Token)

  • JWT把登录信息(也称作令牌)保存在客户端。
  • 为了防止客户端的数据造假,保存在客户端的令牌经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器端收到客户端提交过来的令牌的时候都要检查一下签名。

(3)JWT组成

  • 头部(Header)

  • 负载(Payload)

  • 签名(Signature)

1.2 生成JWT临牌

(1)Install-Package System.IdentityModel.Tokens.Jwt

(2)生成JWT令牌

image-20220310152146314

using System.Security.Claims;
using System.Text;

//身份信息
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, "1"));
claims.Add(new Claim(ClaimTypes.Name, "peng"));
claims.Add(new Claim(ClaimTypes.Role, "User"));
claims.Add(new Claim(ClaimTypes.Role, "Admin"));
claims.Add(new Claim("zidingyi", "peng xiao shuai"));
string key = "asdasdasdasdasd13123!#@!";
//设置过期时间
DateTime expires = DateTime.Now.AddDays(1);
byte[] secBytes = Encoding.UTF8.GetBytes(key);
var secKey = new SymmetricSecurityKey(secBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new JwtSecurityToken(claims: claims,
    expires: expires, signingCredentials: credentials);
string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);

Console.WriteLine(jwt);

(3)解码

image-20220310152515757

Console.WriteLine("解码JWT");
string[] segments = jwt.Split('.');
string head = JwtDecode(segments[0]);
string payload = JwtDecode(segments[1]);
Console.WriteLine("---head---");
Console.WriteLine(head);
Console.WriteLine("---payload---");
Console.WriteLine(payload);

string JwtDecode(string s)
{
	s = s.Replace('-', '+').Replace('_', '/');
	switch (s.Length % 4)
	{
		case 2:
			s += "==";
			break;
		case 3:
			s += "=";
			break;
	}
	var bytes = Convert.FromBase64String(s);
	return Encoding.UTF8.GetString(bytes);
}

image-20220310152609056

  • 负载中的内容是明文形式保存的;

  • 不要把不能被客户端知道的信息放到JWT中;

(4)JwtSecurityTokenHandler

image-20220310153243516

Console.WriteLine("====================");
Console.WriteLine("JwtSecurityTokenHandler对JWT解码");
JwtSecurityTokenHandler tokenHandler = new();
TokenValidationParameters valParam = new();
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
valParam.IssuerSigningKey = securityKey;
valParam.ValidateIssuer = false;
valParam.ValidateAudience = false;
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt,
			valParam, out SecurityToken secToken);
foreach (var claim in claimsPrincipal.Claims)
{
	Console.WriteLine($"{claim.Type}={claim.Value}");
}

1.3 Asp.Net Core使用JWT

(1)创建JWT配置类

image-20220310155446865

public class JWTOptions
{
    public string SigningKey { get; set; }
    public int ExpireSeconds { get; set; }
}

(2)安装 Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

(3)JWT配置

image-20220310155622105

builder.Services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
	var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>();
	byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
	var secKey = new SymmetricSecurityKey(keyBytes);
	x.TokenValidationParameters = new()
	{
		ValidateIssuer = false,
		ValidateAudience = false,
		ValidateLifetime = true,
		ValidateIssuerSigningKey = true,
		IssuerSigningKey = secKey
	};
});

(4)app.UseAuthorization()

image-20220310155738681

(5)通过Controller获取JWT Token

 [Route("api/[controller]/[action]")]
    [ApiController]
    public class LoginController : ControllerBase
    {
        [HttpPost]
        public ActionResult<string> GetJWTToken(LoginRequest req,[FromServices] IOptions<JWTOptions> jwtOptions)
        {
            var claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.NameIdentifier,"1"));
            claims.Add(new Claim(ClaimTypes.Name, req.UserName));
            claims.Add(new Claim(ClaimTypes.Role,"Admin"));                    
            string jwtToken = BuildToken(claims, jwtOptions.Value);
            return Ok(jwtToken);
        }

        private static string BuildToken(IEnumerable<Claim> claims, JWTOptions options)
        {
            DateTime expires = DateTime.Now.AddSeconds(options.ExpireSeconds);
            byte[] keyBytes = Encoding.UTF8.GetBytes(options.SigningKey);
            var secKey = new SymmetricSecurityKey(keyBytes);
            var credentials = new SigningCredentials(secKey,
                SecurityAlgorithms.HmacSha256Signature);
            var tokenDescriptor = new JwtSecurityToken(expires: expires,
                signingCredentials: credentials, claims: claims);
            return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
        }
    }

    public record LoginRequest(string UserName,string Password);

(6)测试

[Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]

    public class ValuesController : ControllerBase
    {
        [HttpGet]
        public IActionResult Hello1()
        {
            string id = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            string userName = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            IEnumerable<Claim> roleClaims = this.User.FindAll(ClaimTypes.Role);
            string roleNames = string.Join(',', roleClaims.Select(c => c.Value));
            return Ok($"id={id},userName={userName},roleNames ={roleNames}");
        }

        
        [HttpGet]
        [Authorize(Roles = "Admin")]
        public IActionResult Hello2()
        {
            string id = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            string userName = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            IEnumerable<Claim> roleClaims = this.User.FindAll(ClaimTypes.Role);
            string roleNames = string.Join(',', roleClaims.Select(c => c.Value));
            return Ok($"id={id},userName={userName},roleNames ={roleNames}");
        }

        [HttpGet]
        [AllowAnonymous]
        public IActionResult Hello3()
        {
            return Ok("OK");
        }

    }

获取Token

image-20220310160843558

访问Hello2

  • ValuesController需要权限认证,Hello2需要Admin角色才能访问

  • Authorization的值为“Bearer JWTToken”, Authorization的值中的“Bearer”和JWT令牌之间一定要通过空格分隔。前后不能多出来额外的空格、换行等。

image-20220310160906019

1.4 在Swagger中设置JWT

(1)设置AddSwaggerGen

builder.Services.AddSwaggerGen(c =>
{
    var scheme = new OpenApiSecurityScheme()
    {
        Description = "Authorization header. \r\nExample: 'Bearer 123456'",
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "Authorization"
        },
        Scheme = "oauth2",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
    };
    c.AddSecurityDefinition("Authorization", scheme);
    var requirement = new OpenApiSecurityRequirement();
    requirement[scheme] = new List<string>();
    c.AddSecurityRequirement(requirement);
});

(2)测试

设置Token

image-20220310161729079

访问权限的接口

image-20220310161859311

1.5 解决JWT提前撤回

(1)JWT的缺点

  • 到期前,令牌无法提前撤回
  • 如果需要在JWT中实现,思路:用Redis保存状态,或者用refresh_token+access_token机制等

(2)获取token的时候生成guid,保存在payload中

image-20220310193246677

(3)通过ActionFilter过滤每次请求,当执行禁用用户、撤回用户的令牌等操作的时候,把这个用户对应的JWTVersion列的值重新生成guid,每次请求经过ActionFilter比较guid,当guid不一应的时候就意味着失效

public class JWTValidationFilter : IAsyncActionFilter
    {
        private IMemoryCache memCache;
        private UserManager<User> userMgr;

        public JWTValidationFilter(IMemoryCache memCache, UserManager<User> userMgr)
        {
            this.memCache = memCache;
            this.userMgr = userMgr;
        }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var claimUserId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
        //对于登录接口等没有登录的,直接跳过
        if (claimUserId == null)
        {
            await next();
            return;
        }
        long userId = long.Parse(claimUserId!.Value);
        string cacheKey = $"JWTValidationFilter.UserInfo.{userId}";
        User user = await memCache.GetOrCreateAsync(cacheKey, async e =>
        {
            e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5);
            return await userMgr.FindByIdAsync(userId.ToString());
        });
        if (user == null)
        {
            var result = new ObjectResult($"UserId({userId}) not found");
            result.StatusCode = (int)HttpStatusCode.Unauthorized;
            context.Result = result;
            return;
        }
        var claimVersion = context.HttpContext.User.FindFirst(ClaimTypes.Version);
        //jwt中保存的版本号
        //long jwtVerOfReq = long.Parse(claimVersion!.Value);
        string jwtVerOfReq = claimVersion!.Value;
        //由于内存缓存等导致的并发问题,
        //假如集群的A服务器中缓存保存的还是版本为5的数据,但客户端提交过来的可能已经是版本号为6的数据。因此只要是客户端提交的版本号>=服务器上取出来(可能是从Db,也可能是从缓存)的版本号,那么也是可以的
        if (jwtVerOfReq != user.JWTVersion)
        {
            await next();
        }
        else
        {
            var result = new ObjectResult($"JWTVersion mismatch");
            result.StatusCode = (int)HttpStatusCode.Unauthorized;
            context.Result = result;
            return;
        }
    }
}
posted @ 2022-04-08 15:58  peng_boke  阅读(557)  评论(0编辑  收藏  举报