受欢迎的博客标签

ASP.NET Core Web API Authentication series:认证授权方式Jwt自定义实现

Published

Version:ASP.NET Core 2.x

Core3.1WebApi_Filter-Authorize详解

https://mp.weixin.qq.com/s?__biz=MzU4Mjc4NzgyOQ==&mid=2247484813&idx=1&sn=7e59d67efac66d56da8d2455694581cd&utm_source=tuicool&utm_medium=referral

 

思路如下:

step 1:用户通过web api 提交appid和secret

step 2:web api 使用代码登录,同时设置UserName,roles,expire过期时间

step 3:web api 生成token,并将token (key)和该用户信息UserInfo (value)存入Redis server,并设置对应的过期时间。

 

step 4:用户携带token访问Web Api

step 5:从token中获取用户信息

step 6:用中间件判断该用户是否已经登录

 

读取 Http Request Header 中的 Authorization 信息
读取 Authorization 值里面的 Bearer 信息
验证 Bearer 是否合法,会得到一个 ClaimsPrincipal
使用 ClaimsPrincipal 构建一个 Ticket(票据)
调用 Options.Events.TokenValidated(context),用户可以重写此方法验证Token合法性
返回验证成功

 
在普通的MVC项目中 我们普遍的使用Cookie来作为认证授权方式,使用简单。登录成功后将用户信息写入Cookie;但当我们做WebApi的时候显然Cookie这种方式就有点不适用了。

在.NET Core 中 WebApi中目前比较流行的认证授权方式是Jwt  (Json Web Token) 技术。Jwt 是一种无状态的分布式身份验证方式,Jwt 是将用户登录信息加密后存放到返回的Token中 ,相当于用户信息是存储在客户端。

JWT它是一个很长的字符串,中间用点(.)分隔成三个部分。

JWT 的三个部分依次如下:

 
Header(头部)
Payload(负载)
Signature(签名)

Jwt的加密方式有两种 :对称加密与非对称加密,非对称加密即 RSA 加密的方式

自己手写认证授权代码和Jwt的思路是一样的;不同之处在于:

1、加密方式仅仅是采用的对称加密方式 简单高效。哈哈!(弊端就是没有非对称加密更安全);

2、用户登录信息主要保存在Redis中,即服务端。

自己写的好处:

1、扩展性强,可根据自己的需要进行各种扩展,比如在验证授权时可很方便的添加多设备登录挤下线功能等。

2、可随时调整用户的Token失效时间。

认证及授权流程

1、先请求登录接口,登录成功,为用户产生一个Token,

登录获取Token 图片中ticket字段。

 

2、 客户端拿到Token在其他请求中将Token信息添加到请求头中传递到服务端。

开发思路

1、添加一个过滤器。在Startup 中ConfigureServices方法里添加一个Filters 即我们自己授权代码类。

 
public void ConfigureServices(IServiceCollection services)
 {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            services.AddMvc(mvc =>
            {
                //添加自己的授权验证
                mvc.Filters.Add(typeof(AuthorizeFilter));
            });
}

 

 添加过滤器之后我们的每次请求都会优先执行过滤器的代码。在这里我们就可以判断用户是否已经登录,从而进行拦截没有授权的的请求。

/// <summary>
    /// 安全认证过滤器
    /// </summary>
    public class AuthorizeFilter : IActionFilter, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            //允许匿名访问
            if (context.HttpContext.User.Identity.IsAuthenticated ||
                context.Filters.Any(item => item is IAllowAnonymousFilter))
                return;
            var httpContext = context.HttpContext;
            var claimsIdentity = httpContext.User.Identity as ClaimsIdentity;
            var request = context.HttpContext.Request;
            var authorization = request.Headers["Authorization"].ToString();
            if (authorization != null && authorization.Contains("BasicAuth"))
            {
                //获取请求头中传递的ticket
                var current_ticket = authorization.Split(" ")[1];
                //校验ticket并获取用户信息
                var userInfo = TicketEncryption.VerifyTicket(current_ticket, out string dec_client);
                if (userInfo != null)
                {
                    //同一个终端多次登录挤下线功能 返回403
                    if (userInfo.ticket != current_ticket && userInfo.client.ToString() == dec_client)
                    {
                        #region 多设备挤下线代码
                        var response = new HttpResponseMessage();
                        context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                        context.Result = new JsonResult("Forbidden:The current authorization has expired");
                        #endregion

                        return;
                    }
                    else
                    {
                        return;
                    }
                }

            }
            // 401 未授权
            context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
            context.Result = new JsonResult("Forbidden:Tiket Invalid");

        }
    }

2、登录并获取Token

由于添加了IAuthorizationFilter类型的过滤器,所以每个请求都会被拦截。所以登录接口我们需要允许匿名访问。

3、加解密Token

加密

加密:登录成功后就要产生个Token了,产生也简单。将用户的唯一信息比如uid或者guid进行对称式加密。当然如果需要对登录设备做区分或者多设备登录挤下线功能时最好也将登录设备一起加密进去。

我们都知道 在加密中一般情况下只要加密的数据及加密key不变;那么加密后的内容也会一直保持不变。如果我们每次登录产生的Token一直没有任何变化只要这个Token被泄露了那将很危险的。竟然我们希望每次登录产生的Token都有变化。那就要改变加密数据或者加密key了。加密数据是用户唯一信息这个显然不可能产生变化。所以我们能改变的地方只能是加密key了;我们采用固定key+随机key的方式。

因为加密key在我们解密时也是需要一一对应的。所以我们得想办法将我们的随机key告诉我们解密的代码中。办法就是 我们将加密后的内容(一般情况进行base64编码)再加上随机key。(随机key一定是固定长度 不然后面无法解析拆分)

比如加密内容是

guid=73e01eab-210d-4d19-a72a-d0d64e053ec0+client=ios  固定key=123654+随机key=FEZaaWbyimaWiJHah 

即加密过程:

加密(73e01eab-210d-4d19-a72a-d0d64e053ec0&ios,123654FEZaaWbyimaWiJHah)=M0EzM0ZGRjk2QzgwRDY2RDJDMTdFOEJGRUE0NDI3NEE1RDlFNkU4NDQ0MERFNEIyMkQ5QjM4MjAxODcwj加随机key:FEZaaWbyimaWiJHah

所以我们返回给用户的Token实际上是包含了随机key的。当然这个随机key只有我们自己知道。因为随机key的长度以及位置只有我们自己知道。这种方式即使我们固定key被泄露了 只要别人不知道我们随机key处理方法也无济于事。

解密

解密:知道加密过程后就好解密了。拿到用户提交的Token后首先按照随机key的固定位置进行截取。将加密内容和随机key拆开。然后将固定key和随机key组合一起解密加密的内容,取得用户guid和登录的客户端类型。

 

完整加解密代码

代码中的ticket代表本文中的Token。代码中使用的是DES加解密

public class TicketEncryption
    {
        //加密key 实际中请用配置文件配置
        private static readonly string key = "yvDlky7GXGtlPCGr";
        /// <summary>
        /// 获取一个新的ticket
        /// </summary>
        /// <param name="guid">用户的guid</param>
        /// <param name="client">客户端</param>
        /// <returns></returns>
        public static string GenerateTicket(string guid, string client)
        {
            //随机key
            string randomKey = Randoms.GetRandomString(15);
            var keys = key + randomKey;
            var desStr = Encryption.DesEncrypt(guid + "&" + client, keys);
            var base64Str = Encryption.Base64Encrypt(desStr) + randomKey;
            return base64Str;
        }

        /// <summary>
        /// 校验ticket
        /// </summary>
        /// <param name="encryptStr"></param>
        /// <returns></returns>
        public static UserInfo VerifyTicket(string encryptStr,out string client)
        {
            try
            {
                RedisHelper redisHelper = new RedisHelper("127.0.0.1:6379");
                //加密原型:guid&client; 如:08e80f78-95ad-427c-b506-a5f1504e29ac&ios
                string randomKey = encryptStr.Substring(encryptStr.Length - 15);
                var base64 = encryptStr.Substring(0, encryptStr.Length - 15);
                var deBase64 = Encryption.Base64Decrypt(base64);
                var keys = key + randomKey;
                string ticketInfo = Encryption.DesDecrypt(deBase64, keys);
                var guid = ticketInfo.Split("&")[0];
                client = ticketInfo.Split("&")[1];
                string redisKey = "ticket_" + guid;
                var obj = redisHelper.Get<UserInfo>(redisKey);
                return obj;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
    }
 

 

完整demo代码请看github

https://github.com/cfan1236/NetCoreAuthorize