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的连接字符串

"ConnectionStrings": {
    
    
    "Mysql": "server=localhost;port=3306;uid=root;pwd=123456;database=authorization_demo"
}

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

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

[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; }
}

用户

[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; }
}

角色

[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; }

}

权限

[Table("t_user_role")]
public class UserRole
{
    
    
    public long UserId {
    
      get; set; }

    public long RoleId {
    
     get; set; }
}

用户与角色的中间实体类

[Table("t_role_authority")]
public class RoleAuthority
{
    
    
    public long RoleId {
    
      get; set; }

    public long AuthorityId {
    
      get; set; }
}

角色与权限的中间实体类

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

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文件中注册数据库上下文服务

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类来做密码加密和校验。

builder.Services.AddScoped<IPasswordHasher<User>, PasswordHasher<User>>();

4、定义接口响应格式

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、创建新增用户的接口

[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。

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、创建登录接口

[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方法

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中注册这个类,作为认证的处理类

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中的配置如下

builder.Services.AddMemoryCache();

10、创建后台服务

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

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里面注册后台服务

builder.Services.AddHostedService<MemoryBgService>();

11、自定义授权Attribute

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

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方法

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里面注册授权处理类的服务

builder.Services.AddSingleton<IAuthorizationHandler,MyAuthorizationHandler>();

13、设置需要认证的接口

[Authorize]属性是用来设置需要认证的接口的,如果每个控制层的类都要加上这个属性,也太麻烦了,所以为了一劳永逸,我在Program.cs里面注册了如下服务

builder.Services.AddAuthorization(options =>
{
    
    
    options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
});

这表示所有接口都要认证才能访问,可以给不需要认证的接口加上[AllowAnonymous]属性,比如登录接口。

14、设置接口权限码

可以在接口上加上前面定义的AuthorityAttribute属性,设置权限码

[Authority("user:save")]
[HttpPost]
public async Task<Result<object>> save(UserParam userParam)

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

猜你喜欢

转载自blog.csdn.net/woshihedayu/article/details/139601563