.net core基于Oauth2+jwt两种方式实现身份认证(附单点登录)

基于.net core 3.1微服务架构的SSO单点登录实战

本文所涵盖的几个比较重要的知识点如下:

  • 资源请求管道模型
  • 依赖注入
  • 自定义中间件
  • 静态扩展
  • AOP

先看两幅图大概了解一下

Oauth2

Oauth2身份认证的四种方式
-------------------------------------分割线-----------------------------------------

JWT

JWT

由于篇幅所限,对与OAuth2和JWT的相关概念将不再赘述
核心代码分以下几个模块

数据仓储层,存放数据库实体以及数据库上下文

//当前登录用户实体
public class LoginUser
{
    public string UserId { get; set; }
    public string UserName { get; set; }
    public string Mobile { get; set; }
    /// <summary>
    /// 租户ID
    /// </summary>
    public string TenantId { get; set; }
}
//用户数据
public class AuthSysUser
{
    public string UserId { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public string Mobile { get; set; }
    public string TenantId { get; set; }
}
//用户token
public class AuthSysUserToken
{
    public string UserId { get; set; }
    public string Token { get; set; }
    public DateTime? ExpireTime { get; set; }
    public DateTime? UpdateTime { get; set; }
}
//数据库上下文,这里博主用的是sqlserver,当然你也可以用其他数据库实现
//右键依赖项管理NuGet程序包 搜索并添加 Microsoft.EntityFrameworkCore.SqlServer;
using Microsoft.EntityFrameworkCore;
public class AuthDbContext : DbContext
  {
      public AuthDbContext() { }
      public AuthDbContext(DbContextOptions<AuthDbContext> options)
        : base(options) { }

      public DbSet<AuthSysUserToken> SysUserTokens { get; set; }

      public DbSet<AuthSysUser> Users { get; set; }

  }

服务层,对于JWT和Oauth2提供接口约束和实现

 //定义接口
 public interface IAuthService
 {
     string ServiceName { get; }
     /// 判断权限
     Task<bool> PermissionAsync(string token, string path);
     /// 获取用户
     Task SetUserAsync(string token);
 }

//JWT实现
public class JwtAuthServiceImpl : IAuthService
{
    private readonly GlobalSettings _globalSettings;
    private readonly LoginUser _currentUser;

    public JwtAuthServiceImpl(IOptions<GlobalSettings> options, LoginUser currentUser)
    {
        _globalSettings = options.Value;
        
        _currentUser = currentUser;
    }

    public string ServiceName => nameof(JwtAuthServiceImpl);

    public Task<bool> PermissionAsync(string token, string path)
    {
        return Task.FromResult(JwtUtils.CheckToken(token, _globalSettings.Jwt.Secret));
    }

    public Task SetUserAsync(string token)
    {
        var payload = JwtUtils.GetPayload(token);
        _currentUser.UserId = payload.sub;
        return Task.CompletedTask;
    }
}

//Oauth2实现
public class OauthAuthServiceImpl : IAuthService
{
     private readonly AuthDbContext _passportDbContext;
     private readonly GlobalSettings _globalSettings;
     private readonly LoginUser _currentUser;

     public OauthAuthServiceImpl(AuthDbContext passportDbContext, IOptions<GlobalSettings> options, LoginUser currentUser)
     {
         _passportDbContext = passportDbContext;
         _globalSettings = options.Value;
         _currentUser = currentUser;
     }

     public string ServiceName => nameof(OauthAuthServiceImpl);

     /// <summary>
     /// 测试TOKEN
     /// </summary>
     public static string TEST_TOKEN = "73c8aa709d744848b3f4b697b48905ca";
     /// <summary>
     /// 测试用户
     /// </summary>
     public static string TEST_ADMIN = "a437a67c2de345a9a5c56ade745c0ecd";

     public async Task<bool> PermissionAsync(string token, string path)
     {
         if (token == null) throw new ArgumentNullException(nameof(token));
         if (path == null) throw new ArgumentNullException(nameof(path));

         if (token == TEST_TOKEN)
         {
             await SetUserAsync(token);
             return true;
         }
         //Token过期
         var tokenEntity = await _passportDbContext.SysUserTokens.Where(s => s.Token == token).FirstOrDefaultAsync();
         if (tokenEntity == null) return false;
         if (tokenEntity.ExpireTime != null && tokenEntity.ExpireTime <= DateTime.Now) return false;

         return true;
     }

     public async Task SetUserAsync(string token)
     {
         if (string.IsNullOrEmpty(_currentUser.UserId))
         {
             //TODO: 固定一个测试用户Token
             if (token == TEST_TOKEN)
             {
                 var user = new LoginUser
                 {
                     UserId = Guid.NewGuid().ToString("N"),
                     Mobile = "13012345678",
                     UserName = "Roy",
                     TenantId = DateTime.Now.ToString()
                 };
                 user.CopyTo(_currentUser);
             }
             else
             {
                 var user = await (from t in _passportDbContext.SysUserTokens.AsNoTracking()
                                   join u in _passportDbContext.Users.AsNoTracking()
                                   on t.UserId equals u.UserId
                                   where t.Token == token
                                   select new LoginUser
                                   {
                                       UserId = u.UserId,
                                       Mobile = u.Mobile,
                                       UserName = u.Username,
                                       TenantId = u.TenantId
                                   }).FirstOrDefaultAsync();
                 user?.CopyTo(_currentUser);
             }
         }
     }
 }

中间件层,用于服务注册在管道模型中

public sealed class LoginMiddlerware
{
     private readonly RequestDelegate _next;

     public LoginMiddlerware(RequestDelegate next)
     {
         _next = next;
     }

     /// <summary>
     /// 设置登录用户
     /// </summary>
     /// <param name="context"></param>
     /// <param name="_authService"></param>
     /// <returns></returns>
     public async Task InvokeAsync(HttpContext context, IEnumerable<IAuthService> _authService)
     {
         string token = GetToken();
         if (!string.IsNullOrEmpty(token))
         {
             if (token.StartsWith("Bearer") || token.Contains('.'))
             {
                 await _authService.First(a => a.ServiceName == nameof(JwtAuthServiceImpl)).SetUserAsync(token);
             }
             else
             {
                 await _authService.First(a => a.ServiceName == nameof(OauthAuthServiceImpl)).SetUserAsync(token);
             }
         }
         await _next.Invoke(context);
         string GetToken()
         {
             string token = context.Request.Headers["token"];
             if (string.IsNullOrEmpty(token))
             {
                 token = context.Request.Query["token"];
             }
             return token;
         }
     }
 }

 //过滤器依赖注入(AOP)
  public class ClaimRequirementAttribute
  {
       /// <summary>
       /// 自定义授权验证特性
       /// </summary>
       public class RequiresPermissionsAttribute : TypeFilterAttribute
       {
           public RequiresPermissionsAttribute(ClaimType claimType, string claimValue = "") : base(typeof(ClaimRequirementFilter))
           {
               Arguments = new object[] { new Claim(claimType.ToString(), claimValue) };
           }
       }

       /// <summary>
       /// 自定义授权验证过滤器
       /// </summary>
       public class ClaimRequirementFilter : IAuthorizationFilter
       {
           //授权声明
           readonly Claim _claim;
           //授权接口
           readonly IEnumerable<IAuthService> _authService;
           //全局配置类
           private readonly GlobalSettings _globalSettings;

           //构造函数注入
           public ClaimRequirementFilter(Claim claim, IEnumerable<IAuthService> authService, IOptions<GlobalSettings> _options)
           {
               _claim = claim;
               _authService = authService;
               _globalSettings = _options.Value;
           }

           public void OnAuthorization(AuthorizationFilterContext context)
           {
               //获取控制器描述符
               ControllerActionDescriptor controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
               if (controllerActionDescriptor != null)
               {
                   var skipAuthorization = controllerActionDescriptor.MethodInfo.GetCustomAttributes(inherit: true)
                       .Any(a => a.GetType().Equals(typeof(AllowAnonymousAttribute)));
                   //如果存在描述符则跳过验证 比如【AllowAnonymous】
                   if (skipAuthorization)
                   {
                       return;
                   }
               }

               //检查白名单
               if (_globalSettings.WhiteList != null && _globalSettings.WhiteList.Any(path => path == context.HttpContext.Request.Path.Value))
               {
                   return;
               }
               //声明类型
               ClaimType claimType = Enum.Parse<ClaimType>(_claim.Type);
               bool permission = false;
               //获取token
               string token = GetToken();
               if (string.IsNullOrEmpty(token))
               {
                   //返回401
                   context.Result = new UnauthorizedResult();
                   return;
               }
               if (claimType == ClaimType.Multiple)
               {
                   //根据Token类型选择认证方式
                   if (token.Any(t => t == '.'))
                   {
                       claimType = ClaimType.JWT;
                   }
                   else
                   {
                       claimType = ClaimType.Oauth2;
                   }
               }

               switch (claimType)
               {
                   case ClaimType.Oauth2:
                       permission = _authService.First(a => a.ServiceName == nameof(OauthAuthServiceImpl)).PermissionAsync(token, _claim.Value).Result;
                       break;
                   case ClaimType.JWT:
                       permission = _authService.First(a => a.ServiceName == nameof(JwtAuthServiceImpl)).PermissionAsync(token, _claim.Value).Result;
                       break;
                   default:
                       throw new Exception($"没有指定的授权方式:{claimType}");
               }

               if (!permission)
               {
                   //返回401
                   context.Result = new UnauthorizedResult();
                   return;
               }

               string GetToken()
               {
                   string token = context.HttpContext.Request.Headers["token"];
                   if (string.IsNullOrEmpty(token))
                   {
                       token = context.HttpContext.Request.Query["token"];
                   }
                   if (string.IsNullOrEmpty(token))
                   {
                       context.HttpContext.Request.Cookies.TryGetValue("token", out token);
                   }
                   return token;
               }
           }
       }
   }

    public enum ClaimType
    {
        Oauth2,
        JWT,
        Multiple
    }

附上JWT帮助类

using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace MyTest.Utils
{
    public static class JwtUtils
    {
        /// <summary>
        /// 生成token
        /// </summary>
        /// <param name="claims"></param>
        /// <returns></returns>
        public static string CreateToken(IEnumerable<Claim> claims, string securityKey)
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512);
            var securityToken = new JwtSecurityToken(
                issuer: null,
                audience: null,
                claims: claims,
                //expires: DateTime.Now.AddMinutes(settings.ExpMinutes),
                signingCredentials: creds);
            var token = new JwtSecurityTokenHandler().WriteToken(securityToken);
            return token;
        }


        /// <summary>
        /// 生成Jwt
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="roleName"></param>
        /// <param name="userId"></param>
        /// <returns></returns>
        public static string GenerateToken(string userId, string securityKey)
        {
            //声明claim
            var claims = new Claim[] {
                new Claim(JwtRegisteredClaimNames.Typ,"JWT"),
                new Claim(JwtRegisteredClaimNames.Sub, userId),
                new Claim(JwtRegisteredClaimNames.Iat,DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),ClaimValueTypes.Integer64),
                new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddDays(2).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), //过期时间
                new Claim("username","roy")
            };
            return CreateToken(claims, securityKey);
        }

        /// <summary>
        /// 从token中获取用户身份
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public static IEnumerable<Claim> GetClaims(string token)
        {
            var handler = new JwtSecurityTokenHandler();
            var securityToken = handler.ReadJwtToken(token);
            return securityToken?.Claims;
        }


        /// <summary>
        /// 从Token中获取用户身份
        /// </summary>
        /// <param name="token"></param>
        /// <param name="securityKey">securityKey明文,Java加密使用的是Base64</param>
        /// <returns></returns>
        public static ClaimsPrincipal GetPrincipal(string token, string securityKey)
        {
            try
            {
                var handler = new JwtSecurityTokenHandler();
                TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)),
                    ValidateLifetime = false
                };
                return handler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken);
            }
            catch (Exception ex)
            {
                return null;
            }
        }

        /// <summary>
        /// 校验Token
        /// </summary>
        /// <param name="token">token</param>
        /// <returns></returns>
        public static bool CheckToken(string token, string securityKey)
        {
            var principal = GetPrincipal(token, securityKey);
            if (principal is null)
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// 获取Token中的载荷数据
        /// </summary>
        /// <param name="token">token</param>
        /// <returns></returns>
        public static JwtPayload GetPayload(string token)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken securityToken = jwtHandler.ReadJwtToken(token);
            return new JwtPayload
            {
                sub = securityToken.Payload[JwtRegisteredClaimNames.Sub]?.ToString(),
                exp = DateTimeOffset.FromUnixTimeSeconds(long.Parse(securityToken.Payload[JwtRegisteredClaimNames.Exp].ToString())).ToLocalTime().DateTime,
                iat = securityToken.Payload[JwtRegisteredClaimNames.Iat]?.ToString()
            };
        }

        /// <summary>
        /// 获取Token中的载荷数据
        /// </summary>
        /// <typeparam name="T">泛型</typeparam>
        /// <param name="token">token</param>
        /// <returns></returns>
        public static T GetPayload<T>(string token)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(token);
            return JsonConvert.DeserializeObject<T>(jwtToken.Payload.SerializeToJson());
        }
    }

    /// <summary>
    /// Jwt载荷信息
    /// </summary>
    public class JwtPayload
    {
        public string sub { get; set; }

        public string iat { get; set; }

        public DateTime exp { get; set; }
    }
}

噢对了,还有静态扩展方法,通过泛型参数和反射用来实现cope两个对象的属性值

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;

namespace MyTest.Extension
{
    public static class ModelExtension
    {
        /// <summary>
        /// 反射实现两个类的对象之间相同属性的值的复制
        /// </summary>
        /// <typeparam name="Tag">返回的实体</typeparam>
        /// <typeparam name="Source">数据源实体</typeparam>
        /// <param name="tag">目标实体</param>
        /// <param name="sourct">数据源实体</param>
        /// <param name="ignoreId">忽略ID字段</param>
        /// <param name="ignoreEmpty">忽略空字段</param>
        /// <returns></returns>
        public static void CopyTo<Source, Tag>(this Source sourct, Tag tag, bool ignoreId = true, bool ignoreEmpty = false)
        {
            var Types = sourct.GetType();
            var TypeTag = typeof(Tag);
            foreach (PropertyInfo source in Types.GetProperties())
            {
                foreach (PropertyInfo tagProp in TypeTag.GetProperties())
                {
                    if (!(ignoreId && tagProp.Name.Equals("id", StringComparison.OrdinalIgnoreCase)) && tagProp.Name.Equals(source.Name, StringComparison.OrdinalIgnoreCase) && tagProp.PropertyType == source.PropertyType)
                    {
                        if (tagProp.CanWrite && source.GetValue(sourct) != null)
                        {
                            tagProp.SetValue(tag, source.GetValue(sourct, null), null);
                        }
                    }
                }
            }
        }
    }
}

OK核心代码上完,接下来就是重头戏了,我们都知道Startup启动类是整个项目的灵魂,里面包含了管道处理模型,中间件,以及各种服务的注入,于是乎对两个主要配置函数Configure和ConfigureServices进行扩展
Configure扩展

public static class GoAppExtensions
{
    public static IApplicationBuilder UseGo(this IApplicationBuilder app, IConfiguration configuration)
    {
        //允许body重用
        app.Use(next => context =>
        {
            context.Request.EnableBuffering();
            return next(context);
        });

        //设置基础路由ContextPath
        var config = configuration.GetValue<string>("GlobalSettings:ContextPath");

        if (config != null)
        {
            app.UsePathBase(new Microsoft.AspNetCore.Http.PathString($"/{config}"));
        }
        //路由匹配中间件,找到匹配的终结者路由Endpoint
        app.UseRouting();
        //启用 跨域 中间件
        app.UseCors();
        //登录中间件
        app.UseMiddleware(typeof(LoginMiddlerware));
        //针对 UseRouting 中间件中匹配到的路由进行拦截 做授权验证操作
        app.UseAuthorization();
        //终结者路由,针对 UseRouting 中间件匹配到的路由进行 委托方法的执行等操作
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
        return app;
    }
}

ConfigureServices扩展

 public static class GoServicesExtensions
 {
     public static IServiceCollection AddGoServices(this IServiceCollection services, IConfiguration configuration)
     {
         services.Configure<KestrelServerOptions>(x => x.AllowSynchronousIO = true);
         //添加对控制器以及与 API 相关的功能
         services.AddControllers();

         //跨域
         services.AddCors(options =>
         {
             options.AddDefaultPolicy(builder =>
             {
                 builder
                 .SetIsOriginAllowed(t => true)
                 .AllowAnyMethod()
                 .AllowAnyHeader()
                 .AllowCredentials();
             });
         });

         services.AddDbContext<AuthDbContext>(optionsBuilder =>
         {
             //数据库连接字符串,取配置文件,可自行更改
             optionsBuilder.UseSqlServer(configuration.GetConnectionString("passport"));
         });
         //当前登录用户实体
         services.AddScoped<LoginUser>();

         services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
         //注入配置类
         services.Configure<GlobalSettings>(configuration.GetSection("GlobalSettings"));
         //鉴权服务
         services.AddScoped<IAuthService, OauthAuthServiceImpl>();
         services.AddScoped<IAuthService, JwtAuthServiceImpl>();
         //ServiceLocator.SetLocatorProvider(services.BuildServiceProvider());
         return services;
     }
 }

Startup.cs

 public class Startup
 {
     public Startup(IConfiguration configuration)
     {
         Configuration = configuration;
     }

     public IConfiguration Configuration { get; }

     // This method gets called by the runtime. Use this method to add services to the container.
     public void ConfigureServices(IServiceCollection services)
     {
         services.AddGoServices(Configuration);
     }

     // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
     {
         app.Use(next => context =>
         {
             context.Request.EnableBuffering();
             return next(context);
         });
         app.UseGo(Configuration);
     }
 }

大功告成,整个框架就搭建完毕了,要想做单点登录的话可以在登录成功后把生成的token存储在数据库,设置一个有效期,然后把token返回到客户端存储,每次请求接口在请求头部带上token即可。

服务端如果有接口需要登录才能允许访问的,可以直接在Controller或者Action上面加上特性 [RequiresPermissions(ClaimType.Oauth2)]
不需要身份效验的Action上加上 [AllowAnonymous] (注意命名空间:using Microsoft.AspNetCore.Authorization)就OK了

最后附上项目源码Git地址

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值