GVKun编程网logo

【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

20

想了解【.NETCore项目实战-统一认证平台】第十四章授权篇-自定义授权方式的新动态吗?本文将为您提供详细的信息,此外,我们还将为您介绍关于.NetCore5中的自定义授权属性、.NETCore中实

想了解【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式的新动态吗?本文将为您提供详细的信息,此外,我们还将为您介绍关于.Net Core 5 中的自定义授权属性、.NET Core中 实现H5微信登录(静默授权方式)、Asp.net core 2.2 Cookie验证 自定义授权 过滤器(注入方法)、asp.net Core 中AuthorizationHandler 实现自定义授权的新知识。

本文目录一览:

【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

原文: 【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

【.NET Core项目实战-统一认证平台】开篇及目录索引

上篇文章我介绍了如何强制令牌过期的实现,相信大家对IdentityServer4的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方式集成老的业务系统验证,然后根据不同的客户端使用不同的认证方式来集成到统一认证平台。

.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。

一、自定授权源码剖析

当我们需要使用开源项目的某些功能时,最好了解实现的原理,才能正确和熟练使用功能,避免出现各种未知bug问题和出现问题无法解决的被动场面。

在使用此功能前,我们需要了解完整的实现流程,下面我将从源码开始讲解IdentityServer4是如何实现自定义的授权方式。

从我之前的文章中我们知道授权方式是通过Grant_Type的值来判断的,所以我们自定义的授权方式,也是通过此值来区分,所以需要了解自定义的值处理流程。TokenRequestValidator是请求验证的方法,除了常规验证外,还增加了自定义的验证方式。

public async Task<TokenRequestValidationResult> ValidateRequestAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult)
{
    _logger.LogDebug("Start token request validation");

    _validatedRequest = new ValidatedTokenRequest
    {
        Raw = parameters ?? throw new ArgumentNullException(nameof(parameters)),
        Options = _options
    };

    if (clientValidationResult == null) throw new ArgumentNullException(nameof(clientValidationResult));

    _validatedRequest.SetClient(clientValidationResult.Client, clientValidationResult.Secret, clientValidationResult.Confirmation);

    /////////////////////////////////////////////
    // check client protocol type
    /////////////////////////////////////////////
    if (_validatedRequest.Client.ProtocolType != IdentityServerConstants.ProtocolTypes.OpenIdConnect)
    {
        LogError("Client {clientId} has invalid protocol type for token endpoint: expected {expectedProtocolType} but found {protocolType}",
                 _validatedRequest.Client.ClientId,
                 IdentityServerConstants.ProtocolTypes.OpenIdConnect,
                 _validatedRequest.Client.ProtocolType);
        return Invalid(OidcConstants.TokenErrors.InvalidClient);
    }

    /////////////////////////////////////////////
    // check grant type
    /////////////////////////////////////////////
    var grantType = parameters.Get(OidcConstants.TokenRequest.GrantType);
    if (grantType.IsMissing())
    {
        LogError("Grant type is missing");
        return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
    }

    if (grantType.Length > _options.InputLengthRestrictions.GrantType)
    {
        LogError("Grant type is too long");
        return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
    }

    _validatedRequest.GrantType = grantType;

    switch (grantType)
    {
        case OidcConstants.GrantTypes.AuthorizationCode:
            return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters);
        case OidcConstants.GrantTypes.ClientCredentials:
            return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters);
        case OidcConstants.GrantTypes.Password:
            return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters);
        case OidcConstants.GrantTypes.RefreshToken:
            return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters);
        default://统一的自定义的验证方式
            return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters);
    }
}

从上面代码可以看出,除了内置的授权方式,其他的都是用ValidateExtensionGrantRequestAsync来进行验证,详细的验证规则继续分析实现过程。

private async Task<TokenRequestValidationResult> ValidateExtensionGrantRequestAsync(NameValueCollection parameters)
{
    _logger.LogDebug("Start validation of custom grant token request");

    /////////////////////////////////////////////
    // 校验客户端是否开启了此授权方式
    /////////////////////////////////////////////
    if (!_validatedRequest.Client.AllowedGrantTypes.Contains(_validatedRequest.GrantType))
    {
        LogError("{clientId} does not have the custom grant type in the allowed list, therefore requested grant is not allowed", _validatedRequest.Client.ClientId);
        return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
    }

    /////////////////////////////////////////////
    // 判断是否注入了此自定义的授权实现
    /////////////////////////////////////////////
    if (!_extensionGrantValidator.GetAvailableGrantTypes().Contains(_validatedRequest.GrantType, StringComparer.Ordinal))
    {
        LogError("No validator is registered for the grant type: {grantType}", _validatedRequest.GrantType);
        return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
    }

    /////////////////////////////////////////////
    // 校验是否支持scope
    /////////////////////////////////////////////
    if (!await ValidateRequestedScopesAsync(parameters))
    {
        return Invalid(OidcConstants.TokenErrors.InvalidScope);
    }

    /////////////////////////////////////////////
    // 调用自定义的验证实现方法
    /////////////////////////////////////////////
    var result = await _extensionGrantValidator.ValidateAsync(_validatedRequest);

    if (result == null)
    {
        LogError("Invalid extension grant");
        return Invalid(OidcConstants.TokenErrors.InvalidGrant);
    }

    if (result.IsError)
    {
        if (result.Error.IsPresent())
        {
            LogError("Invalid extension grant: {error}", result.Error);
            return Invalid(result.Error, result.ErrorDescription, result.CustomResponse);
        }
        else
        {
            LogError("Invalid extension grant");
            return Invalid(OidcConstants.TokenErrors.InvalidGrant, customResponse: result.CustomResponse);
        }
    }

    if (result.Subject != null)
    {
        /////////////////////////////////////////////
        // 判断当前的用户是否可用
        /////////////////////////////////////////////
        var isActiveCtx = new IsActiveContext(
            result.Subject,
            _validatedRequest.Client,
            IdentityServerConstants.ProfileIsActiveCallers.ExtensionGrantValidation);

        await _profile.IsActiveAsync(isActiveCtx);

        if (isActiveCtx.IsActive == false)
        {
            // todo: raise event?

            LogError("User has been disabled: {subjectId}", result.Subject.GetSubjectId());
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }

        _validatedRequest.Subject = result.Subject;
    }

    _logger.LogDebug("Validation of extension grant token request success");
    return Valid(result.CustomResponse);
}

从代码中可以看出,实现流程如下:

  • 1、客户端是否配置了自定义的授权方式。
  • 2、是否注入了自定义的授权实现。
  • 3、授权的scope客户端是否有权限。
  • 4、使用自定义的授权验证方式校验请求数据是否合法。
  • 5、判断是否有有效数据信息,可自行实现接口。

从源码中,可以发现流程已经非常清晰了,核心类ExtensionGrantValidator实现了自定义授权的校验过程,进一步分析下此类的代码实现。

using IdentityServer4.Models;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace IdentityServer4.Validation
{
    /// <summary>
    /// Validates an extension grant request using the registered validators
    /// </summary>
    public class ExtensionGrantValidator
    {
        private readonly ILogger _logger;
        private readonly IEnumerable<IExtensionGrantValidator> _validators;

        /// <summary>
        /// Initializes a new instance of the <see cref="ExtensionGrantValidator"/> class.
        /// </summary>
        /// <param name="validators">The validators.</param>
        /// <param name="logger">The logger.</param>
        public ExtensionGrantValidator(IEnumerable<IExtensionGrantValidator> validators, ILogger<ExtensionGrantValidator> logger)
        {
            if (validators == null)
            {
                _validators = Enumerable.Empty<IExtensionGrantValidator>();
            }
            else
            {
                _validators = validators;
            }

            _logger = logger;
        }

        /// <summary>
        /// Gets the available grant types.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<string> GetAvailableGrantTypes()
        {
            return _validators.Select(v => v.GrantType);
        }

        /// <summary>
        /// Validates the request.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<GrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
        {
            var validator = _validators.FirstOrDefault(v => v.GrantType.Equals(request.GrantType, StringComparison.Ordinal));

            if (validator == null)
            {
                _logger.LogError("No validator found for grant type");
                return new GrantValidationResult(TokenRequestErrors.UnsupportedGrantType);
            }

            try
            {
                _logger.LogTrace("Calling into custom grant validator: {type}", validator.GetType().FullName);

                var context = new ExtensionGrantValidationContext
                {
                    Request = request
                };
            
                await validator.ValidateAsync(context);
                return context.Result;
            }
            catch (Exception e)
            {
                _logger.LogError(1, e, "Grant validation error: {message}", e.Message);
                return new GrantValidationResult(TokenRequestErrors.InvalidGrant);
            }
        }
    }
}

从上面代码可以发现,自定义授权方式,只需要实现IExtensionGrantValidator接口即可,然后支持多个自定义授权方式的共同使用。

到此整个验证过程解析完毕了,然后再查看下生成Token流程,实现方法为TokenResponseGenerator,这个方法并不陌生,前几篇介绍不同的授权方式都介绍了,所以直接看实现代码。

public virtual async Task<TokenResponse> ProcessAsync(TokenRequestValidationResult request)
{
    switch (request.ValidatedRequest.GrantType)
    {
        case OidcConstants.GrantTypes.ClientCredentials:
            return await ProcessClientCredentialsRequestAsync(request);
        case OidcConstants.GrantTypes.Password:
            return await ProcessPasswordRequestAsync(request);
        case OidcConstants.GrantTypes.AuthorizationCode:
            return await ProcessAuthorizationCodeRequestAsync(request);
        case OidcConstants.GrantTypes.RefreshToken:
            return await ProcessRefreshTokenRequestAsync(request);
        default://自定义授权生成Token的方式
            return await ProcessExtensionGrantRequestAsync(request);
    }
}

protected virtual Task<TokenResponse> ProcessExtensionGrantRequestAsync(TokenRequestValidationResult request)
{
    Logger.LogTrace("Creating response for extension grant request");
    return ProcessTokenRequestAsync(request);
}

实现的代码方式和客户端模式及密码模式一样,这里就不多介绍了。

最后我们查看下是如何注入IExtensionGrantValidator,是否对外提供接入方式,发现IdentityServer4提供了AddExtensionGrantValidator扩展方法,我们自己实现自定义授权后添加即可,详细实现代码如下。

public static IIdentityServerBuilder AddExtensionGrantValidator<T>(this IIdentityServerBuilder builder)
            where T : class, IExtensionGrantValidator
        {
            builder.Services.AddTransient<IExtensionGrantValidator, T>();
            return builder;
        }

二、自定义授权实现

现在开始开发第一个自定义授权方式,GrantType定义为CzarCustomUser,然后实现IExtensionGrantValidator接口,为了演示方便,我新建一个测试用户表,用来模拟老系统的登录方式。

Create Table CzarCustomUser
(
    iid int identity,
    username varchar(50),
    usertruename varchar(50),
    userpwd varchar(100)
)
--插入测试用户密码信息,测试数据密码不加密
insert into CzarCustomUser values(''jinyancao'',''金焰的世界'',''777777'')

然后把实现验证的方法,由于代码太简单,我就直接贴代码如下。

namespace Czar.AuthPlatform.Web.Application.IRepository
{
    public interface ICzarCustomUserRepository
    {
        /// <summary>
        /// 根据账号密码获取用户实体
        /// </summary>
        /// <param name="uaccount">账号</param>
        /// <param name="upassword">密码</param>
        /// <returns></returns>
        CzarCustomUser FindUserByuAccount(string uaccount, string upassword);
    }
}

namespace Czar.AuthPlatform.Web.Application.Repository
{
    public class CzarCustomUserRepository : ICzarCustomUserRepository
    {
        private readonly string DbConn = "";
        public CzarCustomUserRepository(IOptions<CzarConfig> czarConfig)
        {
            DbConn = czarConfig.Value.DbConnectionStrings;
        }

        /// <summary>
        /// 根据账号密码获取用户实体
        /// </summary>
        /// <param name="uaccount">账号</param>
        /// <param name="upassword">密码</param>
        /// <returns></returns>
        public CzarCustomUser FindUserByuAccount(string uaccount, string upassword)
        {
            using (var connection = new SqlConnection(DbConn))
            {
                string sql = @"SELECT * from CzarCustomUser where username=@uaccount and userpwd=upassword ";
                var result = connection.QueryFirstOrDefault<CzarCustomUser>(sql, new { uaccount, upassword });
                return result;
            }
        }
    }
}

namespace Czar.AuthPlatform.Web.Application.IServices
{
    public interface ICzarCustomUserServices
    {
        /// <summary>
        /// 根据账号密码获取用户实体
        /// </summary>
        /// <param name="uaccount">账号</param>
        /// <param name="upassword">密码</param>
        /// <returns></returns>
        CzarCustomUser FindUserByuAccount(string uaccount, string upassword);
    }
}

namespace Czar.AuthPlatform.Web.Application.Services
{
    public class CzarCustomUserServices: ICzarCustomUserServices
    {
        private readonly ICzarCustomUserRepository czarCustomUserRepository;
        public CzarCustomUserServices(ICzarCustomUserRepository czarCustomUserRepository)
        {
            this.czarCustomUserRepository = czarCustomUserRepository;
        }

        /// <summary>
        /// 根据账号密码获取用户实体
        /// </summary>
        /// <param name="uaccount">账号</param>
        /// <param name="upassword">密码</param>
        /// <returns></returns>
        public CzarCustomUser FindUserByuAccount(string uaccount, string upassword)
        {
            return czarCustomUserRepository.FindUserByuAccount(uaccount, upassword);
        }
    }
}

现在可以定义自定义的授权类型了,我起名为CzarCustomUserGrantValidator,实现代码如下。

using Czar.AuthPlatform.Web.Application.IServices;
using IdentityServer4.Models;
using IdentityServer4.Validation;
using System.Threading.Tasks;

namespace Czar.AuthPlatform.Web.Application.Ids4
{
    /// <summary>
    /// 金焰的世界
    /// 2019-01-28
    /// 自定义用户授权
    /// </summary>
    public class CzarCustomUserGrantValidator : IExtensionGrantValidator
    {
        public string GrantType => "CzarCustomUser";

        private readonly ICzarCustomUserServices czarCustomUserServices;

        public CzarCustomUserGrantValidator(ICzarCustomUserServices czarCustomUserServices)
        {
            this.czarCustomUserServices = czarCustomUserServices;
        }

        public Task ValidateAsync(ExtensionGrantValidationContext context)
        {
            var userName = context.Request.Raw.Get("czar_name");
            var userPassword = context.Request.Raw.Get("czar_password");

            if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(userPassword))
            {
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
            }
            //校验登录
            var result = czarCustomUserServices.FindUserByuAccount(userName, userPassword);
            if (result==null)
            {
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
            }
            //添加指定的claims
            context.Result = new GrantValidationResult(
                         subject: result.iid.ToString(),
                         authenticationMethod: GrantType,
                         claims: result.Claims);
            return Task.CompletedTask;
        }
    }
}

这就实现了自定义授权的功能,是不是很简单呢?然后添加此扩展方法。

services.AddIdentityServer(option =>
            {
                option.PublicOrigin = Configuration["CzarConfig:PublicOrigin"];
            })
                .AddDeveloperSigningCredential()
                .AddDapperStore(option =>
                {
                    option.DbConnectionStrings = Configuration["CzarConfig:DbConnectionStrings"];
                })
                .AddResourceOwnerValidator<CzarResourceOwnerPasswordValidator>()
                .AddProfileService<CzarProfileService>()
                .AddSecretValidator<JwtSecretValidator>()
                //添加自定义授权
                .AddExtensionGrantValidator<CzarCustomUserGrantValidator>();

现在是不是就可以使用自定义授权的方式了呢?打开PostMan测试,按照源码解析和设计参数,测试信息如下,发现报错,原来是还未配置好客户端访问权限,开启权限测试如下。

三、客户端权限配置

在使用IdentityServer4时我们一定要理解整个验证流程。根据这次配置,我再梳理下流程如下:

  • 1、校验客户端client_id和Client_Secret。
  • 2、校验客户端是否有当前的授权方式。
  • 3、校验是否有请求scope权限。
  • 4、如果非客户端验证,校验账号密码或自定义规则是否正确。
  • 5、非客户端验证,校验授权信息是否有效。

通过此流程会发现我们缺少授权方式配置,所以请求时提示上面的提示,既然知道原因了,那就很简单的来实现,添加客户端自定义授权模式。此信息是在ClientGrantTypes表中,字段为客户端ID和授权方式。我测试的客户端ID为21,授权方式为CzarCustomUser,那直接使用SQL语句插入关系,然后再测试。

INSERT INTO ClientGrantTypes VALUES(21,''CzarCustomUser'');

发现可以获取到预期结果,然后查看access_token是什么内容,显示如下。

显示的信息和我们定义的信息相同,而且可以通过amr来区分授权类型,不同的业务系统使用不同的认证方式,然后统一集成到认证平台即可。

四、总结与思考

本篇我介绍了自定义授权方式,从源码解析到最后的实现详细讲解了实现原理,并使用测试的用户来实现自定义的认证流程,本篇涉及的知识点不多,但是非常重要,因为我们在使用统一身份认证时经常会遇到多种认证方式的结合,和多套不同应用用户的使用,在掌握了授权原理后,就能在不同的授权方式中切换的游刃有余。

思考下,有了这些知识后,关于短信验证码登录和扫码登录是不是有心理有底了呢?如果自己实现这类登录应该都知道从哪里下手了吧。

下篇我将介绍常用登录的短信验证码授权方式,尽情期待吧。

.Net Core 5 中的自定义授权属性

.Net Core 5 中的自定义授权属性

如何解决.Net Core 5 中的自定义授权属性?

我有以下自定义授权属性的代码

public class CustomAuthorizeAttribute : AuthorizeAttribute  
{  
    
   protected override bool AuthorizeCore(HttpContextBase httpContext)  
   { 
    } 
    
}

问题是没有HttpContextBase这样的东西。我也有所有的 httpcontext using,但仍然对我大喊大叫。我做错了什么?

解决方法

如果我们想编写自定义逻辑来授权用户,我建议您可以考虑使用 AuthorizeAttribute 和 IAuthorizationFilter。

IAuthorizationFilter 提供了 OnAuthorization 方法,该方法可以编写一些自定义逻辑来授权用户。

更多细节,您可以参考以下代码:

public class CustomAuthorizeAttribute : AuthorizeAttribute,IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        //Custom code ...

  

        //Return based on logic
        context.Result = new UnauthorizedResult();
    }


}

此外,asp.net core 推荐使用新的策略设计。新方法背后的基本思想是使用新的 [Authorize] 属性来指定“策略”(例如 [Authorize( Policy = "YouNeedToBe18ToDoThis")]。

更多细节,你可以参考这个answer。

,

你可以这样写代码:-

您可以使用示例中提到的 HttpContextBase 代替 AuthorizationFilterContext

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomAuthorizeAttribute : Attribute,IAuthorizationFilter
{
  public void OnAuthorization(AuthorizationFilterContext context)
  {
    //your code logic..........
  }
}

.NET Core中 实现H5微信登录(静默授权方式)

.NET Core中 实现H5微信登录(静默授权方式)

需求

假设现在有一个H5需要有微信登录、手机号登录、邮箱登录 三种登录方式。让我们一起来看看微信登录如何实现吧

界面:

最终实现的效果图(登录成功后返回个人页):

因为微信登录目前没有实现移动端的其他浏览器授权登录,所以,再用除微信以外的浏览器操作登录时,我们需要给出用户提醒,比如这样:

 

 实现

准备工作

登录服务号或订阅号的微信公众号后台,找到AppId以及AppSecret。后面会用到

在公众号设置中,设置安全域名、js域名以及网页授权域名

其中再网页授权域名设置的时候需要注意,将腾讯提供的一个唯一标识文件存放于项目根目录下

 

数据库部分

新建一张Login表,用于存放用户登录信息

CREATE TABLE `NewTable` (
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`loginaccount`  varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL ,
`password`  varchar(45) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL ,
`type`  tinyint(4) NOT NULL ,
`userid`  int(11) NULL DEFAULT 0 ,
`isvalid`  tinyint(2) NULL DEFAULT 1 ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_bin
AUTO_INCREMENT=28
ROW_FORMAT=DYNAMIC
;

前端部分

前端要做的 比较简单,放置一个button按钮,以及js处理判断是否是微信内点击即可:

<div class="row">
    <div class="col-xs-12">
        <button type="button" class="btn  btn-lg btn-block wechatBtn" id="weChatLogin">微信</button>
    </div>
</div>

对应的js部分为:

$("#weChatLogin").on("click",
        function () {
            var layerLoading = layer.load(1, {
                icon: 0,
                shade: [0.3, ''black'']
            });
            var result = isWeiXin();
            if (result === 0) {
                setTimeout(function () {
                    layer.closeAll();
                    var local = "回调地址";
                    window.location.href =
                        ''https://open.weixin.qq.com/connect/oauth2/authorize?appid=服务号的appId&redirect_uri='' +
                        encodeURIComponent(local) +
                        ''&response_type=code&scope=snsapi_base&state=a#wechat_redirect'';
                },
                    500);
            } else {
                setTimeout(function () {
                    layer.closeAll();
                    layer.msg("请在微信内打开~<br/>或尝试下其他登录方式哦");

                },500);
            } 
        });

上面这段js代码中,有两个黄色背景的代码需要注意,函数isWeiXin是用于判断当前用户打开的浏览器是否是微信浏览器,而参数snsapi_base则表示微信登录时采取的静默授权方式(即这样 只能获取到用户的Openid,无法获取到其他资料).

isWeiXin函数如下

//判断是否是微信浏览器的函数
    function isWeiXin() {
        //window.navigator.userAgent属性包含了浏览器类型、版本、操作系统类型、浏览器引擎类型等信息,这个属性可以用来判断浏览器类型
        if (browser.versions.mobile) {//判断是否是移动设备打开。browser代码在下面
            var ua = navigator.userAgent.toLowerCase();//获取判断用的对象
            if (ua.match(/MicroMessenger/i) == "micromessenger") {
                return 0;
            } else {
                return 1;
            }
        } else {
            //否则就是PC浏览器打开
            return 2;
        }
    }

    var browser = {
        versions: function () {
            var u = navigator.userAgent, app = navigator.appVersion;
            return {         //移动终端浏览器版本信息
                trident: u.indexOf(''Trident'') > -1, //IE内核
                presto: u.indexOf(''Presto'') > -1, //opera内核
                webKit: u.indexOf(''AppleWebKit'') > -1, //苹果、谷歌内核
                gecko: u.indexOf(''Gecko'') > -1 && u.indexOf(''KHTML'') == -1, //火狐内核
                mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端
                ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端
                android: u.indexOf(''Android'') > -1 || u.indexOf(''Linux'') > -1, //android终端或uc浏览器
                iPhone: u.indexOf(''iPhone'') > -1, //是否为iPhone或者QQHD浏览器
                iPad: u.indexOf(''iPad'') > -1, //是否iPad
                webApp: u.indexOf(''Safari'') == -1 //是否web应该程序,没有头部与底部
            };
        }(),
        language: (navigator.browserLanguage || navigator.language).toLowerCase()
    }

 后端部分

 其中code和state是微信服务器发起请求的时候会带过来。code有效期为5分钟,state为自定义的一个参数,具体可参考微信网页授权文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842

 

 public async Task<IActionResult> Index()
 {
            var code = Request.Query["code"];
            var state = Request.Query["state"];
            OAuthToken tokenModel = new OAuthToken();
            if (!string.IsNullOrEmpty(code) && !string.IsNullOrEmpty(state))
            {
                tokenModel = await _dataServices.LoginWeChat(code, _config[ConfigurationKeys.PAY_APPID]);//调取接口
         _logger.LogError($"微信登录:{tokenModel.Openid}");
         code = string.Empty;
                if (tokenModel.errmsg.Contains("success"))
                {
                    var model = await _dataServices.GetUserByIdAccount(tokenModel.Openid);//具体可根据自己的项目业务来操作
//TODO
} } return View("~/Views/Home/Index.cshtml"); }

 上述代码中从第一个OAuthToken说起,它是一个自定义存放微信授权的实体类,内容如下

public class OAuthToken:BaseRes
    {
        /// <summary>
        /// 网页授权接口调用凭证。注意:此access_token与基础支持的access_token不同
        /// </summary>
        [JsonProperty("access_token")]
        public string AccessToken { get; set; }

        private int _expiresIn;

        /// <summary>
        /// access_token接口调用凭证超时时间,单位(秒)
        /// </summary>
        [JsonProperty("expires_in")]
        public int ExpiresIn
        {
            get { return _expiresIn; }
            set
            {
                ExpiresTime = DateTime.Now.AddSeconds(value);
                _expiresIn = value;
            }
        }
        /// <summary>
        /// 用于刷新access_token
        /// </summary>
        [JsonProperty("refresh_token")]
        public string RefreshToken { get; set; }

        /// <summary>
        /// 用户唯一标识。请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的openid
        /// </summary>
        [JsonProperty("openid")]
        public string Openid { get; set; }

        /// <summary>
        /// 用户授权的作用域,使用逗号(,)分隔
        /// </summary>
        [JsonProperty("scope")]
        public string Scope { get; set; }

        [JsonProperty("expires_time")]
        public DateTime ExpiresTime { get; set; }


        [JsonProperty("unionid")]
        public string Unionid { get; set; }
    }

其中BaseRes,是返回的错误实体类

public class BaseRes
{
   public BaseRes()
   {
     errmsg = "success";
   }
public int errcode { get; set; } public string errmsg { get; set; } }

第二个ConfigurationKeys.PAY_APPID是获取配置项

第三个LoginWeChat,我们来看看这个接口中是如何实现的

首先我们看到这个接口接收两个参数,和上面我们请求的参数与对应,一个是code,另一个是appId

[Route("[controller]/LoginByWeChat")]
        [HttpGet]
        public async Task<OAuthTokenDto> LoginByWeChat(string code, string appid = "")
        {
            return await _authenticationService.LoginByWeChat(code, appid);
        }

请求微信登录:

/// <summary>
        /// 通过code换取网页授权access_token
        /// </summary>
        /// <param name="appid">公众号的唯一标识</param>
        /// <param name="code">填写第一步获取的code参数</param>
        /// <returns></returns>
        public  async Task<OAuthTokenModel> LoginByWeChat(string code, string appid = "")
        {
            var config = OpenApi.GetConfig(appid, PlatformType.Mp);
            var url =
                $"https://api.weixin.qq.com/sns/oauth2/access_token?appid={config.AppId}&secret={config.AppSecret}&code={code}&grant_type=authorization_code";
            return await HttpUtil.GetResultAsync<OAuthTokenModel>(url);
        }
        /// <summary>
        /// 根据appid,获取对应的接口参数信息
        /// </summary>
        /// <param name="appid"></param>
        /// <returns></returns>
        public static ApiConfig GetConfig(string appid = "", PlatformType platform = PlatformType.Mp)
        {
            if (string.IsNullOrEmpty(appid) && apiConfigs?.Count > 0)
            {
                return apiConfigs.FirstOrDefault(a => a.Platform == platform);
            }
            return apiConfigs.FirstOrDefault(a => a.AppId == appid);
        }
    public class ApiConfig
    {
        public string AppId { get; set; }
        public string AppSecret { get; set; }
        public PlatformType Platform { get; set; } = PlatformType.Mp;
    }
    public enum PlatformType
    {
        /// <summary>
        /// 公众号
        /// </summary>
        Mp,
        /// <summary>
        /// 小程序
        /// </summary>
        Mini,
        /// <summary>
        /// 开放平台
        /// </summary>
        Open,
        /// <summary>
        /// 企业号
        /// </summary>
        Qy
    }
        /// <summary>
        /// 发起GET请求,并获取请求返回值
        /// </summary>
        /// <typeparam name="T">返回值类型</typeparam>
        /// <param name="url">接口地址</param>
        public static async Task<T> GetResultAsync<T>(string url)
        {
            var retdata = await HttpGetAsync(url);
            return JsonConvert.DeserializeObject<T>(retdata);
        }

这里我们调用了Get异步请求:

        public static async Task<string> HttpGetAsync(string url)
        {
            var request = CreateRequest(url, HttpMethod.GET);
            return await GetResponseStringAsync(request);
        }
private static HttpWebRequest CreateRequest(string url, HttpMethod method, string postData = "", string certpath = "", string certpwd = "") { var request = (HttpWebRequest)WebRequest.Create(url); request.Method = method.ToString(); request.ContentType = "application/x-www-form-urlencoded"; request.Accept = "*/*"; request.Timeout = 15000; request.AllowAutoRedirect = false; ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback((a, b, c, d) => true); if (!string.IsNullOrEmpty(certpath) && !string.IsNullOrEmpty(certpwd)) { X509Certificate2 cer = new X509Certificate2(certpath, certpwd, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet); request.ClientCertificates.Add(cer); } if (method == HttpMethod.POST) { using (var sw = new StreamWriter(request.GetRequestStream())) { sw.Write(postData); } } return request; }
private static async Task<string> GetResponseStringAsync(HttpWebRequest request) { using (var response = await request.GetResponseAsync() as HttpWebResponse) { using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { return reader.ReadToEnd();//获取响应 } } }
    public enum HttpMethod
    {
        GET,
        POST
    }

这样,我们就可以拿到返回的响应结果了

OAuthToken resultDto = JsonConvert.DeserializeObject<OAuthToken>(resultDetail);
  _logger.LogError($"OpenId:{resultDto.Openid},ErrorMsg:{resultDto.errmsg}");

 return Task.FromResult(resultDto);

我们查看日志,可以看到OpenId已经被打印出来了

 

这样,我们只需要将我们的Openid 再数据库中进行查找,就可以知道是否存在此用户,若不存在,则可以操作新增

//判断是否数据库有登录记录 ,若无则新增
                if (!string.IsNullOrEmpty(model.Openid))
                {
                    var result = await _authenticationDataServices.FindAccountById(model.Openid);
                    if (string.IsNullOrEmpty(result.LoginAccount))
                    {
                        LoginUserModel userData = new LoginUserModel
                        {
                            LoginAccount = model.Openid,
                            Password = string.Empty,
                            Type = (int) LoginType.Wexin,
                            UserId = 0,
                            IsValid = true
                        };
                        var res =await _authenticationDataServices.AddLoginUser(userData);
                        if (res <= 0) logger.Error(res);
                    }
                }

查找用户的实现

public async Task<LoginUserModel> FindAccountById(string account)
        {
            using (var conn = GetMySqlConnection())
            {
                if (conn.State == ConnectionState.Closed)
                {
                    await conn.OpenAsync();
                }

                try
                {
                    string sql =
                        @"select Loginaccount,Password,Type,Userid,Isvalid from centraldb.login  where loginaccount=@recordId;";

                    var user = conn.Query<LoginUserModel>(sql, new { recordId = account }, commandType: CommandType.Text).FirstOrDefault();
                    return user ?? new LoginUserModel();
                }
                catch (Exception e)
                {
                    throw;
                }
                
            }
        }

 

新增的实现

public async Task<int> AddLoginUser(LoginUserModel loginUser)
        {
            using (var conn = GetMySqlConnection())
            {
                if (conn.State == ConnectionState.Closed)
                {
                    await conn.OpenAsync();
                }

                const string sql =
                    @"insert into centraldb.login(loginaccount, `password`, `type`, userid, isvalid)
                            values(@loginaccount, @password, @type, @userid, @isvalid);
                      select max(id) from centraldb.login;";

                try
                {
                    var userId = (await conn.QueryAsync<int>(sql, new
                    {
                        loginaccount = loginUser.LoginAccount, password = loginUser.Password,
                        type = loginUser.Type, userid = loginUser.UserId, isvalid = loginUser.IsValid
                    }, commandType: CommandType.Text)).FirstOrDefault();

                    return userId;
                }
                catch (Exception e)
                {
                    return -1;
                }
            }
        }

这样,运行项目之后,数据库中就会插入相应的数据:

 

原文出处:https://www.cnblogs.com/zhangxiaoyong/p/10847743.html

Asp.net core 2.2 Cookie验证 自定义授权 过滤器(注入方法)

Asp.net core 2.2 Cookie验证 自定义授权 过滤器(注入方法)

一、注册服务。

services.AddAuthentication(option =>
            {
                option.DefaultScheme = "Cookie";
                option.DefaultChallengeScheme = "Cookie";
                option.DefaultAuthenticateScheme = "Cookie";
                option.DefaultForbidScheme = "Cookie";
                option.DefaultSignInScheme = "Cookie";
                option.DefaultSignOutScheme = "Cookie";
            }).AddCookie("Cookie",option=>
            {
                option.LoginPath = "/login";
                option.AccessDeniedPath = "/forbidden";
            });

二、登录

注意:需要using Microsoft.AspNetCore.Authentication; 才能使用 HttpContext.SignInAsync

var claims = new List<Claim> {
                new Claim("user","admin"),
                new Claim("role","1,2,3,4,5"),
                new Claim("id","1")
            };
            await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies")));
//注意:await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "role")));
//用这个里面的参数,user必须和上面的 new claim user一样,这样 User.Identity.Name才能获取到值,不然是空的

三、检查

这里就可以使用User

1、User.Identity.IsAuthenticated 是否登录

2、User.Identity.Name 用户名

3、User.Claims 其它登录时参数或其它,就可以从这里读取

 

---------其它-----

var claim = new Claim("name", "wallee");//我的众多信息中的一个信息单元,还有年龄、性别、家庭等等
            var identity = new ClaimsIdentity("身份证");//我的众多身份证件中的一个,还有驾驶证、准考证、会计证、计算机二级证等等
            identity.AddClaim(claim);//将上面那个信息片段添加到我的身份证里面
            var principal=new ClaimsPrincipal(identity);//将身份证作为我这个人的初始化参数,初始化一个ClaimsPrincipal就代表了一个主体。
            HttpContext.SignInAsync(principal);//最后,利用这个主体,调用HttpContext的扩展方法进行登陆。

 

 


 

上面只是完成了,登录及获取登录后的信息,下面授权,我没用系统的那一套东西,因为我需要的是对于权限这块,希望更灵活一些。我对这个理解的也不深。所以就自定义过滤器,来实现。

 

一、新建类:AppAuthorizeAttribute

///
using
Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.RazorPages; // public class AppAuthorizeAttribute : ResultFilterAttribute { public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) {
//这里面就可以做很多事情了,
var result = (PageResult)context.Result; if (context.HttpContext.User.Identity.IsAuthenticated) { await next.Invoke(); } else { context.HttpContext.Response.StatusCode = 404; } } }

二、调用:

[AppAuthorize]//就是这个
    public class adminModel : PageModel
    {
        public void OnGet()
        {

        }
    }
//还可以 扩展:PageModel,来实现哦。

 


 

在实际应用中,经常会把登录的用户信息封装起来,方便后面使用,无论过滤器还是扩展PageModel

一、用户类:CurUser

public class CurUser
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        public CurUser(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }
        public bool IsLogin { get
            {
                return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated;
            } }
    }

在AppAuthorizeAttribute 过滤器,我可以这么使用。

public class AppAuthorizeAttribute : ResultFilterAttribute
    {
//这块就是注入了,
public CurUser curUser { get; set; } public AppAuthorizeAttribute(CurUser _curuser) { curUser = _curuser; } public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { var login=curUser.IsLogin; var result = (PageResult)context.Result; if (context.HttpContext.User.Identity.IsAuthenticated) { context.HttpContext.Response.Headers.Add("islogin", curUser.IsLogin.ToString()); await next.Invoke(); } else { context.HttpContext.Response.StatusCode = 404; } } }

前台页面调用,就得需要修改一下了,

[ServiceFilter(typeof(AppAuthorizeAttribute))]//这里修改成这样,不然原先的[AppAuthorizeAttribute]这样写是会报错的,
    public class adminModel : PageModel
    {
        public void OnGet()
        {

        }
    }

这还没有结束

还得到Startup.cs 这里注册一下才可以使用。

services.AddScoped<AppAuthorizeAttribute>();
            services.AddScoped<CurUser>();

 

asp.net Core 中AuthorizationHandler 实现自定义授权

asp.net Core 中AuthorizationHandler 实现自定义授权

前言

ASP.NET Core 中 继承的是AuthorizationHandler ,而ASP.NET Framework 中继承的是AuthorizeAttribute.

它们都是用过重写里面的方法实现过滤请求的。 

现在我们实现如何在 ASP.NET Core MVC 实现自定义授权。

关于AuthorizationHandler 详细介绍可以看这里

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.2#authorization-handlers

如何自定义授权

比如我们后台有个博客管理功能,那我们可以新建一个Blog的控制器,比如BlogController

里面有添加,删除,编辑等功能,分别是Add,Delete,Edit

代码如下

public class BlogController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
        /// <summary>
        /// 博客添加页面
        /// </summary>
        /// <returns></returns>
        public IActionResult Add()
        {
            return View();
        }
        /// <summary>
        /// 博客列表页面
        /// </summary>
        public IActionResult List()
        {
            return View();
        }
        /// <summary>
        /// 博客编辑页面
        /// </summary>
        public IActionResult Edit()
        {
            return View();
        }
    }

如果有打印可以起个名字叫  public IActionResult Print()

自定义就是做个控制界面做勾选功能,用户根据自身业务选择。

以此类推,在ASP.NET 框架下默认路由就是Controller和Action,除非你修改默认路由,当然了你修改默认路由你的权限逻辑也得变。

实现过滤器

AuthorizationHandler 参数里面有个IAuthorizationRequirement要我们去填充,根据我们自己业务自己选择定义数据。

public class PermissionRequirement : IAuthorizationRequirement
    {
        /// <summary>
        /// 无权限action
        /// </summary>
        public string DeniedAction { get; set; } = "/Home/visitDeny";

        /// <summary>
        /// 认证授权类型
        /// </summary>
        public string ClaimType { internal get; set; }
        /// <summary>
        /// 默认登录页面
        /// </summary>
        public string LoginPath { get; set; } = "/Home/Login";
        /// <summary>
        /// 过期时间
        /// </summary>
        public TimeSpan Expiration { get; set; }
        /// <summary>
        /// 构造
        /// </summary>
        /// <param name="deniedAction"></param>
        /// <param name="claimType"></param>
        /// <param name="expiration"></param>
        public PermissionRequirement(string deniedAction, string claimType, TimeSpan expiration)
        {
            ClaimType = claimType;
            DeniedAction = deniedAction;
            Expiration = expiration;
        }
    }

第一个参数集合

public class PermissionItem
    {
        /// <summary>
        /// 用户或角色或其他凭据名称
        /// </summary>
        public virtual string Role { get; set; }
        /// <summary>
        /// 配置的Controller名称
        /// </summary>
        public virtual string controllerName { get; set; }
        /// <summary>
        /// 配置的Action名称
        /// </summary>
        public virtual string actionName { get; set; }
    }

 

Startup 里面,添加一个授权策略,PermissionRequirement 放进去,然后注入

////权限要求参数
            var permissionRequirement = new PermissionRequirement(
                "/Home/visitDeny",// 拒绝授权的跳转地址
                ClaimTypes.Name,//基于用户名的授权
                expiration: TimeSpan.FromSeconds(60 * 5)//接口的过期时间
                );
            #endregion

            //【授权】
            services.AddAuthorization(options =>
            {
                options.AddPolicy("Permission", policy => policy.Requirements.Add(permissionRequirement));
            });
            // 注入权限处理器
            services.AddTransient<IAuthorizationHandler, PermissionHandler>();

控制器里面加上标示

[Authorize("Permission")]
 public class BlogController : Controller
{
}

 

登录页面授权

 [HttpPost]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            if (ModelState.IsValid)
            {
                if (model.textUser == null)
                {
                    ModelState.AddModelError("", "请输入账号.");
                    return View(model);
                }
                if (model.textPassword == null)
                {
                    ModelState.AddModelError("", "请输入密码.");
                    return View(model);
                }
                if (model.textUser == "admin"  && model.textPassword == "123")
                {
                    #region 传统的登录  
                    //只判断是否登录  通过[Authorize] 小项目中只有一个管理员 只要账号和密码对就行
                    var claimIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
                    claimIdentity.AddClaim(new Claim(ClaimTypes.Name, model.textUser));

                    var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
                    //await HttpContext.SignInAsync(claimsPrincipal);
                    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
                    #endregion

                    //下面代码是演示的,实际项目要从根据用户名或者角色从数据库读取出来 配置到 List<PermissionItem>里面
                    //这里我用的是用户名判断的,根据自己的业务自己处理
                    //测试的时候 可以 删除一条记录试试,或者添加一条
                    List<PermissionItem> lsperm = new List<PermissionItem>();
                    lsperm.Add(new PermissionItem() { Role = model.textUser, controllerName = "Blog", actionName = "Add" });//添加博客页面的权限
                    lsperm.Add(new PermissionItem() { Role = model.textUser, controllerName = "Blog", actionName = "Edit" });//编辑博客页面的权限
                    lsperm.Add(new PermissionItem() { Role = model.textUser, controllerName = "Blog", actionName = "List" });//查看博客页面的权限
                    string perData = JsonConvert.SerializeObject(lsperm);
                    await _cacheService.SetStringAsync("perm" + model.textUser, perData);
                    return RedirectToAction("Index", "Home");
                }
            }
            return View(model);
        }

List<PermissionItem> 我用Redis存储的,大家根据实际情况存储。

权限判断

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
        public IAuthenticationSchemeProvider Schemes;
        readonly IDistributedCache _cacheService;
        /// <summary>
        /// 构造函数注入
        /// </summary>
        public PermissionHandler(IAuthenticationSchemeProvider schemes, IDistributedCache cacheService)
        {
            Schemes = schemes;
            _cacheService = cacheService;
        }

        // 重载异步处理程序
        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
        {
            //从AuthorizationHandlerContext转成HttpContext,以便取出表求信息
            AuthorizationFilterContext filterContext = context.Resource as AuthorizationFilterContext;
            HttpContext httpContext = filterContext.HttpContext;
            AuthenticateResult result = await httpContext.AuthenticateAsync(Schemes.GetDefaultAuthenticateSchemeAsync().Result.Name);
            //如果没登录result.Succeeded为false
            if (result.Succeeded)
            {
                httpContext.User = result.Principal;
                //当前访问的Controller
                string controllerName = filterContext.RouteData.Values["Controller"].ToString();//通过ActionContext类的RouteData属性获取Controller的名称:Home
                //当前访问的Action
                string actionName = filterContext.RouteData.Values["Action"].ToString();//通过ActionContext类的RouteData属性获取Action的名称:Index
                string name = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Name)?.Value;
                string perData = await _cacheService.GetStringAsync("perm" + name);
                List<PermissionItem> lst = JsonConvert.DeserializeObject<List<PermissionItem>>(perData);
                if (lst.Where(w => w.controllerName == controllerName && w.actionName == actionName).Count() > 0)
                {
                    //如果在配置的权限表里正常走
                    context.Succeed(requirement);
                }
                else
                {
                    //不在权限配置表里 做错误提示
                    //如果是AJAX请求 (包含了VUE等 的ajax)
                    string requestType = filterContext.HttpContext.Request.Headers["X-Requested-With"];
                    if (!string.IsNullOrEmpty(requestType) && requestType.Equals("XMLHttpRequest", StringComparison.CurrentCultureIgnoreCase))
                    {
                        //ajax 的错误返回
                        //filterContext.Result = new StatusCodeResult(499); //自定义错误号 ajax请求错误 可以用来错没有权限判断 也可以不写 用默认的
                        context.Fail();
                    }
                    else
                    {
                        //普通页面错误提示 就是跳转一个页面
                        //httpContext.Response.Redirect("/Home/visitDeny");//第一种方式跳转
                        filterContext.Result = new RedirectToActionResult("visitDeny", "Home", null);//第二种方式跳转
                        context.Fail();
                    }
                }
            }
            else
            {
                context.Fail();
            }
        }
    }

至此我们实现定义授权判断。实际业务上每个人可以根据自己的情况做处理。

 

今天关于【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式的分享就到这里,希望大家有所收获,若想了解更多关于.Net Core 5 中的自定义授权属性、.NET Core中 实现H5微信登录(静默授权方式)、Asp.net core 2.2 Cookie验证 自定义授权 过滤器(注入方法)、asp.net Core 中AuthorizationHandler 实现自定义授权等相关知识,可以在本站进行查询。

本文标签: