Skip to content
微信扫码关注公众号

.NET Core 自定义登录验证

记录一下最近微信小程序后台接口项目(.NET Core 2.1)使用的自定义登录验证。

简单来说就是使用 .NET Core 自带的身份验证中间件 + 自定义 IAuthenticationHandler 接口实现 来实现的。

具体的验证处理因项目而异,这里使用的方法比较简单,仅验证令牌在 Redis 中是否存在。

本来的方案是通过加密的方式保存 OpenId时间戳 的,但是因为对性能有些影响,放弃了这个方案。

1. 自定义 IAuthenticationHandler 接口实现

csharp
/// <summary>
/// 微信认证处理
/// </summary>
public class WXAuthorizationHandler : IAuthenticationHandler
{
    /// <summary>
    /// 认证体系
    /// </summary>
    public AuthenticationScheme Scheme { get; private set; }
    /// <summary>
    /// 当前上下文
    /// </summary>
    protected HttpContext Context { get; private set; }

    /// <summary>
    /// 初始化认证
    /// </summary>
    /// <param name="scheme"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
    {
        Scheme = scheme;
        Context = context;
        return Task.CompletedTask;
    }

    /// <summary>
    /// 认证处理
    /// </summary>
    /// <returns></returns>
    public Task<AuthenticateResult> AuthenticateAsync()
    {
        // 验证令牌是否有效
        string token = Context.Request.Headers[WXAuthorizationConst.WX_TOKEN_HEADER].ToString();
        WXSessionState.Token = token;
        (bool isValid, WXTokenEntity tokenEntity) = WXToken.Valid(token);
        WXSessionState.CurrentToken = tokenEntity;
        if (!isValid || tokenEntity == null)
        {
            return Task.FromResult(AuthenticateResult.Fail("未登录或授权已过期。"));
        }

        // 生成 AuthenticationTicket
        AuthenticationTicket ticket = new AuthenticationTicket(tokenEntity.ToClaimsPrincipal(), Scheme.Name);
        return Task.FromResult(AuthenticateResult.Success(ticket));
    }

    /// <summary>
    /// 未登录时的处理
    /// </summary>
    /// <param name="properties"></param>
    /// <returns></returns>
    public Task ChallengeAsync(AuthenticationProperties properties)
    {
        Context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
        return Task.CompletedTask;
    }

    /// <summary>
    /// 权限不足时的处理
    /// </summary>
    /// <param name="properties"></param>
    /// <returns></returns>
    public Task ForbidAsync(AuthenticationProperties properties)
    {
        Context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
        return Task.CompletedTask;
    }
}

WXToken.cs

csharp
/// <summary>
/// 微信令牌
/// </summary>
public class WXToken
{
    /// <summary>
    /// 验证 token 验证是否合法(缓存中是否存在该 Key)
    /// </summary>
    /// <param name="token">令牌内容</param>
    /// <returns>令牌是否合法, 令牌实体</returns>
    public static (bool isValid, WXTokenEntity tokenEntity) Valid(string token)
    {
        if (string.IsNullOrEmpty(token) || token.Length != WXAuthorizationConst.WX_TOKEN_LENGTH)
        {
            return (false, null);
        }

        // 从 Session 中获取令牌实体
        WXTokenEntity tokenEntity = RedisUtil.Get<WXTokenEntity>(GetCacheKey(token));

        if (tokenEntity == null)
        {
            return (false, null);
        }

        return (true, tokenEntity);
    }

    /// <summary>
    /// 创建令牌
    /// </summary>
    /// <returns>新令牌</returns>
    public static string Create()
    {
        return Guid.NewGuid().ToString("N");
    }

    /// <summary>
    /// 获取 Token 的缓存 Key
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    public static string GetCacheKey(string token)
    {
        return $"{CacheKey.WXSessionKeyPrefix}_{token}";
    }
}

WXTokenEntityExtention.cs

csharp
/// <summary>
/// 微信令牌实体扩展方法
/// </summary>
public static class WXTokenEntityExtention
{
    /// <summary>
    /// 令牌实体 转 ClaimsPrincipal
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    public static ClaimsPrincipal ToClaimsPrincipal(this WXTokenEntity token)
    {
        var claimsIdentity = new ClaimsIdentity(new Claim[] {
                new Claim(ClaimTypes.Name, token.OpenId),
                new Claim(ClaimTypes.Role, token.Role),
            }, WXAuthorizationConst.DEFAULT_AUTHENTICATION_TYPE);

        var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

        return claimsPrincipal;
    }
}

2. 在 Start.cs 中启用权限认证、配置权限方案

csharp
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
    // something else

    //配置权限方案
    services.AddAuthentication(options => {
        options.AddScheme<WXAuthorizationHandler>(WXAuthorizationConst.DEFAULT_SCHEME_NAME, "Default Wechat Scheme");
        options.DefaultAuthenticateScheme = WXAuthorizationConst.DEFAULT_SCHEME_NAME;
        options.DefaultChallengeScheme = WXAuthorizationConst.DEFAULT_SCHEME_NAME;
    });

    //配置 MVC
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    // something else
}

/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // something else

    //使用权限验证
    app.UseAuthentication();

    app.UseMvc();

    // something else
}

3. SampleController.cs 示例用接口

  • [AllowAnonymous]:匿名可访问
  • [Authorize]:必须登录才可访问
  • [Authorize(Roles = WXAuthorizationConst.Role.GUEST)]:已登录且必须是 GUEST 角色才可访问
  • [Authorize(Roles = WXAuthorizationConst.Role.COLONEL + "," + WXAuthorizationConst.Role.FAN)]:已登录且必须是 COLONELFAN 角色才可访问
  • 方法上特性的优先级比 Controller 上的优先级高
csharp
/// <summary>
/// 接口示例
/// </summary>
[Route("api/sample")]
[ApiController]
[Authorize]
public class SampleController : ControllerBase
{
    /// <summary>
    /// 欢迎
    /// </summary>
    /// <returns></returns>
    [HttpPost("hello")]
    [AllowAnonymous]
    public Result Hello()
    {
        return new Result()
        {
            IsSuccess = true,
            Msg = "Hello, World!",
        };
    }

    /// <summary>
    /// 欢迎游客
    /// </summary>
    /// <returns></returns>
    [HttpPost("hello/guest")]
    [Authorize(Roles = WXAuthorizationConst.Role.GUEST)]
    public Result HelloGuest()
    {
        return new Result()
        {
            IsSuccess = true,
            Msg = $"Hello, {WXSessionState.OpenId}!",
        };
    }

    /// <summary>
    /// 欢迎会员
    /// </summary>
    /// <returns></returns>
    [Authorize(Roles = WXAuthorizationConst.Role.COLONEL + "," + WXAuthorizationConst.Role.FAN)]
    [HttpPost("hello/member")]
    public Result HelloMember()
    {
        return new Result()
        {
            IsSuccess = true,
            Msg = $"Hello, {WXSessionState.OpenId}!",
        };
    }
}

参考

  1. ASP.NET Core 运行原理解剖 -5:Authentication
  2. Migrate authentication and Identity to ASP.NET Core 2.0
  3. [Draft] Auth 2.0 Migration announcement
  4. 在 ASP.NET Core 中的简单授权
  5. ASP.NET Core 中基于角色的授权
  6. 理解 ASP.NET Core 验证模型 (Claim, ClaimsIdentity, ClaimsPrincipal) 不得不读的英文博文
  7. Introduction to Authentication with ASP.NET Core
  8. ASP.NET Core 中间件
  9. 写入自定义 ASP.NET Core 中间件
  10. 应该还有其它一些文章的,忘记记录了