.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)]
:已登录且必须是 COLONEL 或 FAN 角色才可访问- 方法上特性的优先级比 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}!",
};
}
}
参考
- ASP.NET Core 运行原理解剖 -5:Authentication
- Migrate authentication and Identity to ASP.NET Core 2.0
- [Draft] Auth 2.0 Migration announcement
- 在 ASP.NET Core 中的简单授权
- ASP.NET Core 中基于角色的授权
- 理解 ASP.NET Core 验证模型 (Claim, ClaimsIdentity, ClaimsPrincipal) 不得不读的英文博文
- Introduction to Authentication with ASP.NET Core
- ASP.NET Core 中间件
- 写入自定义 ASP.NET Core 中间件
- 应该还有其它一些文章的,忘记记录了