[TOC]
① Store API Accessible by Roles/Users
For example,
Use List<ApiPermission>
to store the authorized API list for the role.
Optional.
Authorized APIs can also be stored in the Token, which can solely contain role information and user identity information.
/// <summary>
/// API
/// </summary>
public class ApiPermission
{
/// <summary>
/// API Name
/// </summary>
public virtual string Name { get; set; }
/// <summary>
/// API URL
/// </summary>
public virtual string Url { get; set; }
}
② Implement IAuthorizationRequirement Interface
The IAuthorizationRequirement
interface represents the user's identity information and is used for authentication verification and authorization verification.
In fact, IAuthorizationRequirement
does not have any content to implement.
namespace Microsoft.AspNetCore.Authorization
{
//
// Summary:
// Represents an authorization requirement.
public interface IAuthorizationRequirement
{
}
}
By implementing IAuthorizationRequirement
, you can define any needed properties, which will serve as a convenience for custom validation. To see how to use it, you can define it as a global identifier and set globally applicable data. I later found that my implementation was not very good:
// IAuthorizationRequirement is the Microsoft.AspNetCore.Authorization interface
/// <summary>
/// Necessary parameters class for user authentication information
/// </summary>
public class PermissionRequirement : IAuthorizationRequirement
{
/// <summary>
/// User's associated role
/// </summary>
public Role Roles { get; set; } = new Role();
public void SetRolesName(string roleName)
{
Roles.Name = roleName;
}
/// <summary>
/// Redirect to this API when there is no permission
/// </summary>
public string DeniedAction { get; set; }
/// <summary>
/// Authentication and authorization type
/// </summary>
public string ClaimType { internal get; set; }
/// <summary>
/// Redirect when unauthorized
/// </summary>
public string LoginPath { get; set; } = "/Account/Login";
/// <summary>
/// Issuer
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// Audience
/// </summary>
public string Audience { get; set; }
/// <summary>
/// Expiration time
/// </summary>
public TimeSpan Expiration { get; set; }
/// <summary>
/// Issued time
/// </summary>
public long IssuedTime { get; set; }
/// <summary>
/// Signature verification
/// </summary>
public SigningCredentials SigningCredentials { get; set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="deniedAction">Redirect to this API when there is no permission</param>
/// <param name="userPermissions">User permission collection</param>
/// <param name="deniedAction">URL of rejected requests</param>
/// <param name="permissions">Permission collection</param>
/// <param name="claimType">Claim type</param>
/// <param name="issuer">Issuer</param>
/// <param name="audience">Audience</param>
/// <param name="issusedTime">Issued time</param>
/// <param name="signingCredentials">Signing verification entity</param>
public PermissionRequirement(string deniedAction, Role Role, string claimType, string issuer, string audience, SigningCredentials signingCredentials,long issusedTime, TimeSpan expiration)
{
ClaimType = claimType;
DeniedAction = deniedAction;
Roles = Role;
Issuer = issuer;
Audience = audience;
Expiration = expiration;
IssuedTime = issusedTime;
SigningCredentials = signingCredentials;
}
}
③ Implement TokenValidationParameters
Configuration of token information
public static TokenValidationParameters GetTokenValidationParameters()
{
var tokenValida = new TokenValidationParameters
{
// Define Token content
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AuthConfig.SecurityKey)),
ValidateIssuer = true,
ValidIssuer = AuthConfig.Issuer,
ValidateAudience = true,
ValidAudience = AuthConfig.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireExpirationTime = true
};
return tokenValida;
}
④ Generate Token
Used to store user identity information (Claims) and role authorization information (PermissionRequirement) in the Token.
/// <summary>
/// Get a JWT-based Token
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
{
var now = DateTime.UtcNow;
var jwt = new JwtSecurityToken(
issuer: permissionRequirement.Issuer,
audience: permissionRequirement.Audience,
claims: claims,
notBefore: now,
expires: now.Add(permissionRequirement.Expiration),
signingCredentials: permissionRequirement.SigningCredentials
);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new
{
Status = true,
access_token = encodedJwt,
expires_in = permissionRequirement.Expiration.TotalMilliseconds,
token_type = "Bearer"
};
return response;
}
⑤ Implement Service Injection and Authentication Configuration
Import configuration information from other variables, optional
// Set the key used to encrypt the Token
// Configure role permissions
var roleRequirement = RolePermission.GetRoleRequirement(AccountHash.GetTokenSecurityKey());
// Define how to generate the user's Token
var tokenValidationParameters = RolePermission.GetTokenValidationParameters();
Configure ASP.NET Core's identity authentication services
Three configurations need to be implemented:
- AddAuthorization Import role-based identity authentication strategy
- AddAuthentication Authentication type
- AddJwtBearer Jwt authentication configuration
// Import role-based identity authentication strategy
services.AddAuthorization(options =>
{
options.AddPolicy("Permission",
policy => policy.Requirements.Add(roleRequirement));
// ↓ Authentication type
}).AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
// ↓ Jwt authentication configuration
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = tokenValidationParameters;
options.SaveToken = true;
options.Events = new JwtBearerEvents()
{
// Called after the security token and ClaimsIdentity are validated
// If the user accesses the logout page
OnTokenValidated = context =>
{
if (context.Request.Path.Value.ToString() == "/account/logout")
{
var token = ((context as TokenValidatedContext).SecurityToken as JwtSecurityToken).RawData;
}
return Task.CompletedTask;
}
};
});
Inject custom authorization service PermissionHandler
Inject the custom authentication model class roleRequirement
// Add httpcontext interception
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
services.AddSingleton(roleRequirement);
Add Middleware
I saw an example on the Microsoft website like this... but I tested and found that when the client carried the Token information and the request passed the validation context, it still failed, resulting in a 403 response.
app.UseAuthentication();
app.UseAuthorization();
I found that it should be like this:
app.UseAuthorization();
app.UseAuthentication();
Refer to the comments below the articles~
⑥ Implement Login
It is possible to store the APIs that can be accessed when issuing the Token, but this method is not suitable for situations where there are many APIs.
User information (Claims) and role information can be stored, and the backend can obtain the authorized API list through the role information.
/// <summary>
/// Login
/// </summary>
/// <param name="username">Username</param>
/// <param name="password">Password</param>
/// <returns>Token information</returns>
[HttpPost("login")]
public JsonResult Login(string username, string password)
{
var user = UserModel.Users.FirstOrDefault(x => x.UserName == username && x.UserPossword == password);
if (user == null)
return new JsonResult(
new ResponseModel
{
Code = 0,
Message = "Login failed!"
});
// Configure user claims
var userClaims = new Claim[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, user.Role),
new Claim(ClaimTypes.Expiration, DateTime.Now.AddMinutes(_requirement.Expiration.TotalMinutes).ToString()),
};
_requirement.SetRolesName(user.Role);
// Generate user identity
var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
identity.AddClaims(userClaims);
var token = JwtToken.BuildJwtToken(userClaims, _requirement);
return new JsonResult(
new ResponseModel
{
Code = 200,
Message = "Login successful! Please remember to save your Token credential!",
Data = token
});
}
⑦ Add API Authorization Policy
[Authorize(Policy = "Permission")]
⑧ Implement Custom Authorization Validation
To implement custom API role/policy authorization, you need to inherit AuthorizationHandler<TRequirement>
.
The content inside is fully customizable, and AuthorizationHandlerContext
is the context for the authentication and authorization, where custom access authorization validation is implemented.
You can also add the function to automatically refresh the Token.
/// <summary>
/// Validate user information, perform authorization Handler
/// </summary>
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
List<PermissionRequirement> requirements = new List<PermissionRequirement>();
foreach (var item in context.Requirements)
{
requirements.Add((PermissionRequirement)item);
}
foreach (var item in requirements)
{
// Validate issuer and audience
if (!(item.Issuer == AuthConfig.Issuer ?
item.Audience == AuthConfig.Audience ?
true : false : false))
{
context.Fail();
}
// Validate expiration time
var nowTime = DateTimeOffset.Now.ToUnixTimeSeconds();
var issued = item.IssuedTime + Convert.ToInt64(item.Expiration.TotalSeconds);
if (issued < nowTime)
context.Fail();
// Check if there is permission to access this API
var resource = ((Microsoft.AspNetCore.Routing.RouteEndpoint)context.Resource).RoutePattern;
var permissions = item.Roles.Permissions.ToList();
var apis = permissions.Any(x => x.Name.ToLower() == item.Roles.Name.ToLower() && x.Url.ToLower() == resource.RawText.ToLower());
if (!apis)
context.Fail();
context.Succeed(requirement);
// Redirect to a page when no permission
// var httpcontext = new HttpContextAccessor();
// httpcontext.HttpContext.Response.Redirect(item.DeniedAction);
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
⑨ Some Useful Code
Generate hash value from string, for example, for passwords.
For security, remove special characters from the string such as "
, '
, $
.
public static class AccountHash
{
// Get the hash value of a string
public static string GetByHashString(string str)
{
string hash = GetMd5Hash(str.Replace("\"", String.Empty)
.Replace("'", String.Empty)
.Replace("$", String.Empty));
return hash;
}
/// <summary>
/// Get the key used for encrypting Token
/// </summary>
/// <returns></returns>
public static SigningCredentials GetTokenSecurityKey()
{
var securityKey = new SigningCredentials(
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(AuthConfig.SecurityKey)), SecurityAlgorithms.HmacSha256);
return securityKey;
}
private static string GetMd5Hash(string source)
{
MD5 md5Hash = MD5.Create();
byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(source));
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
return sBuilder.ToString();
}
}
Issuing Token
PermissionRequirement
is not mandatory, it is used to store role or policy authentication information, Claims should be mandatory.
/// <summary>
/// Issue user Token
/// </summary>
public class JwtToken
{
/// <summary>
/// Get JWT based Token
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
{
var now = DateTime.UtcNow;
var jwt = new JwtSecurityToken(
issuer: permissionRequirement.Issuer,
audience: permissionRequirement.Audience,
claims: claims,
notBefore: now,
expires: now.Add(permissionRequirement.Expiration),
signingCredentials: permissionRequirement.SigningCredentials
);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new
{
Status = true,
access_token = encodedJwt,
expires_in = permissionRequirement.Expiration.TotalMilliseconds,
token_type = "Bearer"
};
return response;
}
Representing Timestamps
// Unix timestamp
DateTimeOffset.Now.ToUnixTimeSeconds();
// Check if Token is expired
// Convert TimeSpan to Unix timestamp
Convert.ToInt64(TimeSpan);
DateTimeOffset.Now.ToUnixTimeSeconds() + Convert.ToInt64(TimeSpan);
文章评论