preface

In the development of back-end projects, how to control user permissions is the first consideration. In the actual development process, we may use some mature solutions to help us achieve this function. In the project estadore. VuCore, I will use Jwt to achieve user permission control. In this chapter, I will demonstrate how to implement user authorization and authentication using Jwt.

ASP.NET Core project

Storage address: github.com/Lanesra712/…

Step by Step

First, some concepts

Jwt(JSON Web Token) is a stateless authorization token based on JSON. As Jwt is a standard data transmission specification, it is not a technical specification unique to any one company, so it is very suitable for building single sign-on services. Provide authorization services for web, client, APP and other interface users.

In the process of using Jwt for permission control, we need to first request the authorization server to obtain the token and store the token locally on the client (in the case of web project, we can store the token in localstorage or cookie), then, For each request from the server, you need to add the obtained token information to the HTTP request header.

$.ajax({
    url: url,
    method: "POST".data: JSON.stringify(data),
    beforeSend: function (xhr) {
        /* Authorization header */
        xhr.setRequestHeader("Authorization"."Bearer " + token);
    },
    success: function (data) {}});Copy the code

Does the user have access to all functions of the system once they have the token? Of course the answer is no. For a system, there may be multiple user roles, and each user role has different access to resources. Therefore, after the user has the token, we also need to identify the user role, so as to further control the user’s permissions.

In estadual.VuCore, I adopted policy-based authorization. I defined an authorization policy to improve Jwt authentication, and then injected this custom policy into IServiceCollection container to further improve permission control. In this way, the user access rights can be controlled.

Policy-based authorization is a new form of authorization that Microsoft has added to ASP.NET Core by defining one or more requirements for a policy. Will the custom authorization policies in Startup. ConfigureServices method as part of the authorization service configuration register after can be in accordance with our policy for permissions control handler.

services.AddAuthorization(options =>
{
    options.AddPolicy("Permission",
       policy => policy.Requirements.Add(new PolicyRequirement()));
})

services.AddSingleton<IAuthorizationHandler, PolicyHandler>();
Copy the code

As I do in later code, I define an authorization policy called Permission, which contains an authentication requirement called PolicyRequirement. After implementing the authorization policy, By injecting the authentication method PolicyHandler based on this requirement into the service collection as an AddSingleton, we can add attributes to the controllers that need to be authenticated.

[Authorize(Policy = "Permission")]
public class SecretController : ControllerBase
{}
Copy the code

Second, authorization,

In estadore. VuCore, the location of the code related to authorization has been marked in the following figure. ASP.NET Core: ASP.NET Core Web API and ue. Js to build the front and back end separate framework) introduced the whole project framework, saying that estad. Application is the Application layer of the project, which, as the name implies, is a class library for realizing the actual business functions in our project. Therefore, our relevant business code for implementing Jwt should be in this layer. Meanwhile, as Microsoft’s JwtBearer components are used for token issuance and authentication of Jwt, we need to add references to estado. Application through Nuget before using it.

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Install-Package System.IdentityModel.Tokens.Jwt
Copy the code

Each subapplication folder (Jwt, Secret) contains the same structure: Dto data transfer objects, functional interfaces, and implementation classes for functional interfaces, where interfaces are inherited in a single inheritance manner.

Public interface IJwtAppService {/// <summary> // </summary> // <param name=" dTO ">  /// <returns></returns> JwtAuthorizationDto Create(UserDto dto); / / / < summary > / / / refresh Token / / / < summary > / / / < param name = "Token" > Token < param > / / / < param name = "dto" > user information data transfer objects < param >  /// <returns></returns> Task<JwtAuthorizationDto> RefreshAsync(string token, UserDto dto); / / / < summary > / effective / / / / / whether the current Token < summary > / / / < returns > < / returns > Task < bool > IsCurrentActiveTokenAsync (); /// <summary> // DeactivateCurrentAsync(); /// </summary> // </returns> </returns> Task DeactivateCurrentAsync(); /// </summary> // <param name=" Token ">Token</param> /// < RETURNS ></ RETURNS > Task<bool> IsActiveAsync(string token); /// </summary> // </returns> Task DeactivateAsync(String Token); /// </summary> </returns> </returns> Task DeactivateAsync(string Token); }Copy the code

JwtAuthorizationDto is a token information transfer object, which contains the token information created by us and is used to return the token information to the foreground for use. The UserDto is the data transfer object when the user logs in to obtain the token and receives the parameter values during login.

When creating a token or verifying a token, information such as the issuer and receiver of the token can be called from multiple places. I have stored such information in the configuration file. When we need to use it later, This is achieved by injecting an IConfiguration. The Jwt configuration file contains four main items: the issuer of the token, the recipient of the token, the key value of the encrypted token, and the expiration time of the token, which you can adjust according to your own needs.

"Jwt": {
    "Issuer": "yuiter.com",
    "Audience": "yuiter.com",
    "SecurityKey": "a48fafeefd334237c2ca207e842afe0b",
    "ExpireMinutes": "20"
}
Copy the code

The token creation process can be easily broken down into three parts: create a token based on configuration information and user information, write the encrypted user information to the HttpContext context, and add the created token information to the static HashSet collection.

In the whole life cycle of token creation and verification, Scheme, Claim, ClaimsIdentity and ClaimsPrincipal are involved. If you have used Microsoft Identity authentication before, you will be familiar with these terms. For those of you who have never used Identity before, let me briefly introduce the meanings of these nouns.

The Scheme mode, which is separate from the rest of the term, mainly indicates how we are authorized. For example, you have authorised it as a cookie or as an OpenId or as we have done here with Jwt Bearer.

Take our real life as an example, we all have an id card, which contains our name, gender, ethnicity, date of birth, home address, and ID number. Each item of data can be regarded as a type-value, for example, name: Zhang SAN. Each item of information on the ID card is our Claim declaration, name: Zhang SAN, is a Claim; Gender: male, also a Claim. For ClaimsIdentity, just as this item’s information eventually forms our IDENTITY card, this item’s Claim eventually forms our ClaimsIdentity. ClaimsPrincipal is the owner of the ClaimsIdentity, just like we have an ID card.

Claim = ClaimsIdentity = ClaimsPrincipal Where a ClaimsIdentity can contain multiple Claims, and a ClaimsPrincipal can contain multiple ClaimSidEntities.

If you want to learn more about ASP.NET Core’s authorization policy, you can check out this article on the blog: “ASP.NET Core operating principle anatomy [5]:Authentication, ASP.NET Core Authentication = Introduction to Authentication with ASP.NET Core

The final code implementation for token generation is shown below. As you can see, when creating the ClaimsIdentity “id” information, I add the user’s role information and write the encrypted user information to the HttpContext. We will get the user’s role information from HttpContext later in the validation process to determine whether the user can access the currently requested address.

/// <summary> // new Token /// </summary> // <param name=" DTO "> User information data transmission object </param> // <returns></ RETURNS > public JwtAuthorizationDto Create(UserDto dto) { JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecurityKey"])); DateTime authTime = DateTime.UtcNow; DateTime expiresAt = authTime.AddMinutes(Convert.ToDouble(_configuration["Jwt:ExpireMinutes"])); / / add user information to the Claim of var identity = new ClaimsIdentity (JwtBearerDefaults. AuthenticationScheme); IEnumerable<Claim> claims = new Claim[] { new Claim(ClaimTypes.Name,dto.UserName), new Claim(ClaimTypes.Role,dto.Role.ToString()), new Claim(ClaimTypes.Email,dto.Email), new Claim(ClaimTypes.Expiration,expiresAt.ToString()) }; identity.AddClaims(claims); // Issue an encrypted user information credential, Used to identify the identity of the user _httpContextAccessor. HttpContext. SignInAsync (JwtBearerDefaults AuthenticationScheme, new ClaimsPrincipal(identity)); Var tokenDescriptor = new SecurityTokenDescriptor {Subject = new ClaimsIdentity(Claims),// Issuer = to create statement information _Configuration ["Jwt:Issuer"],//Jwt token Issuer Audience = _Configuration ["Jwt:Audience"],//Jwt token receiver Expires = ExpiresAt, / / expiration time SigningCredentials = new SigningCredentials (key, SecurityAlgorithms HmacSha256) / / create a token}; var token = tokenHandler.CreateToken(tokenDescriptor); Var JWT = new JwtAuthorizationDto {UserId = to.Id, Token = tokenHandler.WriteToken(Token), Auths = new DateTimeOffset(authTime).ToUnixTimeSeconds(), Expires = new DateTimeOffset(expiresAt).ToUnixTimeSeconds(), Success = true }; _tokens.Add(jwt); return jwt; }Copy the code

After the token is created, the client can add the token information to the Http request header to access the protected resource. However, in some cases, for example, after the user changes the password, the current token information may not be expired, but we cannot allow the user to use the current token information to access the interface. In this case, the token information will be disabled and refreshed.

In this case, when we disable token information, we add the disabled token information to Redis cache, and then we can judge whether the token exists in Redis when the user requests it.

You can also delete the token in the HashSet when you disable the token of the current user, and then determine whether the token is valid by checking whether the token is in the HashSet.

There are many ways, depending on your own needs.

For Redis read and write operations, I use Microsoft Redis components, you can modify as you like. If you’re like me, you’ll need to add a reference to Microsoft’s Distributed Cache Abstract INTERFACE DLL in estadore. Application via Nuget, and Microsoft’s Redis implementation in estadore. WebApi project.

Install-Package Microsoft.Extensions.Caching.Abstractions ## Distributed cache Abstract interface
Install-Package Microsoft.Extensions.Caching.Redis # # Redis implementation
Copy the code

When we deactivate the token, we obtain the TOKEN information in the HTTP Header through the HttpContext and store the token information in the Redis cache. In this way, we complete the deactivation of the token.

public class JwtAppService : IJwtAppService {/// <summary> // disable Token /// </summary> // <param name=" Token ">Token</param> /// <returns></returns>  public async Task DeactivateAsync(string token) => await _cache.SetStringAsync(GetKey(token), " ", new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(Convert.ToDouble(_configuration["Jwt:ExpireMinutes"])) }); /// </summary> // </summary> // <returns> public Async Task DeactivateCurrentAsync() => await DeactivateAsync(GetCurrentAsync()); // </summary> // <param name=" Token ">Token</param> // <returns></returns> private static string GetKey(string token) => $"deactivated token:{token}"; /// <summary> // obtain the Token value of the HTTP request /// </summary> /// <returns></ RETURNS > private String GetCurrentAsync() {// HTTP header var authorizationHeader = _httpContextAccessor .HttpContext.Request.Headers["authorization"]; //token return authorizationHeader == StringValues.Empty ? string.Empty : authorizationHeader.Single().Split(" ").Last(); // bearer tokenvalue } }Copy the code

For the token refresh, in fact, we can regard it as a token information is regenerated, but we need to disable the previous token information.

public class JwtAppService : IJwtAppService {/// <summary> // refresh Token /// </summary> // <param name=" Token ">Token</param> /// <param Name =" dTO "> user info </param> // <returns></returns> public Async Task<JwtAuthorizationDto> RefreshAsync(string token, UserDto dto) { var jwtOld = GetExistenceToken(token); If (jwtOld == null) {return new JwtAuthorizationDto() {Token = "did not obtain the current Token information ", Success = false}; } var jwt = Create(dto); // deactivate Token message await DeactivateCurrentAsync(); return jwt; } /// <summary> // Check whether the current Token exists /// </summary> // <param name=" Token ">Token</param> /// <returns></returns> private JwtAuthorizationDto GetExistenceToken(string token) => _tokens.SingleOrDefault(x => x.Token == token); }Copy the code

At this point, we have completed the creation, refresh and deactivation of the token code. Next, we will implement the verification of the token information. PS: The following code is in the Startup class unless otherwise specified.

Third, authentication

In ASP.NET Core applications, dependency injection is everywhere, and we use our function methods by dependency injection into the container and call through the function interface. Therefore, we need to inject our interface and its implementation classes into the IServiceCollection container. Here, we use reflection to batch inject the interfaces in the assembly and their implementation classes.

public void ConfigureServices(IServiceCollection services) { Assembly assembly = Assembly.Load("Grapefruit.Application"); foreach (var implement in assembly.GetTypes()) { Type[] interfaceType = implement.GetInterfaces(); foreach (var service in interfaceType) { services.AddTransient(service, implement); }}}Copy the code

As we have adopted Microsoft’s JwtBearer authorization and authentication components for the basic authorization verification, we only need to configure the basic authentication operations of token information in the middleware. At the same time, we also defined some operations on token information in the IJwtAppService interface, and for our user-defined permission authentication policy, we need to implement the policy-based authorization mode.

First of all, we need to define an inheritance in PolicyRequirement IAuthorizationRequirement custom authorization request. So in this class, you can define some properties, you can construct them by using a constructor with parameters, so I’m not going to define any properties here, I’m just going to create this class.

public class PolicyRequirement : IAuthorizationRequirement
{ }
Copy the code

Once we have created the PolicyRequirement class, we can implement our authorization logic by inheriting AuthorizationHandler. The code logic for permission control here is implemented primarily by overriding the HandleRequirementAsync method.

protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement) {//Todo: List<Menu> List = new List<Menu> {new Menu {Role = guid.empty.tostring (), Url = "/ API /v1.0/Values"}, The new Menu {Role = Guid. Empty. The ToString (), Url = "/ API/v1.0 / secret/deactivate"}, new Menu {Role = Guid. Empty. The ToString (), Url = "/ API/v1.0 / secret/refresh"}}; var httpContext = (context.Resource as AuthorizationFilterContext).HttpContext; / / to get authorization way var defaultAuthenticate = await Schemes. GetDefaultAuthenticateSchemeAsync (); if (defaultAuthenticate ! = null) {/ / validate user information issued by the var result = await httpContext. AuthenticateAsync (defaultAuthenticate. Name); If (result.Succeeded) {// Determine if the Token is disabled if (! await _jwtApp.IsCurrentActiveTokenAsync()) { context.Fail(); return; } httpContext.User = result.Principal; / / determine whether roles and Url corresponding / / var Url = httpContext Request. Path. The Value. ToLower (); var role = httpContext.User.Claims.Where(c => c.Type == ClaimTypes.Role).FirstOrDefault().Value; var menu = list.Where(x => x.Role.Equals(role) && x.Url.ToLower().Equals(url)).FirstOrDefault(); if (menu == null) { context.Fail(); return; } return; } } context.Fail(); }Copy the code

When determining whether a user can access the current requested address, the first step is to get the user’s role and the list of addresses it is allowed to access. Here I used simulated data. Check whether the role of the current login user contains the requested address. If the role does not have the permission to access the address, the 403 Forbidden status code is returned.

It is important to note that if you are going to use a RESTful API, because the requested address is the same, you need to add an HTTP predicate parameter to specify the requested method for access control purposes.

The code for the authorization configuration that contains the basic authentication of the token is shown below. In the process of Jwt verification, the middleware verifies whether the authorization method is Bearer or not and compares it with the user data when the token is generated after decryption through the token attributes, so as to judge whether the token is valid.

public void ConfigureServices(IServiceCollection services) { string issuer = Configuration["Jwt:Issuer"]; string audience = Configuration["Jwt:Audience"]; string expire = Configuration["Jwt:ExpireMinutes"]; TimeSpan expiration = TimeSpan.FromMinutes(Convert.ToDouble(expire)); SecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecurityKey"])); Services. AddAuthorization (options = > {/ / 1, the Definition authorization policy options. AddPolicy (" Permission ", policy => policy.Requirements.Add(new PolicyRequirement())); }). AddAuthentication (s = > {/ / 2, Authentication, s.d. efaultAuthenticateScheme = JwtBearerDefaults. AuthenticationScheme; s.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; s.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }). AddJwtBearer (s = > {/ / 3, the Use of Jwt bearer s.T okenValidationParameters = new TokenValidationParameters {ValidIssuer = issuer, ValidAudience = audience, IssuerSigningKey = key, ClockSkew = expiration, ValidateLifetime = true }; s.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { //Token expired if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; }}; }); //DI handler process function services.AddSingleton<IAuthorizationHandler, PolicyHandler>(); }Copy the code

public void ConfigureServices(IServiceCollection services)
{
	services.AddSwaggerGen(s =>
	{
	    //Add Jwt Authorize to http header
	    s.AddSecurityDefinition("Bearer", new ApiKeyScheme
	    {
	        Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
	        Name = "Authorization",//Jwt default param name
	        In = "header",//Jwt store address
	        Type = "apiKey"//Security scheme type
	    });
	    //Add authentication type
	    s.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
	    {
	        { "Bearer", new string[] { } }
	    });
	});
}
Copy the code

In the deactivating token code, we used Redis to store the deactivating token information, so we need to configure our Redis connection.

public void ConfigureServices(IServiceCollection services)
{
	services.AddDistributedRedisCache(r =>
    {
        r.Configuration = Configuration["Redis:ConnectionString"];
    });
}
Copy the code

Now that the entire business-related code is complete, we can create the interface that the front end accesses. Here I created a SecretController in the V1 folder under Controllers to build the interface to the front end. There are three main methods in the controller, which are CancelAccessToken (disable token), Login (obtain token) and RefreshAccessTokenAsync (refresh token).

public class SecretController : ControllerBase {/// <summary> // </summary> // <returns></returns> [HttpPost("deactivate")] public async Task<IActionResult> CancelAccessToken() { await _jwtApp.DeactivateCurrentAsync(); return Ok(); <param > [HttpPost("token")] [AllowAnonymous] <param > [HttpPost("token")] Public IActionResult Login([FromBody] SecretDto) {//Todo: Var user = new UserDto {Id = guid.newGuid (), UserName = "yuiter", Role = guid.empty, Email = "[email protected]", Phone = "13912345678", }; If (user == null) return Ok(new JwtResponseDto {Access = "unauthorized Access ", Type = "Bearer", Profile = new Profile { Name = dto.Account, Auths = 0, Expires = 0 } }); var jwt = _jwtApp.Create(user); return Ok(new JwtResponseDto { Access = jwt.Token, Type = "Bearer", Profile = new Profile { Name = user.UserName, Auths = jwt.Auths, Expires = jwt.Expires } }); } / / / < summary > / / / / / / refresh Jwt authorization data < / summary > / / / < param name = "dto" > refresh the authorized user information < param > / / / < returns > < / returns > [HttpPost("refresh")] public Async Task<IActionResult> RefreshAccessTokenAsync([FromBody] SecretDto) {//Todo: Var user = new UserDto {Id = guid.newGuid (), UserName = "yuiter", Role = guid.empty, Email = "[email protected]", Phone = "13912345678", }; If (user == null) return Ok(new JwtResponseDto {Access = "unauthorized Access ", Type = "Bearer", Profile = new Profile { Name = dto.Account, Auths = 0, Expires = 0 } }); var jwt = await _jwtApp.RefreshAsync(dto.Token, user); return Ok(new JwtResponseDto { Access = jwt.Token, Type = "Bearer", Profile = new Profile { Name = user.UserName, Auths = jwt.Success ? jwt.Auths : 0, Expires = jwt.Success ? jwt.Expires : 0 } }); }}Copy the code

As you can see from the following figure, when we do not obtain the token, the access interface prompts us with 401 Unauthorized access. When we log in to the protected resource after obtaining the token information, we can obtain the response data. Later, when we refresh the token and use the original token information to access it, we cannot access it. 403 Forbidden is displayed. At the same time, we can see that the deactivated token information already exists in Redis, and we can access it again using the new token information.

At this point, the entire Jwt authorization authentication related code has been completed, due to the length of the code, please go to Github to view the complete code (elevator direct).

conclusion

In this chapter, Jwt is used to complete user authorization and authentication, and the creation, refresh, deactivation and verification of user tokens are realized. In actual development, a mature wheel is probably a better solution. If you have a better solution for user authorization and authentication of Jwt, please point it out in the comments section. Drag a long time, should be the last article before the year, I wish everyone a happy New Year in advance ha ~~~

Of the pit

Personal Profile: Born in 1996, born in a fourth-tier city in Anhui province, graduated from Top 10 million universities. .NET programmer, gunslinger, cat. It will begin in December 2016. NET programmer career, Microsoft. NET technology stalwart, aspired to be the cloud cat kid programming for Google the best. NET programmer. Personal blog: yuiter.com blog garden blog: www.cnblogs.com/danvic712