139 lines
4.1 KiB
C#
139 lines
4.1 KiB
C#
using System.Collections.Concurrent;
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Security.Claims;
|
|
using System.Security.Cryptography;
|
|
using Fantasy;
|
|
using Fantasy.Async;
|
|
using Fantasy.Network.HTTP;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
namespace NBF;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[ServiceFilter(typeof(SceneContextFilter))]
|
|
public class AuthController : NBControllerBase
|
|
{
|
|
private static readonly ConcurrentDictionary<string, CaptchaInfo> CaptchaCache = new();
|
|
private static readonly TimeSpan CaptchaLifetime = TimeSpan.FromMinutes(10);
|
|
private readonly Scene _scene;
|
|
|
|
public AuthController(Scene scene)
|
|
{
|
|
_scene = scene;
|
|
}
|
|
|
|
[HttpGet("login")]
|
|
[AllowAnonymous]
|
|
public async FTask<IActionResult> Login([FromQuery] string account, [FromQuery] string code)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(account) || string.IsNullOrWhiteSpace(code))
|
|
{
|
|
await FTask.CompletedTask;
|
|
return Error(ErrorCode.ArgsError, "account or code is empty");
|
|
}
|
|
|
|
var key = account.Trim();
|
|
if (!CaptchaCache.TryGetValue(key, out var captchaInfo))
|
|
{
|
|
await FTask.CompletedTask;
|
|
return Error(ErrorCode.ArgsError, "captcha not found");
|
|
}
|
|
|
|
if (captchaInfo.ExpireAtUtc <= DateTime.UtcNow)
|
|
{
|
|
CaptchaCache.TryRemove(key, out _);
|
|
await FTask.CompletedTask;
|
|
return Error(ErrorCode.ArgsError, "captcha expired");
|
|
}
|
|
|
|
if (!string.Equals(captchaInfo.Code, code.Trim(), StringComparison.Ordinal))
|
|
{
|
|
await FTask.CompletedTask;
|
|
return Error(ErrorCode.ArgsError, "captcha invalid");
|
|
}
|
|
|
|
CaptchaCache.TryRemove(key, out _);
|
|
var (token, expireAtUtc) = GenerateJwtToken(key);
|
|
|
|
await FTask.CompletedTask;
|
|
return Success(new
|
|
{
|
|
token,
|
|
expireAtUtc
|
|
});
|
|
}
|
|
|
|
[HttpGet("code")]
|
|
[AllowAnonymous]
|
|
public async FTask<IActionResult> SendCode([FromQuery] string account)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(account))
|
|
{
|
|
await FTask.CompletedTask;
|
|
return Error(ErrorCode.ArgsError, "account is empty");
|
|
}
|
|
|
|
var key = account.Trim();
|
|
var captchaCode = RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6");
|
|
var expireAtUtc = DateTime.UtcNow.Add(CaptchaLifetime);
|
|
|
|
CaptchaCache[key] = new CaptchaInfo
|
|
{
|
|
Code = captchaCode,
|
|
ExpireAtUtc = expireAtUtc
|
|
};
|
|
|
|
await DingTalkHelper.SendCAPTCHA(account, captchaCode);
|
|
|
|
return Success();
|
|
}
|
|
|
|
[HttpGet("me")]
|
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
|
public IActionResult Me()
|
|
{
|
|
var account = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty;
|
|
return Success(new
|
|
{
|
|
account
|
|
});
|
|
}
|
|
|
|
private static (string Token, DateTime ExpireAtUtc) GenerateJwtToken(string account)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var expireAtUtc = now.Add(ApiJwtHelper.TokenLifetime);
|
|
var tokenHandler = new JwtSecurityTokenHandler();
|
|
|
|
var claims = new[]
|
|
{
|
|
new Claim(ClaimTypes.NameIdentifier, account),
|
|
new Claim(ClaimTypes.Name, account),
|
|
new Claim(ClaimTypes.Role, "Player")
|
|
};
|
|
|
|
var token = tokenHandler.CreateToken(new SecurityTokenDescriptor
|
|
{
|
|
Subject = new ClaimsIdentity(claims),
|
|
Expires = expireAtUtc,
|
|
NotBefore = now,
|
|
Issuer = ApiJwtHelper.Issuer,
|
|
Audience = ApiJwtHelper.Audience,
|
|
SigningCredentials = new SigningCredentials(
|
|
ApiJwtHelper.CreateSigningKey(),
|
|
SecurityAlgorithms.HmacSha256)
|
|
});
|
|
|
|
return (tokenHandler.WriteToken(token), expireAtUtc);
|
|
}
|
|
|
|
private sealed class CaptchaInfo
|
|
{
|
|
public string Code { get; init; } = string.Empty;
|
|
public DateTime ExpireAtUtc { get; init; }
|
|
}
|
|
} |