Asp.Net Core 7.0 基于JWT角色权限认证(一)

背景:

公司的权限认证模块比较老旧,想写一个支持多平台,同一个用户在不同平台拥有不同的角色的功能,本篇文章先基于jwt自定义基本的权限认证。

什么是JWT?

JWT是目前最流行的跨域身份验证解决方案。

JWT的官网地址:https://jwt.io/

通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限。

JWT中包含了身份认证必须的参数以及用户自定义的参数,JWT可以使用秘密(使用HMAC算法)或使用RSAECDSA的公钥/私钥对进行签名。

简单介绍一下JWT令牌结构

JSON Web Tokens由 .分隔的三个部分组成,它们是:

  • Header 头
  • Payload 有效载荷
  • Signature 签名

JWT的详情:JWT详情

JWT安装

为了方便,本项目采用仓储模式,由Api,BizManagement,DataBase,Entity 4个模块组成,本次基于Rider开发,通过Nuget包管理器进行安装,如下所示:

项目模块组成: 

依赖组件: 

1. redis (StackExchange.Redis: 2.6.122) 查询全量数据的持久化,token的保存
2. Nlog(5.3.5) 日志记录到文件
3. Bcypt.Net-Next 用于将用户密码加密保存到数据库中
4. Mysql(Pomelo.EntityFrameworkCore.MySql: 7.0.0) 持久化Auth到数据库
5. AutoMapper(12.0.1) 实体映射
6. EFCore(7.0.13) ORM管理
 

代码部分(核心):

1.appsettings.json配置JWT的基本信息
"JWTTokenOptions": {
    "Isuser": "org.huage.auth",
    "Audience": "org.huage.auth",
    "SecurityKey": "nadjhfgkadshgoihfkajhkjdhsfaidkuahfhdksjaghidshyaukfhdjks"
  }
2.Entity模块定义配置信息实体 
//注意这里的属性名称必须和配置信息的Key相同
public class JWTTokenOptions
{
    public string Audience { get; set; }
    public string Isuser { get; set; }
    public string SecurityKey { get; set; }
    
   
}
3.依赖注入,Api模块定义一个ServiceExtension工具类,添加JwtAuthentication方法
public static void JwtAuthentication(this WebApplicationBuilder builder)
    {
        JWTTokenOptions tokenOptions = new JWTTokenOptions();//初始化
        builder.Configuration.Bind("JWTTokenOptions", tokenOptions);
        builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    //JWT有一些默认的属性,就是给鉴权时就可以筛选了
                    ValidateIssuer = true, //是否验证Issuer
                    ValidateAudience = true, //是否验证Audience
                    ValidateIssuerSigningKey = true, //是否验证SecurityKey
                    ValidAudience = tokenOptions.Audience,
                    ValidIssuer = tokenOptions.Isuser, //Issuer,这两项和前面签发jwt的设置一致
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenOptions.SecurityKey))
                };
            });
    }
4.Program.cs中注入
builder.JwtAuthentication();
builder.Services.AddTransient<IJwtService,JwtService>();
//注册过滤器
builder.Services.AddControllers(o =>
{
    o.Filters.Add<YesAttribute>();
});
5.写JWT服务,主要包含生成token,验证token两个方法
接口:IJwtService
public interface IJwtService
{
    Task<string> GetToken(UserModel userModel);

    Task<List<string>> AnalysisToken(string token);

    Task<JwtSecurityTokenHandler> VerifyToken(string token);
}
 实现类:JwtService
public class JwtService : IJwtService
{
    private readonly IConfiguration _configuration;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IRedisManager _redisManager;
    private readonly IUserManager _userManager;
    private readonly ILogger<JwtService> _logger;

    public JwtService(IConfiguration configuration, IHttpContextAccessor httpContextAccessor,
        IRedisManager redisManager, IUserManager userManager, ILogger<JwtService> logger)
    {
        _configuration = configuration;
        _httpContextAccessor = httpContextAccessor;
        _redisManager = redisManager;
        _userManager = userManager;
        _logger = logger;
    }

    public async Task<string> GetToken(int organizationId, UserModel dto)
    {
        var jwtTokenOptions = _configuration.GetSection("JWTTokenOptions").Get<JWTTokenOptions>();

        SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtTokenOptions.SecurityKey));

        //查询用户的角色
        var role = await _userManager.QueryRoleByUserIdAsync(organizationId, dto.Id);
        var roleNames = role.RoleModels.Select(_ => _.Name).ToList();
        var roleString = string.Join(",", roleNames);
        //new Claim("Expiration", expiresAt.ToString())
        IEnumerable<Claim> claims = new Claim[]
        {
            new Claim("UserName", dto.UserName),
            new Claim("Role", roleString)
        };

        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        //expires: expiresAt, //60分钟有效期
        var token = new JwtSecurityToken(
            issuer: jwtTokenOptions.Isuser,
            audience: jwtTokenOptions.Audience,
            claims: claims,
            notBefore: DateTime.Now, //立即生效  DateTime.Now.AddMilliseconds(30),//30s后有效
            signingCredentials: creds);

        string returnToken = new JwtSecurityTokenHandler().WriteToken(token);

        //存到redis当中
        var dataBase = await _redisManager.SwitchDataBase(1);
        dataBase.StringSet(dto.Id.ToString(), returnToken, TimeSpan.FromMinutes(60));

        return returnToken;
    }

    /// <summary>
    /// 每次登录后刷新token有效时间。
    /// </summary>
    /// <param name="id"></param>
    /// <param name="token"></param>
    /// <returns></returns>
    public async Task<bool> RefreshToken(string id)
    {
        var dataBase = await _redisManager.SwitchDataBase(1);
        dataBase.KeyExpire(id, TimeSpan.FromMinutes(60));
        return true;
    }

    /// <summary>
    /// 解析token,拿到权限信息。
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    public async Task<List<string>> AnalysisToken(string token)
    {
        try
        {
            //校验token
            var claimsPrincipal = await VerifyToken(token);

            //拿到claims
            var jwtSecurityToken = claimsPrincipal.ReadJwtToken(token);
            //拿到权限列表
            var roleString = jwtSecurityToken.Claims.FirstOrDefault(c => c.Type == "Role")?.Value;
            var roleList = roleString!.Split(",").ToList();
            return roleList;
        }
        catch (Exception e)
        {
            _logger.LogError($"AnalysisToken occur exception:{e.Message}");
            throw;
        }
    }

    /// <summary>
    /// 校验token 的有效性
    /// </summary>
    /// <param name="token"></param>
    public async Task<JwtSecurityTokenHandler> VerifyToken(string token)
    {
        var jwtTokenOptions = _configuration.GetSection("JWTTokenOptions").Get<JWTTokenOptions>();
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtTokenOptions!.SecurityKey));
        //校验token  ValidateLifetime = true,ClockSkew = TimeSpan.Zero //校验过期时间必须加此属性
        var validateParameter = new TokenValidationParameters()
        {
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = jwtTokenOptions.Isuser,
            ValidAudience = jwtTokenOptions.Audience,
            IssuerSigningKey = securityKey,
        };

        try
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            //校验是否合法
            tokenHandler.ValidateToken(token, validateParameter, out SecurityToken _);
            return tokenHandler;
        }
        catch (Exception e)
        {
            _logger.LogError("Token invalid.");
            throw;
        }
    }
}

6. 自定义特性Attribute和实现过滤器(注册过滤器在第四步一起)
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using org.huage.AuthManagement.BizManager.service;

namespace org.huage.AuthManagement.Api.Auth;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class YesAttribute : Attribute,IAuthorizationFilter
{
    public string Roles { get; set; }

    public YesAttribute()
    {
    }

    //自定义权限认证
    public async void OnAuthorization(AuthorizationFilterContext context)
    {
        //如果不携带这个参数则不校验
        if (! context.ActionDescriptor.EndpointMetadata.Any(item => item is YesAttribute))
        {
            return;
        }
        //如果方法标有AllowAnonymousAttribute则跳过权限检查
        if (context.ActionDescriptor.EndpointMetadata.Any(item => item is AllowAnonymousAttribute))
        {
            return;
        }
        //拿到token
        string userToken = context.HttpContext.Request.Headers["token"].ToString(); //获取token
        if (string.IsNullOrEmpty(userToken))
        {
            context.Result = new ContentResult() { StatusCode = 403, Content = "Headers no token,please check." };  //没有Cookie则跳转到登陆页面
            return;
        }
        else
        {
            //拿到jwt服务来校验
            var jwtService = context.HttpContext.RequestServices.GetService<IJwtService>();
            //解析token,拿到role集合。
            var roles =await jwtService!.AnalysisToken(userToken);
            if (! roles.Any())
            {
                context.Result = new ContentResult() { StatusCode = 401, Content = "Please check the roles you owned." };
                return;
            }
            //获取attribute的roles;
            var methodInfo = ((ControllerActionDescriptor)context.ActionDescriptor).MethodInfo;
            var attribute = methodInfo.GetCustomAttribute<YesAttribute>();
            if (attribute ==null)
            {
                context.Result = new ContentResult() { StatusCode = 401, Content = "Please check the roles you owned." };   
                return;
            }
            
            var attributeRoles = attribute.Roles;
            var a = attributeRoles.Split(",").ToHashSet();
            var of = roles.ToHashSet().IsSupersetOf(a);
            if (!of)
            {
                context.Result = new ContentResult() { StatusCode = 401, Content = "You don't have this role" };
            }
        }
       
    }
}
7.测试,在AuthenticationController中定义Login()和Test()
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using org.huage.AuthManagement.Api.Auth;
using org.huage.AuthManagement.BizManager.Helper;
using org.huage.AuthManagement.BizManager.Redis;
using org.huage.AuthManagement.BizManager.service;
using org.huage.AuthManagement.Entity.Managers;
using org.huage.AuthManagement.Entity.Request;
using StackExchange.Redis;

namespace org.huage.AuthManagement.Api.Controllers;

[ApiController]
[Route("/[controller]/[action]")]
public class AuthenticationController : Controller 
{
    private readonly ILogger<AuthenticationController> _logger;

    private readonly IJwtService _jwtService;
    private readonly IUserManager _userManager;
    private readonly IRedisManager _redisManager;
    public AuthenticationController(IJwtService jwtService, IUserManager userManager, IRedisManager redisManager, ILogger<AuthenticationController> logger)
    {
        _jwtService = jwtService;
        _userManager = userManager;
        _redisManager = redisManager;
        _logger = logger;
    }

    /// <summary>
    /// 过去时间由redis控制,不再jwt里面设置,不好refresh 时间
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    [HttpPost]
    [AllowAnonymous]
    public async Task<string> Login(LoginRequest request)
    {
        if (string.IsNullOrEmpty(request.Phone) || string.IsNullOrEmpty(request.PassWord))
            return "Account and Password is must.";

        string userId = "";
        IDatabase dataBase =null;
        try
        {
            //查询数据库校验参数
            var user = await _userManager.QueryUserByPhone(request.Phone);
            
            userId = user.Id.ToString();
            //密码加密
            var realPwd = PwdBCrypt.Encryption(request.PassWord);
            if (!user.Phone.Equals(request.Phone) || !user.PassWord.Equals(realPwd))
            {
                return "Account or Password wrong.";
            }

            //切库,token专门设立一个库
            dataBase = await _redisManager.SwitchDataBase(1);
            
            var x = await dataBase.StringGetAsync(userId);
            var token=x.ToString();
            if (string.IsNullOrEmpty(token))
            {
                //首次登录 ,生成token
                token = await _jwtService.GetToken(request.OrganizationId,user);
            }
            else
            {
                //校验token
                await _jwtService.VerifyToken(token);
                //刷新过期时间
                await _jwtService.RefreshToken(userId);
            }
            
            //将token 放到请求头
            Response.Headers.Add(new KeyValuePair<string, StringValues>("Authorization",token));
            
            return "Login success.";
        }
        catch (Exception e)
        {
            //删除token
            await dataBase.KeyDeleteAsync(userId);
            return "Login credentials have expired, please log in again";
        }
    }

    /// <summary>
    /// 加载页面的时候调用这个方法,然后判断是否有token.
    /// </summary>
    [HttpGet]
    public async Task<bool> NoPwdLogin()
    {
        try
        {
            //无密码登录,解析token信息
            string userToken = Request.Headers["token"].ToString();
            await _jwtService.AnalysisToken(userToken);
            return true;
        }
        catch (Exception e)
        {
            _logger.LogWarning("user token is valid,please login.");
            return false;
        }
    }


    /// <summary>
    /// 退出登录
    /// </summary>
    /// <param name="userId"></param>
    /// <returns></returns>
    [HttpGet]
    public async Task<IActionResult> Logout(int userId)
    {
        //删除redis中的token,页面跳转到登录页面
        await _redisManager.HashDeleteFiled(RedisKeyGenerator.UserToken(), userId.ToString());
        //页面跳转
        return new RedirectResult("/index/login");
    }

    [HttpGet]
    [Yes(Roles = "admin")]
    public void Test()
    {
        Console.WriteLine("in");
    }
}
先登录成功后,再调用Tes()方法,在请求头中携带上token参数   
     

其余辅助代码:

1. 登录密码加密:
(注意这里存在的坑,自定义Salt会出现BCrypt.Net.SaltParseException: Invalid salt version,随意可以先BCrypt.Net.BCrypt.GenerateSalt(),拿到salt后设置常量。)
public static class PwdBCrypt
{
    private const string PwdSalt = "$2a$11$jY8wPl8a0t6vaSQDz/Y1KO";
    
    /// <summary>
    /// 给密码加密
    /// </summary>
    /// <param name="pwd"></param>
    /// <returns></returns>
    public static string Encryption(string pwd)
    {
        return BCrypt.Net.BCrypt.HashPassword(pwd,PwdSalt);
    }
}

本篇文章只是简单实现JWT加过滤器的基本权限认证,后续会扩展为基于组织,用户,角色,权限模式的认证模式。

Remark:源代码正在整理,后续贴上。

  • 18
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值