可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I want to protect ASP.NET Core Web API using JWT. Additionally, I would like to have an option of using roles from tokens payload directly in controller actions attributes.
Now, while I did find it out how to use it with Policies:
Authorize(Policy="CheckIfUserIsOfRoleX") ControllerAction()...
I would like better to have an option to use something usual like:
Authorize(Role="RoleX")
where Role would be automatically mapped from JWT payload.
{ name: "somename", roles: ["RoleX", "RoleY", "RoleZ"] }
So, what is the easiest way to accomplish this in ASP.NET Core? Is there a way to get this working automatically through some settings/mappings (if so, where to set it?) or should I, after token is validated, intercept generation of ClaimsIdentity
and add roles claims manually (if so, where/how to do that?)?
回答1:
Sample - ASP.NET Core JWT
Consider this is the payload.
{ name:"somename", roles:["RoleX", "RoleY", "RoleZ"] }
JWT MiddleWare
public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { var keyAsBytes = Encoding.ASCII.GetBytes("mysuperdupersecret"); var options = new JwtBearerOptions { TokenValidationParameters = { IssuerSigningKey = new SymmetricSecurityKey(keyAsBytes) } }; app.UseJwtBearerAuthentication(options); app.UseMvc(); } }
When I make a request to my API with the JWT created above, the array of roles in the roles
claim in the JWT will automatically be added as claims with the type http://schemas.microsoft.com/ws/2008/06/identity/claims/role
to my ClaimsIdentity.
You can test this by creating the following simple API method that returns the user’s claims:
public class ValuesController : Controller { [Authorize] [HttpGet("claims")] public object Claims() { return User.Claims.Select(c => new { Type = c.Type, Value = c.Value }); } }
So when I make a call to the /claims
endpoint above, and pass the JWT generated before, I will get the following JSON returned:
[ { "type": "name", "value": "someone" }, { "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "value": "RoleX" }, { "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "value": "RoleY" }, { "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "value": "RoleZ" } ]
Where this gets really interesting is when you consider that passing Roles to the [Authorize]
will actually look whether there is a claim of type http://schemas.microsoft.com/ws/2008/06/identity/claims/role
with the value of the role(s) you are authorizing.
This means that I can simply add [Authorize(Roles = "Admin")]
to any API method, and that will ensure that only JWTs where the payload contains the claim roles
containing the value of Admin in the array of roles will be authorized for that API method.
public class ValuesController : Controller { [Authorize(Roles = "Admin")] [HttpGet("ping/admin")] public string PingAdmin() { return "Pong"; } }
Now simply decorate the MVC controllers with [Authorize(Roles = "Admin")]
and only users whose ID Token contains those claims will be authorized.
Ensure the roles
claim of your JWT contains an array of roles assigned to the user, and you can use [Authorize(Roles = "???")]
in your controllers. It all works seamlessly.
回答2:
You need get valid claims when generating JWT. Here is example code:
Login logic:
[HttpPost] [AllowAnonymous] public async Task Login([FromBody] ApplicationUser applicationUser) { var result = await _signInManager.PasswordSignInAsync(applicationUser.UserName, applicationUser.Password, true, false); if(result.Succeeded) { var user = await _userManager.FindByNameAsync(applicationUser.UserName); // Get valid claims and pass them into JWT var claims = await GetValidClaims(user); // Create the JWT security token and encode it. var jwt = new JwtSecurityToken( issuer: _jwtOptions.Issuer, audience: _jwtOptions.Audience, claims: claims, notBefore: _jwtOptions.NotBefore, expires: _jwtOptions.Expiration, signingCredentials: _jwtOptions.SigningCredentials); //... } else { throw new ApiException('Wrong username or password', 403); } }
Get user claims based UserRoles
, RoleClaims
and UserClaims
tables (ASP.NET Identity):
private async Task> GetValidClaims(ApplicationUser user) { IdentityOptions _options = new IdentityOptions(); var claims = new List { new Claim(JwtRegisteredClaimNames.Sub, user.UserName), new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()), new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64), new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()), new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName) }; var userClaims = await _userManager.GetClaimsAsync(user); var userRoles = await _userManager.GetRolesAsync(user); claims.AddRange(userClaims); foreach (var userRole in userRoles) { claims.Add(new Claim(ClaimTypes.Role, userRole)); var role = await _roleManager.FindByNameAsync(userRole); if(role != null) { var roleClaims = await _roleManager.GetClaimsAsync(role); foreach(Claim roleClaim in roleClaims) { claims.Add(roleClaim); } } } return claims; }
In Startup.cs
please add needed policies into authorization:
void ConfigureServices(IServiceCollection service) { services.AddAuthorization(options => { // Here I stored necessary permissions/roles in a constant foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) { options.AddPolicy(prop.GetValue(null).ToString(), policy => policy.RequireClaim(ClaimType.Permission, prop.GetValue(null).ToString())); } }); }
I am a beginner in ASP.NET, so please let me know if you have better solutions.
And, I don't know how worst when I put all claims/permissions into JWT. Too long? Performance ? Should I store generated JWT in database and check it later for getting valid user's roles/claims?
回答3:
For generating JWT Tokens we'll need AuthJwtTokenOptions
helper class
public static class AuthJwtTokenOptions { public const string Issuer = "SomeIssuesName"; public const string Audience = "https://awesome-website.com/"; private const string Key = "supersecret_secretkey!12345"; public static SecurityKey GetSecurityKey() => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Key)); }
Account controller code:
[HttpPost] public IActionResult GetToken([FromBody]Credentials credentials) { // TODO: Add here some input values validations User user = _userRepository.GetUser(credentials.Email, credentials.Password); if (user == null) return BadRequest(); ClaimsIdentity identity = GetClaimsIdentity(user); return Ok(new AuthenticatedUserInfoJsonModel { UserId = user.Id, Email = user.Email, FullName = user.FullName, Token = GetJwtToken(identity) }); } private ClaimsIdentity GetClaimsIdentity(User user) { // Here we can save some values to token. // For example we are storing here user id and email Claim[] claims = new[] { new Claim(ClaimTypes.Name, user.Id.ToString()), new Claim(ClaimTypes.Email, user.Email) }; ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token"); // Adding roles code // Roles property is string collection but you can modify Select code if it it's not claimsIdentity.AddClaims(user.Roles.Select(role => new Claim(ClaimTypes.Role, role))); return claimsIdentity; } private string GetJwtToken(ClaimsIdentity identity) { JwtSecurityToken jwtSecurityToken = new JwtSecurityToken( issuer: AuthJwtTokenOptions.Issuer, audience: AuthJwtTokenOptions.Audience, notBefore: DateTime.UtcNow, claims: identity.Claims, // our token will live 1 hour, but you can change you token lifetime here expires: DateTime.UtcNow.Add(TimeSpan.FromHours(1)), signingCredentials: new SigningCredentials(AuthJwtTokenOptions.GetSecurityKey(), SecurityAlgorithms.HmacSha256)); return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); }
In Startup.cs
add following code to ConfigureServices(IServiceCollection services)
method before services.AddMvc
call:
public void ConfigureServices(IServiceCollection services) { // Other code here… services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = AuthJwtTokenOptions.Issuer, ValidateAudience = true, ValidAudience = AuthJwtTokenOptions.Audience, ValidateLifetime = true, IssuerSigningKey = AuthJwtTokenOptions.GetSecurityKey(), ValidateIssuerSigningKey = true }; }); // Other code here… services.AddMvc(); }
Also add app.UseAuthentication()
call to ConfigureMethod
of Startup.cs
before app.UseMvc
call.
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // Other code here… app.UseAuthentication(); app.UseMvc(); }
Now you can use [Authorize(Roles = "Some_role")]
attributes.
To get user id and email in any controller you should do it like this
int userId = int.Parse(HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value); string email = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value;
Also userId
can be retrived this way (this is due to claim type name ClaimTypes.Name
)
int userId = int.Parse(HttpContext.User.Identity.Name);
It's better to move such code to some controller extension helpers:
public static class ControllerExtensions { public static int GetUserId(this Controller controller) => int.Parse(controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value); public static string GetCurrentUserEmail(this Controller controller) => controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value; }
The same is true for any other Claim
you've added. You should just specify valid key.