ASP.NET Core自定义认证和授权搭建流程(使用JWT)

asp.net core本身就自带了认证和授权框架,其中包含了Identity框架,可以自动生成相关的数据库表结构,调用UserManager、RoleManager、SiginManager这些服务,可以自动生成SQL语句访问用户、角色等功能。但是不同的项目,业务功能不一样,Identity自动生成的表结构并不符合所有项目的业务需求,所以我不太看好使用Identity框架来搭建项目,这里总结一下使用asp.net core里面的自定义认证和授权功能来搭建项目的方法。

1、安装EF Core,配置数据库连接

EFCore访问MySQL的nuget包Pomelo.EntityFrameworkCore.MySql,在appsettings.json文件里面配置MySQL的连接字符串

bash 复制代码
"ConnectionStrings": {
    "Mysql": "server=localhost;port=3306;uid=root;pwd=123456;database=authorization_demo"
}

2、创建和配置实体类、生成表结构

这里用到了用于认证和授权的5个基础的实体类,分别代表

csharp 复制代码
[Table("t_user")]
public class User
{
    [Key]
    public long Id { get; set; }

    public string UserName { get; set; }

    public string Password { get; set; }

    public string Email { get; set; }

    public List<Role> Roles { get; set; }
}

用户

csharp 复制代码
[Table("t_role")]
public class Role
{
    [Key]
    public long Id { get; set; }

    public string Name { get; set; }

    public List<User> Users { get; set; }

    public List<Authority> Authoritys { get; set; }
}

角色

csharp 复制代码
[Table("t_authority")]
public class Authority
{
    [Key]
    public long Id { get; set; }

	//权限码
    public string code {  get; set; }
	//描述
    public string Description { get; set; }

    public List<Role> Roles { get; set; }

}

权限

csharp 复制代码
[Table("t_user_role")]
public class UserRole
{
    public long UserId {  get; set; }

    public long RoleId { get; set; }
}

用户与角色的中间实体类

csharp 复制代码
[Table("t_role_authority")]
public class RoleAuthority
{
    public long RoleId {  get; set; }

    public long AuthorityId {  get; set; }
}

角色与权限的中间实体类

然后,创建数据库上下文类,配置用户与角色的多对多关系,角色与权限的多对多关系

csharp 复制代码
public class ApplicationContext : DbContext
{
    public ApplicationContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }
    public DbSet<Authority> Authoritys { get; set; }
    public DbSet<UserRole> UserRoles { get; set; }
    public DbSet<RoleAuthority> RoleAuthoritys { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().HasMany(user => user.Roles).WithMany(role => role.Users).UsingEntity<UserRole>();

        modelBuilder.Entity<Role>().HasMany(role => role.Authoritys).WithMany(authority => authority.Roles).UsingEntity<RoleAuthority>();

    }
}

在Program.cs文件中注册数据库上下文服务

csharp 复制代码
builder.Services.AddDbContext<ApplicationContext>(options =>
{
    string connectionString = builder.Configuration.GetConnectionString("Mysql");
    var serverVersion = ServerVersion.AutoDetect(connectionString);
    options.UseMySql(connectionString, serverVersion);
});

安装nuget包Microsoft.EntityFrameworkCore.Tools,打开nuget控制台,使用efcore的迁移命令add-migration [迁移名称]和update-database生成数据库表。

3、注册加密服务

Program.cs中的配置如下,使用PasswordHasher类来做密码加密和校验。

csharp 复制代码
builder.Services.AddScoped<IPasswordHasher<User>, PasswordHasher<User>>();

4、定义接口响应格式

csharp 复制代码
public record Result<T>(ResultCode Code,string Message,T Data)
{

    public static Result<T> Success(T data=default)
    {
        return new Result<T>(ResultCode.SUCCESS, "成功", data);
    }

    public static Result<T> Fail(string message=null)
    {
        return new Result<T>(ResultCode.FAILURE, message, default);
    }
}

public enum ResultCode { SUCCESS=1, FAILURE }

5、创建新增用户的接口

csharp 复制代码
[HttpPost]
public async Task<Result<object>> save(UserParam userParam)
{
    var (userName,password,email) = userParam;
    var exist=await context.Users.AnyAsync(user => user.UserName == userName);
    if (exist)
    {
        return Result<object>.Fail("用户名已存在");
    }
    var user = new User { UserName = userName, Email = email };
    var encriptPassword=passwordHasher.HashPassword(user,password);
    user.Password = encriptPassword;
    context.Users.Add(user);
    await context.SaveChangesAsync();

    return Result<object>.Success();
}

这里面使用了HashPassword方法对用户密码进行加密,然后保存到数据库。

6、封装JWT的工具类

安装nuget包System.IdentityModel.Tokens.Jwt,定义一个工具类用于生成token和解析token。

csharp 复制代码
public class JwtUtil
{
    private const string KEY = "aaaaabbbbbcccccdddddeeeeefffffggggg";

	//生成token
    public static string Create(string userId,IEnumerable<long> roleIds)
    {
        var claims = new List<Claim>();
        claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
        foreach (var roleId in roleIds)
        {
            claims.Add(new Claim(ClaimTypes.Role, roleId.ToString()));
        }
        var expires=DateTime.Now.AddMinutes(30);
        var keyBytes =Encoding.UTF8.GetBytes(KEY);
        var securityKey = new SymmetricSecurityKey(keyBytes);
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
        var tokenDescriptor=new JwtSecurityToken(claims:claims,expires:expires,signingCredentials:credentials);
        var token = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
        return token;
    }

	//验证token
    public static bool ValidateToken(string token,out IEnumerable<Claim> claims)
    {
        claims = null;
        var tokenHandler =new JwtSecurityTokenHandler();
        var valParam=new TokenValidationParameters();
        var securityKey=new SymmetricSecurityKey(Encoding.UTF8.GetBytes(KEY));
        valParam.IssuerSigningKey = securityKey;
        valParam.ValidateIssuer = false;
        valParam.ValidateAudience = false;
        ClaimsPrincipal claimsPrincipal;
        try
        {
            claimsPrincipal = tokenHandler.ValidateToken(token, valParam, out SecurityToken validatedToken);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            return false;
        }
        claims=claimsPrincipal.Claims;
        return true;
    }
}

这里面我把用户id和角色id保存到了token里面,之所以不直接保存权限码,是因为一个项目可能有几百个接口,这是正常的,如果每个接口的权限码都不一样,那么一个token里面就要保存几百个权限码,会导致token字符串过长,每次请求都占用很多网络带宽。所以,我的想法就是在项目启动的时候,把数据库中所有角色的id和关联的权限码都查出来,放到缓存里面,然后在用户授权的时候,根据token中拿到的角色id去缓存中查询关联的权限码,再跟接口的权限码做比对,就能判断用户有没有权限访问。

一个项目一般顶多也就几十种角色,几百个权限码,放入缓存不会太占内存空间。

7、创建登录接口

csharp 复制代码
[HttpPost("login")]
public async Task<Result<string>> login(string username, string password)
{
    var user = await context.Users.Include(user => user.Roles).Where(user => user.UserName == username).FirstOrDefaultAsync();
    if (user==null)
    {
        return Result<string>.Fail("用户名不存在");
    }
    var result=passwordHasher.VerifyHashedPassword(user, user.Password,password);
    if (result==PasswordVerificationResult.Failed)
    {
        return Result<string>.Fail("密码错误");
    }
    var roleIds= user.Roles.Select(role => role.Id).ToArray();
    var token = JwtUtil.Create(user.Id.ToString(), roleIds);
    return Result<string>.Success(token);
}

这里调用VerifyHashedPassword方法做密码校验,执行result==PasswordVerificationResult.Failed判断密码是否正确,最后生成token发送给前端。

8、自定义认证逻辑

这里面需要定义一个类,继承IAuthenticationHandler接口,并实现AuthenticateAsync、ChallengeAsync、ForbidAsync、InitializeAsync方法

csharp 复制代码
public class MyAuthenticationHandler : IAuthenticationHandler
{
    public const string MY_SCHEMA_NAME = "myAuth";

    private const string HEADER = "Authorization";

    private HttpContext context;

    private AuthenticationScheme scheme;

    public Task<AuthenticateResult> AuthenticateAsync()
    {
        AuthenticateResult result;
        var getHeaderSuccess =context.Request.Headers.TryGetValue(HEADER, out var value);
        if (getHeaderSuccess)
        {
            string token=value.ToString();
            if (!string.IsNullOrWhiteSpace(token))
            {
                var validateSuccess=JwtUtil.ValidateToken(token,out var claims);
                if (validateSuccess)
                {
                    ClaimsIdentity claimsIdentity = new(claims, MY_SCHEMA_NAME);
                    ClaimsPrincipal claimsPrincipal=new(claimsIdentity);
                    AuthenticationTicket ticket = new(claimsPrincipal, scheme.Name);
                    result = AuthenticateResult.Success(ticket);
                }
                else
                {
                    result = AuthenticateResult.Fail("token解析失败");
                }
            }
            else
            {
                result = AuthenticateResult.Fail("token为空");
            }
        }
        else
        {
            result = AuthenticateResult.Fail("token请求头不存在");
        }
        return Task.FromResult(result);
    }

	/// <summary>
	/// 未认证的处理方法
	/// </summary>
	/// <param name="properties"></param>
	/// <returns></returns>
    public Task ChallengeAsync(AuthenticationProperties? properties)
    {
        context.Response.StatusCode= (int)HttpStatusCode.Unauthorized;
        return context.Response.WriteAsync("你还未登录");
    }

	/// <summary>
	/// 未授权的处理方法
	/// </summary>
	/// <param name="properties"></param>
	/// <returns></returns>
    public Task ForbidAsync(AuthenticationProperties? properties)
    {
        context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
        return context.Response.WriteAsync("没有权限");
    }

    public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
    {
        this.scheme = scheme;
        this.context = context;
        return Task.CompletedTask;
    }
}

其中,AuthenticateAsync方法是用来实现认证逻辑的,从请求头中取出token,进行解析,调用AuthenticateResult.Success方法表示认证成功,并将token解析得到的数据保存在claims里面,通过ticket变量传给授权中间件;调用AuthenticateResult.Fail方法代表认证失败。ChallengeAsync方法是未认证的处理方法,ForbidAsync方法是未授权的处理方法。

然后,需要在Program.cs中注册这个类,作为认证的处理类

csharp 复制代码
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = MyAuthenticationHandler.MY_SCHEMA_NAME;
    options.DefaultChallengeScheme = MyAuthenticationHandler.MY_SCHEMA_NAME;
    options.DefaultForbidScheme= MyAuthenticationHandler.MY_SCHEMA_NAME;
    options.AddScheme<MyAuthenticationHandler>(MyAuthenticationHandler.MY_SCHEMA_NAME, MyAuthenticationHandler.MY_SCHEMA_NAME);
});

这里面配置了3个默认的Scheme,分别代表处理认证、认证失败和授权失败的处理类。

9、注册缓存服务

Program.cs中的配置如下

csharp 复制代码
builder.Services.AddMemoryCache();

10、创建后台服务

定义一个类,继承BackgroundService类,并重写ExecuteAsync方法

csharp 复制代码
public class MemoryBgService : BackgroundService
{
    private readonly ApplicationContext context;

    private readonly IMemoryCache cache;

    public MemoryBgService(IServiceScopeFactory scopeFactory,IMemoryCache cache)
    {
        var serviceScop = scopeFactory.CreateScope();
        var serviceProvider = serviceScop.ServiceProvider;
        this.context=serviceProvider.GetRequiredService<ApplicationContext>();
        this.cache = cache;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var roles=await context.Roles.Include(role => role.Authoritys)
            .Select(role => new { RoleId=role.Id,AuthorityCodes=role.Authoritys.Select(authority => authority.code).ToHashSet()})
            .ToArrayAsync();
        foreach (var role in roles)
        {
            cache.Set(role.RoleId,role.AuthorityCodes);
        }
    }
}

这里面需要注入缓存服务,并且会在项目启动的时候运行ExecuteAsync方法,首先会从数据库查询所有的角色和关联的权限码,然后保存到本地缓存里面,其中权限码保存在HashSet里面,方便进行查找。

然后,在Program.cs里面注册后台服务

csharp 复制代码
builder.Services.AddHostedService<MemoryBgService>();

11、自定义授权Attribute

定义一个类,继承AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData,这些类和接口

csharp 复制代码
public class AuthorityAttribute : AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData
{
    public AuthorityAttribute(string code)
    {
        this.Code = code;
    }

    public string Code { get; }

    public IEnumerable<IAuthorizationRequirement> GetRequirements()
    {
        yield return this;
    }
}

其中,Code属性代表权限码。

12、自定义授权逻辑

定义一个类,继承AuthorizationHandler类,并传入前面定义的AuthorityAttribute类型,重写HandleRequirementAsync方法

csharp 复制代码
public class MyAuthorizationHandler : AuthorizationHandler<AuthorityAttribute>
{
    private readonly IMemoryCache cache;

    public MyAuthorizationHandler(IMemoryCache cache)
    {
        this.cache = cache;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorityAttribute requirement)
    {
        var code=requirement.Code;
        var roleIds=context.User.Claims.Where(claim => claim.Type==ClaimTypes.Role).Select(claim => claim.Value).ToArray();
        foreach (var roleId in roleIds)
        {
            var codes=cache.Get<HashSet<string>>(long.Parse(roleId));
            if (!codes.IsNullOrEmpty())
            {
                if (codes.Contains(code))
                {
                    context.Succeed(requirement);
                    return Task.CompletedTask;
                }
            }
        }
        context.Fail();
        return Task.CompletedTask;
    }
}

通过requirement参数可以获取接口的权限码,通过context参数可以获取Claims中保存的角色id,然后根据角色id从缓存中获取权限码的HashSet集合,依次判断其中是否包含接口的权限码,调用context.Succeed表示授权成功,调用context.Fail()表示授权失败。

然后,在Program.cs里面注册授权处理类的服务

csharp 复制代码
builder.Services.AddSingleton<IAuthorizationHandler,MyAuthorizationHandler>();

13、设置需要认证的接口

Authorize\]属性是用来设置需要认证的接口的,如果每个控制层的类都要加上这个属性,也太麻烦了,所以为了一劳永逸,我在Program.cs里面注册了如下服务 ```csharp builder.Services.AddAuthorization(options => { options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); }); ``` 这表示所有接口都要认证才能访问,可以给不需要认证的接口加上\[AllowAnonymous\]属性,比如登录接口。 #### 14、设置接口权限码 可以在接口上加上前面定义的AuthorityAttribute属性,设置权限码 ```csharp [Authority("user:save")] [HttpPost] public async Task> save(UserParam userParam) ``` 其中,user:save代表接口的权限码,只有拥有该权限的用户才能访问。

相关推荐
程序员爱钓鱼2 分钟前
Go语言实战案例 — 项目实战篇:图书管理系统(文件存储)
后端·google·go
元闰子7 分钟前
OLTP上云,哪种架构最划算?·VLDB'25
数据库·后端·云原生
IT_陈寒23 分钟前
Vite 5.0重磅升级:8个性能优化秘诀让你的构建速度飙升200%!🚀
前端·人工智能·后端
hui函数40 分钟前
scrapy框架-day02
后端·爬虫·python·scrapy
Moshow郑锴1 小时前
SpringBootCodeGenerator使用JSqlParser解析DDL CREATE SQL 语句
spring boot·后端·sql
小沈同学呀7 小时前
创建一个Spring Boot Starter风格的Basic认证SDK
java·spring boot·后端
方圆想当图灵9 小时前
如何让百万 QPS 下的服务更高效?
分布式·后端
凤山老林9 小时前
SpringBoot 轻量级一站式日志可视化与JVM监控
jvm·spring boot·后端
凡梦千华9 小时前
Django时区感知
后端·python·django
Chan1610 小时前
JVM从入门到实战:从字节码组成、类生命周期到双亲委派及打破双亲委派机制
java·jvm·spring boot·后端·intellij-idea