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令牌
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)解码
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);
}
-
负载中的内容是明文形式保存的;
-
不要把不能被客户端知道的信息放到JWT中;
(4)JwtSecurityTokenHandler
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配置类
public class JWTOptions
{
public string SigningKey { get; set; }
public int ExpireSeconds { get; set; }
}
(2)安装 Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
(3)JWT配置
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()
(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
访问Hello2
-
ValuesController需要权限认证,Hello2需要Admin角色才能访问
-
Authorization的值为“Bearer JWTToken”, Authorization的值中的“Bearer”和JWT令牌之间一定要通过空格分隔。前后不能多出来额外的空格、换行等。
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
访问权限的接口
1.5 解决JWT提前撤回
(1)JWT的缺点
- 到期前,令牌无法提前撤回
- 如果需要在JWT中实现,思路:用Redis保存状态,或者用refresh_token+access_token机制等
(2)获取token的时候生成guid,保存在payload中
(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;
}
}
}