[TOC]
Description
ASP.NET Core 3.0 is a lightweight JWT role/user authentication library controlled by a single API.
Recently, I had some spare time to redo a role authorization library. The previous library I developed relied on Microsoft's default interfaces, and after consulting many documents, I ultimately ended up with a flawed implementation due to insufficient understanding.
The older version can be found here: https://github.com/whuanle/CZGL.Auth/tree/1.0.0
If you're going to use Microsoft's default interfaces, I personally think it's overly complex, and there's limited documentation on this topic...
For implementing authorization and authentication through the default interfaces, you can refer to another article of mine:
Custom Role/Policy Authorization in ASP.NET Core Using JWT
Thanks to guidance from Da Ben Xiong Ge, I utilized my vacation time to create a new library that leverages Microsoft's built-in authorization and authentication, expanding upon that foundation. The library is characterized by ease of use with minimal configuration; as it doesn't reinvent the wheel, modifications can be made easily if necessary.
This library has been updated to .NET Core 3.0. If you need to use it on 2.2X, you can download the project from the repository and replace the NuGet package with the 2.2 version.
Special thanks to Da Ben Xiong Ge for his guidance.
Project repository: https://github.com/whuanle/CZGL.Auth
1. Defining Roles, APIs, and Users
Create a new website or API project, for example, MyAuth.
Search for CZGL.Auth in NuGet and install version 2.0.1, or use the Package Manager command:
Install-Package CZGL.Auth -Version 2.0.1
The design concept of CZGL.Auth is that a website can have multiple roles, multiple users, and multiple APIs.
A role possesses certain APIs, and it can add or remove roles or modify the API access rights of those roles.
A user can belong to several roles at the same time.
The first step is to consider the design of roles, users, and APIs for the website.
CZGL.Auth stores this information in memory, including which roles a user possesses and which API access rights a role has.
Roles correspond to APIs, and users have a many-to-many relationship with roles.
Create a new class called RoleService.cs, include using CZGL.Auth.Services;
, and have RoleService inherit from ManaRole.
You can manipulate role permission information through the following interfaces:
protected bool AddRole(RoleModel role);
protected bool AddUser(UserModel user);
protected bool RemoveRole(string roleName);
protected bool RemoveUser(string userName);
Clearly, these methods allow for adding/removing a role and adding/removing a user.
Suppose there are three roles: A, B, and C,
and 7 APIs: /A, /B, /C, /AB, /AC, /BC, and /ABC, with the following permissions set:
A can access A, AB, AC, ABC.
B can access B, AB, BC, ABC.
C can access C, AC, BC, ABC.
Here, we simulate data instead of loading actual data from a database.
In RoleService, add a method:
/// <summary>
/// Used to load roles and APIs
/// </summary>
public void UpdateRole()
{
List<RoleModel> roles = new List<RoleModel>
{
new RoleModel
{
RoleName="A",
Apis=new List<OneApiModel>
{
new OneApiModel
{
ApiName="A",
ApiUrl="/A"
},
new OneApiModel
{
ApiName="AB",
ApiUrl="/AB"
},
new OneApiModel
{
ApiName="AC",
ApiUrl="/AC"
},
new OneApiModel
{
ApiName="ABC",
ApiUrl="/ABC"
}
}
},
new RoleModel
{
RoleName="B",
Apis=new List<OneApiModel>
{
new OneApiModel
{
ApiName="B",
ApiUrl="/B"
},
new OneApiModel
{
ApiName="AB",
ApiUrl="/AB"
},
new OneApiModel
{
ApiName="BC",
ApiUrl="/BC"
},
new OneApiModel
{
ApiName="ABC",
ApiUrl="/ABC"
}
}
},
new RoleModel
{
RoleName="C",
Apis=new List<OneApiModel>
{
new OneApiModel
{
ApiName="C",
ApiUrl="/C"
},
new OneApiModel
{
ApiName="AC",
ApiUrl="/AC"
},
new OneApiModel
{
ApiName="BC",
ApiUrl="/BC"
},
new OneApiModel
{
ApiName="ABC",
ApiUrl="/ABC"
}
}
}
};
foreach (var item in roles)
{
AddRole(item);
}
}
Once roles and their corresponding API information are established, it's time to add users.
Assuming there are three users: aa, bb, and cc, all with the password 123456. User aa belongs to role A, user bb belongs to role B, and user cc belongs to role C...
public void UpdateUser()
{
AddUser(new UserModel { UserName = "aa", BeRoles = new List<string> { "A" } });
AddUser(new UserModel { UserName = "bb", BeRoles = new List<string> { "B" } });
AddUser(new UserModel { UserName = "cc", BeRoles = new List<string> { "C" } });
}
To load roles and users into CZGL.Auth, you need to call these methods during application startup, for example in Program.cs:
RoleService roleService = new RoleService();
roleService.UpdateRole();
roleService.UpdateUser();
2. Adding Custom Events
Authorization can involve various scenarios, so you might want to add custom events to log users' access authorization information and influence authorization results.
Import using CZGL.Auth.Interface;
Add a class called RoleEvents that inherits from IRoleEventsHandler:
public class RoleEvents : IRoleEventsHadner
{
public async Task Start(HttpContext httpContext)
{
await Task.CompletedTask;
}
public void TokenEbnormal(object eventsInfo)
{
}
public void TokenIssued(object eventsInfo)
{
}
public void NoPermissions(object eventsInfo)
{
}
public void Success(object eventsInfo)
{
}
public async Task End(HttpContext httpContext)
{
await Task.CompletedTask;
}
}
Call Start before validating authorization in CZGL.Auth and call End afterward, passing the HttpContext type as a parameter. You can add custom authorization information here and potentially affect the request pipeline.
The meanings of the other methods are as follows:
TokenEbnormal: The token carried by the client is not a valid JWT token and cannot be decoded.
TokenIssued: After decoding the token, either the issuer or audience is incorrect.
NoPermissions: No permission to access this API.
These methods will be called at various stages of authorization and authentication.
3. Injecting Authorization Services and Middleware
To use CZGL.Auth, you need to inject the following two services:
services.AddRoleService(authOptions);
services.AddSingleton<IRoleEventsHadner, RoleEvents>();
AddRoleService
injects the authorization service, while AddSingleton
injects your events.
AddRoleService requires an AuthConfigModel type parameter.
You can configure it like this:
var authOptions = new AuthBuilder()
.Security("aaaafsfsfdrhdhrejtrjrt", "ASPNETCORE", "ASPNETCORE")
.Jump("accoun/login", "account/error", false, false)
.Time(TimeSpan.FromMinutes(20))
.InfoScheme(new CZGL.Auth.Models.AuthenticateScheme
{
TokenEbnormal = "Login authentication failed!",
TokenIssued = "Login authentication failed!",
NoPermissions = "Login authentication failed!"
}).Build();
services.AddRoleService(authOptions);
services.AddSingleton<IRoleEventsHadner, RoleEvents>();
In the Security configuration, the parameters are the key string, issuer, and audience.
In Jump, configure the redirect addresses in case of authorization failure. The parameters are the unauthorized redirect, invalid authorization redirect, and the last two bools control redirect behavior.
Time sets the token expiration period.
InfoScheme provides failure messages for authorization issues, such as:
The image above displays the message for a time-out case. When a user requests an API and fails, a 401 status code is returned, and the header will carry the message. CZGL.Auth allows for setting up three cases with custom headers:
TokenEbnormal: The token carried by the client is not a valid JWT token and cannot be decoded.
TokenIssued: After decoding the token, the issuer or audience is incorrect.
NoPermissions: No permission to access this API.
You need to add three middleware components:
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<RoleMiddleware>();
app.UseAuthorization();
is the middleware for Microsoft's authorization certification; CZGL.Auth will first filter out some invalid requests and certification information using the default validation pipeline before CZGL.Auth checks authorization.
4. How to Set API Authorization
It's very simple: with CZGL.Auth, you only need to add the [Authorize]
attribute to a Controller or Action.
CZGL.Auth will only take effect for controllers or actions that use the [Authorize]
attribute.
If a Controller is already decorated with [Authorize]
but you want to bypass authorization for specific Actions inside it, you can use the [AllowAnonymous]
attribute on those Actions.
The way of usage is entirely consistent with Microsoft's defaults, which means no excessive configuration is required.
If you want to define another attribute for additional authorization settings, feel free to create an Issue in my repository or contact me directly via WeChat.
。
Add an APIController,
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet("/A")]
public JsonResult A()
{
return new JsonResult(new { Code = 200, Message = "Success!" });
}
[HttpGet("/B")]
public JsonResult B()
{
return new JsonResult(new { Code = 200, Message = "Success!" });
}
[HttpGet("/C")]
public JsonResult C()
{
return new JsonResult(new { Code = 200, Message = "Success!" });
}
[HttpGet("/AB")]
public JsonResult AB()
{
return new JsonResult(new { Code = 200, Message = "Success!" });
}
[HttpGet("/BC")]
public JsonResult BC()
{
return new JsonResult(new { Code = 200, Message = "Success!" });
}
[HttpGet("/AC")]
public JsonResult AC()
{
return new JsonResult(new { Code = 200, Message = "Success!" });
}
[HttpGet("/ABC")]
public JsonResult ABC()
{
return new JsonResult(new { claims = User.Claims });
}
/// <summary>
/// No one can access this
/// </summary>
/// <returns></returns>
[HttpGet("D")]
public JsonResult D()
{
return new JsonResult(new { Code = 200, Message = "Success!" });
}
[HttpGet("error")]
public JsonResult Denied()
{
return new JsonResult(
new
{
Code = 0,
Message = "Access failed!",
Data = "This account has no access rights!"
});
}
}
IV. Add Login to Issue Token
Add an AccountController.cs to issue login and Token.
[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
[HttpPost("/Login")]
public async Task<JsonResult> Login([FromQuery]string username, string password, string rolename)
{
// Check if username and password are correct
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(rolename))
{
return new JsonResult(new
{
Code = 0,
Message = "What garbage information is this?",
});
}
if(!((username=="aa"||username=="bb"||username=="cc")&&password=="123456"))
{
return new JsonResult(new
{
Code = 0,
Message = "Account or password incorrect",
});
}
// Role/User information service defined by yourself
RoleService roleService = new RoleService();
// Check if the user belongs to this role
var role = roleService.IsUserToRole(username,rolename);
// CZGL.Auth class used for encryption and decryption
EncryptionHash hash = new EncryptionHash();
// Set user claims
var userClaims = hash.BuildClaims(username, rolename);
//// Custom build user claims
/// Custom user claims should at least include the following
//var userClaims = new Claim[]
//{
//new Claim(ClaimTypes.Name, userName),
// new Claim(ClaimTypes.Role, roleName),
// new Claim(JwtRegisteredClaimNames.Aud, Audience),
// new Claim(ClaimTypes.Expiration, TimeSpan.TotalSeconds.ToString()),
// new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString())
//};
/*
iss (issuer): Issuer
exp (expiration time): Expiry time
sub (subject): Subject
aud (audience): Audience
nbf (Not Before): Effective time
iat (Issued At): Issuance time
jti (JWT ID): ID
*/
// Method 1, directly issue Token
ResponseToken token = hash.BuildToken(userClaims);
// Method 2, split into multiple steps to issue token for easier debugging
//var identity = hash.GetIdentity(userClaims);
//var jwt = hash.BuildJwtToken(userClaims);
//var token = hash.BuildJwtResponseToken(jwt);
return new JsonResult(token);
}
}
V. Partial Explanation
Inject Jwt service and issue Token
CZGL.Auth wraps the services using jwt and the code for issuing Tokens, which is not "reinventing the wheel", so you can actually easily extract this part of the code and design it differently.
The code for this part is located at RoleServiceExtension.cs and EncryptionHash.cs.
Authorization Middleware
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<RoleMiddleware>();
My approach is to use ASP.NET Core's jwt to complete basic authentication and authorization, then implement extended authentication in the next pipeline. However, the built-in authentication is extended in app.UseAuthorization();, so using CZGL.Auth only requires you to use it as you normally would with jwt, just with an additional RoleMiddleware.
CZGL.Auth was something I quickly wrote in response to new ideas... It’s best not to use it directly in production; download the project from the github repository and modify it according to your application scenario~.
VI. Verification
First, log in using the aa user and select the A role.
Since the A user can only access APIs that contain "A", such as "/A", "/AB", etc., we can test it.
Next, use this Token to access "/B".
You can continue trying to add APIs or log in with other users to access different APIs.
Since others are not familiar with the front end, I won’t write examples with pages~.
You can test it using Postman.
Examples of the projects can be downloaded from the repository named MyAuth.
Generally, user permissions and role permission information are stored in the database. Another example is CZGL.Auth.Sample2.
This library is just a rough authentication and authorization implementation; for richer requirements, please download the source code and modify~.
If you have questions to discuss, you can find me in the club.
I'm in groups from Shenzhen, Guangzhou, Changsha, Shanghai, etc., ha ha ha, ha ha ha.
文章评论