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<Result<object>> save(UserParam userParam)

其中,user:save代表接口的权限码,只有拥有该权限的用户才能访问。

相关推荐
沈韶珺1 小时前
Visual Basic语言的云计算
开发语言·后端·golang
沈韶珺1 小时前
Perl语言的函数实现
开发语言·后端·golang
美味小鱼2 小时前
Rust 所有权特性详解
开发语言·后端·rust
我的K84092 小时前
Spring Boot基本项目结构
java·spring boot·后端
慕璃嫣3 小时前
Haskell语言的多线程编程
开发语言·后端·golang
晴空๓3 小时前
Spring Boot项目如何使用MyBatis实现分页查询
spring boot·后端·mybatis
Hello.Reader7 小时前
深入浅出 Rust 的强大 match 表达式
开发语言·后端·rust
customer0810 小时前
【开源免费】基于SpringBoot+Vue.JS体育馆管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
计算机-秋大田13 小时前
基于微信小程序的电子竞技信息交流平台设计与实现(LW+源码+讲解)
spring boot·后端·微信小程序·小程序·课程设计