.NetCore登录验证码的实现(.Net core login captcha)

引言

之前项目紧的时候,非功能要求都是能省则省,现在项目完成了;验收时有了安全性要求,自然需要登录验证码。

国外的登录安全验证(captcha)一般用的reCAPTCHA ,也就是经常见到的打勾;

img

有的时候还会触发别的题目,这种验证方式固然好;但不符合国情。

国内用的最多的还是图形验证码。

解决方案

登录验证码是防机器人登录的一种方式,这是一种系统安全手段;因此你的验证码值是不能告诉前端的,那怎么才能判断用户输入的验证码对不对呢?

我的解决方法是Session

  • 启用Session的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void ConfigureServices(IServiceCollection services)
{
//...

services.AddDistributedMemoryCache();

services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(60);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
}
public void Configure(IApplicationBuilder app)
{
//...
app.UseSession();
app.UseCookiePolicy();
}

图形验证码的生成我使用了一个开源的库:SixLaborsCaptcha

  • 验证码的使用可查看官网示例,我的代码如下:
1
2
3
4
5
6
7
8
9
10
using SixLaborsCaptcha.Mvc.Core;
...
public void ConfigureServices(IServiceCollection services)
{
services.AddSixLabCaptcha(x =>
{
x.DrawLines = 4;
});
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// 获得验证码图片
/// </summary>
/// <param name="sixLaborsCaptcha"></param>
/// <param name="now">时间戳,前端使用Date.now()生成</param>
/// <returns></returns>
[HttpGet]
[Route("[action]")]
public async Task<FileResult> GetCaptchaImage([FromServices] ISixLaborsCaptchaModule sixLaborsCaptcha, long? now)
{
now ??= DateTime.Now.Ticks;

string key = Extensions.GetUniqueKey(6);
HttpContext.Session.SetString("CaptchaTime", now.ToString());
HttpContext.Session.SetString("CaptchaKey", key);
await HttpContext.Session.CommitAsync();

_log.LogInformation("current session is {session}", HttpContext.Session.Id);
_log.LogInformation("key is {key}", key);
var imgText = sixLaborsCaptcha.Generate(key);
return File(imgText, "Image/Png");
}
  • 验证码校验:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[HttpPost("[Action]")]
public async Task<UserInfo> Login(LoginInfo loginInfo)
{
_log.LogInformation("current session is {session}", HttpContext.Session.Id);

await HttpContext.Session.LoadAsync();
_log.LogInformation("after load current session is {session}", HttpContext.Session.Id);

if (string.IsNullOrEmpty(loginInfo.Captcha))
{
return BadRequest("请输入验证码!");
}

var timeStr = HttpContext.Session.GetString("CaptchaTime");
if (string.IsNullOrEmpty(timeStr) || !long.TryParse(timeStr, out long result))
{
return BadRequest("登陆无效,请刷新后重试");
}

var time = new DateTime(result);
if (time.AddMilliseconds(1) > DateTime.Now)
{
return BadRequest("验证码无效,请刷新后重试");
}

if (loginInfo.Captcha.ToLower() != HttpContext.Session.GetString("CaptchaKey")?.ToLower())
{
return BadRequest("验证码错误");
}

var info = Login(loginInfo.Account, loginInfo.Password);

return info;
}

验证码校验不区分大小写,同时我设置了一分钟超时时间

常见问题

  • HttpContext.Session.Id数据不断变化(登陆无效,请刷新后重试)的解决方法

session依赖客户端的cookie,在请求图片的时候服务端会在返回头中添加Set-Cookie: .AspNetCore.Session=xxxx,在登录时依据此cookie取得session然后才能判断验证码的正误;因此应首先在ChromeNetwork中查看登录请求是否携带.AspNetCore.Sessioncookie;若无,可检查以下几个方向:

  1. 跨域问题

生成图片的API和登录的API最好属于一个域,若存在跨域问题;一般情况下不会发送cookie,需在前端发起请求时添加withCredentials头,具体可查阅使用的http库文档。

  1. 分布式缓存配置问题

session默认使用基于内存的缓存(AddDistributedMemoryCache),若项目本身使用了基于redis的缓存(AddStackExchangeRedisCache),则默认使用了redis缓存,在redis缓存配置有误时,session无法正常工作,因此不会向前端添加Set-Cookie: .AspNetCore.Session=xxxx,同时系统无任何异常,此时可尝试去掉验证码登录逻辑确认配置正常

  • 自动登录方法 在启用验证码登录后每次测试都需要手输验证码,这让自动化测试变得麻烦;因此我添加了一种额外的基于时间的OTP验证码。使用Otp.NET实现,在验证验证码之前验证OTP code即可。 配合Vaultwarden 可以实现无感登录。

参考资料


.NetCore登录验证码的实现(.Net core login captcha)
http://blog.wangshuai.app/2023-06-13-.NetCore登录验证码的实现/
作者
王帅
发布于
2023年6月13日
许可协议