欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ASP.NET Core 使用 JWT 自定义角色/策略授权需要实现的接口

程序员文章站 2022-05-03 10:45:50
[TOC] ① 存储角色/用户所能访问的 API 例如 使用 存储角色的授权 API 列表。 可有可无。 可以把授权访问的 API 存放到 Token 中,Token 也可以只存放角色信息和用户身份信息。 ② 实现 IAuthorizationRequirement 接口 接口代表了用户的身份信息, ......

① 存储角色/用户所能访问的 api

例如

使用 list<apipermission>存储角色的授权 api 列表。

可有可无。

可以把授权访问的 api 存放到 token 中,token 也可以只存放角色信息和用户身份信息。

    /// <summary>
    /// api
    /// </summary>
    public class apipermission
    {
        /// <summary>
        /// api名称
        /// </summary>
        public virtual string name { get; set; }
        /// <summary>
        /// api地址
        /// </summary>
        public virtual string url { get; set; }
    }

② 实现 iauthorizationrequirement 接口

iauthorizationrequirement 接口代表了用户的身份信息,作为认证校验、授权校验使用。

事实上,iauthorizationrequirement 没有任何要实现的内容。

namespace microsoft.aspnetcore.authorization
{
    //
    // 摘要:
    //     represents an authorization requirement.
    public interface iauthorizationrequirement
    {
    }
}

实现 iauthorizationrequirement ,可以任意定义需要的属性,这些会作为自定义验证的便利手段。

    //iauthorizationrequirement 是 microsoft.aspnetcore.authorization 接口

    /// <summary>
    /// 用户认证信息必要参数类
    /// </summary>
    public class permissionrequirement : iauthorizationrequirement
    {
        /// <summary>
        /// 用户所属角色
        /// </summary>
        public role roles { get;  set; } = new role();
        public void setrolesname(string rolename)
        {
            roles.name = rolename;
        }
        /// <summary>
        /// 无权限时跳转到此api
        /// </summary>
        public string deniedaction { get; set; }

        /// <summary>
        /// 认证授权类型
        /// </summary>
        public string claimtype { internal get; set; }
        /// <summary>
        /// 未授权时跳转
        /// </summary>
        public string loginpath { get; set; } = "/account/login";
        /// <summary>
        /// 发行人
        /// </summary>
        public string issuer { get; set; }
        /// <summary>
        /// 订阅人
        /// </summary>
        public string audience { get; set; }
        /// <summary>
        /// 过期时间
        /// </summary>
        public timespan expiration { get; set; }
        /// <summary>
        /// 颁发时间
        /// </summary>
        public long issuedtime { get; set; }
        /// <summary>
        /// 签名验证
        /// </summary>
        public signingcredentials signingcredentials { get; set; }

        /// <summary>
        /// 构造
        /// </summary>
        /// <param name="deniedaction">无权限时跳转到此api</param>
        /// <param name="userpermissions">用户权限集合</param>
        /// <param name="deniedaction">拒约请求的url</param>
        /// <param name="permissions">权限集合</param>
        /// <param name="claimtype">声明类型</param>
        /// <param name="issuer">发行人</param>
        /// <param name="audience">订阅人</param>
        /// <param name="issusedtime">颁发时间</param>
        /// <param name="signingcredentials">签名验证实体</param>
        public permissionrequirement(string deniedaction, role role, string claimtype, string issuer, string audience, signingcredentials signingcredentials,long issusedtime, timespan expiration)
        {
            claimtype = claimtype;
            deniedaction = deniedaction;
            roles = role;
            issuer = issuer;
            audience = audience;
            expiration = expiration;
            issuedtime = issusedtime;
            signingcredentials = signingcredentials;
        }
    }

③ 实现 tokenvalidationparameters

token 的信息配置

        public static tokenvalidationparameters gettokenvalidationparameters()
        {
            var tokenvalida = new tokenvalidationparameters
            {
                // 定义 token 内容
                validateissuersigningkey = true,
                issuersigningkey = new symmetricsecuritykey(encoding.utf8.getbytes(authconfig.securitykey)),
                validateissuer = true,
                validissuer = authconfig.issuer,
                validateaudience = true,
                validaudience = authconfig.audience,
                validatelifetime = true,
                clockskew = timespan.zero,
                requireexpirationtime = true
            };
            return tokenvalida;
        }

④ 生成 token

用于将用户的身份信息(claims)和角色授权信息(permissionrequirement)存放到 token 中。

        /// <summary>
        /// 获取基于jwt的token
        /// </summary>
        /// <param name="username"></param>
        /// <returns></returns>
        public static dynamic buildjwttoken(claim[] claims, permissionrequirement permissionrequirement)
        {
            var now = datetime.utcnow;
            var jwt = new jwtsecuritytoken(
                issuer: permissionrequirement.issuer,
                audience: permissionrequirement.audience,
                claims: claims,
                notbefore: now,
                expires: now.add(permissionrequirement.expiration),
                signingcredentials: permissionrequirement.signingcredentials
            );
            var encodedjwt = new jwtsecuritytokenhandler().writetoken(jwt);
            var response = new
            {
                status = true,
                access_token = encodedjwt,
                expires_in = permissionrequirement.expiration.totalmilliseconds,
                token_type = "bearer"
            };
            return response;
        }

⑤ 实现服务注入和身份认证配置

从别的变量导入配置信息,可有可无

            // 设置用于加密 token 的密钥
            // 配置角色权限 
            var rolerequirement = rolepermission.getrolerequirement(accounthash.gettokensecuritykey());

            // 定义如何生成用户的 token
            var tokenvalidationparameters = rolepermission.gettokenvalidationparameters();

配置 asp.net core 的身份认证服务

需要实现三个配置

  • addauthorization 导入角色身份认证策略
  • addauthentication 身份认证类型
  • addjwtbearer jwt 认证配置
            // 导入角色身份认证策略
            services.addauthorization(options =>
            {
                options.addpolicy("permission",
                   policy => policy.requirements.add(rolerequirement));


                // ↓ 身份认证类型
            }).addauthentication(options =>
            {
                options.defaultauthenticatescheme = jwtbearerdefaults.authenticationscheme;
                options.defaultscheme = jwtbearerdefaults.authenticationscheme;
                options.defaultchallengescheme = jwtbearerdefaults.authenticationscheme;

                // ↓ jwt 认证配置
            })
            .addjwtbearer(options =>
            {
                options.tokenvalidationparameters = tokenvalidationparameters;
                options.savetoken = true;
                options.events = new jwtbearerevents()
                {
                    // 在安全令牌通过验证和claimsidentity通过验证之后调用
                    // 如果用户访问注销页面
                    ontokenvalidated = context =>
                    {
                        if (context.request.path.value.tostring() == "/account/logout")
                        {
                            var token = ((context as tokenvalidatedcontext).securitytoken as jwtsecuritytoken).rawdata;
                        }
                        return task.completedtask;
                    }
                };
            });

注入自定义的授权服务 permissionhandler

注入自定义认证模型类 rolerequirement

            // 添加 httpcontext 拦截
            services.addsingleton<iauthorizationhandler, permissionhandler>();

            services.addsingleton(rolerequirement);

添加中间件

貌似这两个不区分先后顺序

            app.useauthorization();
            app.useauthentication();

⑥ 实现登陆

可以在颁发 token 时把能够使用的 api 存储进去,但是这种方法不适合 api 较多的情况。

可以存放 用户信息(claims)和角色信息,后台通过角色信息获取授权访问的 api 列表。

        /// <summary>
        /// 登陆
        /// </summary>
        /// <param name="username">用户名</param>
        /// <param name="password">密码</param>
        /// <returns>token信息</returns>
        [httppost("login")]
        public jsonresult login(string username, string password)
        {
            var user = usermodel.users.firstordefault(x => x.username == username && x.userpossword == password);
            if (user == null)
                return new jsonresult(
                    new responsemodel
                    {
                        code = 0,
                        message = "登陆失败!"
                    });


            // 配置用户标识
            var userclaims = new claim[]
            {
                new claim(claimtypes.name,user.username),
                new claim(claimtypes.role,user.role),
                new claim(claimtypes.expiration,datetime.now.addminutes(_requirement.expiration.totalminutes).tostring()),
            };
            _requirement.setrolesname(user.role);
            // 生成用户标识
            var identity = new claimsidentity(jwtbearerdefaults.authenticationscheme);
            identity.addclaims(userclaims);

            var token = jwttoken.buildjwttoken(userclaims, _requirement);

            return new jsonresult(
                new responsemodel
                {
                    code = 200,
                    message = "登陆成功!请注意保存你的 token 凭证!",
                    data = token
                });
        }

⑦ 添加 api 授权策略

    [authorize(policy = "permission")]

⑧ 实现自定义授权校验

要实现自定义 api 角色/策略授权,需要继承 authorizationhandler<trequirement>

里面的内容是完全自定义的, authorizationhandlercontext 是认证授权的上下文,在此实现自定义的访问授权认证。

也可以加上自动刷新 token 的功能。

    /// <summary>
    /// 验证用户信息,进行权限授权handler
    /// </summary>
    public class permissionhandler : authorizationhandler<permissionrequirement>
    {
        protected override task handlerequirementasync(authorizationhandlercontext context,
                                                       permissionrequirement requirement)
        {
            list<permissionrequirement> requirements = new list<permissionrequirement>();
            foreach (var item in context.requirements)
            {
                requirements.add((permissionrequirement)item);
            }
            foreach (var item in requirements)
            {
                // 校验 颁发和接收对象
                if (!(item.issuer == authconfig.issuer ?
                    item.audience == authconfig.audience ?
                    true : false : false))
                {
                    context.fail();
                }
                // 校验过期时间
                var nowtime = datetimeoffset.now.tounixtimeseconds();
                var issued = item.issuedtime +convert.toint64(item.expiration.totalseconds);
                if (issued < nowtime)
                    context.fail();



                // 是否有访问此 api 的权限
                var resource = ((microsoft.aspnetcore.routing.routeendpoint)context.resource).routepattern;
                var permissions = item.roles.permissions.tolist();
                var apis = permissions.any(x => x.name.tolower() == item.roles.name.tolower() && x.url.tolower() == resource.rawtext.tolower());
                if (!apis)
                    context.fail();

                context.succeed(requirement);
                // 无权限时跳转到某个页面
                //var httpcontext = new httpcontextaccessor();
                //httpcontext.httpcontext.response.redirect(item.deniedaction);
            }

            context.succeed(requirement);
            return task.completedtask;
        }
    }

⑨ 一些有用的代码

将字符串生成哈希值,例如密码。

为了安全,删除字符串里面的特殊字符,例如 "'$

    public static class accounthash
    {

        // 获取字符串的哈希值
        public static string getbyhashstring(string str)
        {
            string hash = getmd5hash(str.replace("\"", string.empty)
                .replace("\'", string.empty)
                .replace("$", string.empty));
            return hash;
        }
        /// <summary>
        /// 获取用于加密 token 的密钥
        /// </summary>
        /// <returns></returns>
        public static signingcredentials gettokensecuritykey()
        {
            var securitykey = new signingcredentials(
                new symmetricsecuritykey(
                    encoding.utf8.getbytes(authconfig.securitykey)), securityalgorithms.hmacsha256);
            return securitykey;
        }
        private static string getmd5hash(string source)
        {
            md5 md5hash = md5.create();
            byte[] data = md5hash.computehash(encoding.utf8.getbytes(source));
            stringbuilder sbuilder = new stringbuilder();
            for (int i = 0; i < data.length; i++)
            {
                sbuilder.append(data[i].tostring("x2"));
            }
            return sbuilder.tostring();
        }
    }

签发 token

permissionrequirement 不是必须的,用来存放角色或策略认证信息,claims 应该是必须的。

    /// <summary>
    /// 颁发用户token
    /// </summary>
    public class jwttoken
    {
        /// <summary>
        /// 获取基于jwt的token
        /// </summary>
        /// <param name="username"></param>
        /// <returns></returns>
        public static dynamic buildjwttoken(claim[] claims, permissionrequirement permissionrequirement)
        {
            var now = datetime.utcnow;
            var jwt = new jwtsecuritytoken(
                issuer: permissionrequirement.issuer,
                audience: permissionrequirement.audience,
                claims: claims,
                notbefore: now,
                expires: now.add(permissionrequirement.expiration),
                signingcredentials: permissionrequirement.signingcredentials
            );
            var encodedjwt = new jwtsecuritytokenhandler().writetoken(jwt);
            var response = new
            {
                status = true,
                access_token = encodedjwt,
                expires_in = permissionrequirement.expiration.totalmilliseconds,
                token_type = "bearer"
            };
            return response;
        }

表示时间戳

// unix 时间戳
datetimeoffset.now.tounixtimeseconds();

// 检验 token 是否过期
// 将 timespan 转为 unix 时间戳
convert.toint64(timespan);
datetimeoffset.now.tounixtimeseconds() + convert.toint64(timespan);