Skip to content

Commit 42db73c

Browse files
committed
ResourceOwnerPasswordValidator 必并必验证图形验证码
1 parent 28ff489 commit 42db73c

File tree

7 files changed

+88
-51
lines changed

7 files changed

+88
-51
lines changed

.github/workflows/docker-image.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ jobs:
1717
- name: Login docker regsitry
1818
run: docker login -u zlzforever -p ${{ secrets.DOCKER_USER_PASSWORD }}
1919
- name: Build the Docker image
20-
run: docker build . --file Dockerfile --tag zlzforever/security-token-service:20251224.7
20+
run: docker build . --file Dockerfile --tag zlzforever/security-token-service:20251224.8
2121
- name: Publish the Docker image
22-
run: docker push zlzforever/security-token-service:20251224.7
22+
run: docker push zlzforever/security-token-service:20251224.8

src/Client/Client.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55
<OutputType>Exe</OutputType>
66
</PropertyGroup>
77

src/SecurityTokenService/Controllers/AccountController.cs

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public async Task<IActionResult> ResetPasswordByOriginPassword(
6060
return new ObjectResult(modelErrorResult);
6161
}
6262

63-
var checkCaptchaResult = CheckCaptcha(input.CaptchaCode);
63+
var checkCaptchaResult = Util.CheckCaptcha(memoryCache, logger, Request, input.CaptchaCode);
6464
if (checkCaptchaResult != null)
6565
{
6666
return new ObjectResult(checkCaptchaResult);
@@ -190,7 +190,7 @@ public async Task<IActionResult> Login([FromBody] Inputs.V1.LoginInput model)
190190
return new ObjectResult(new RedirectResult("/"));
191191
}
192192

193-
var checkCaptchaResult = CheckCaptcha(model.CaptchaCode);
193+
var checkCaptchaResult = Util.CheckCaptcha(memoryCache, logger, Request, model.CaptchaCode);
194194
if (checkCaptchaResult != null)
195195
{
196196
return new ObjectResult(checkCaptchaResult);
@@ -385,7 +385,7 @@ public async Task<ApiResult> SendCode([FromBody] Inputs.V1.SendCode input)
385385
return new ApiResult { Code = 429, Message = "验证码发送过于频繁,请稍后再试", Success = false };
386386
}
387387

388-
var checkCaptchaResult = CheckCaptcha(input.CaptchaCode);
388+
var checkCaptchaResult = Util.CheckCaptcha(memoryCache, logger, Request, input.CaptchaCode);
389389
if (checkCaptchaResult != null)
390390
{
391391
return checkCaptchaResult;
@@ -607,38 +607,6 @@ private async Task<ApiResult> SendCodeAsync(string key, Inputs.V1.SendCode input
607607
}
608608
}
609609

610-
private ApiResult CheckCaptcha(string captchaCode)
611-
{
612-
var captchaId = Request.Cookies["CaptchaId"];
613-
if (string.IsNullOrEmpty(captchaId))
614-
{
615-
captchaId = Request.Headers["Z-CaptchaId"];
616-
}
617-
618-
if (string.IsNullOrEmpty(captchaId))
619-
{
620-
return new ApiResult { Code = Errors.VerifyCodeIsExpired, Success = false, Message = "验证码已过期, 请刷新" };
621-
}
622-
623-
var cacheKey = string.Format(Util.CaptchaTtlKey, captchaId);
624-
var realCaptchaCode = memoryCache.Get<string>(cacheKey);
625-
// 步骤3:校验验证码
626-
if (string.IsNullOrEmpty(realCaptchaCode))
627-
{
628-
return new ApiResult { Code = Errors.VerifyCodeIsExpired, Success = false, Message = "验证码已过期, 请刷新" };
629-
}
630-
631-
if (!string.Equals(realCaptchaCode, captchaCode, StringComparison.OrdinalIgnoreCase))
632-
{
633-
logger.LogError("图形验证码校验失败, {CaptchaId} {RealCaptchaCode} {Actual}", captchaId, realCaptchaCode,
634-
captchaCode);
635-
return new ApiResult { Code = Errors.VerifyCodeIsInCorrect, Success = false, Message = "验证码错误" };
636-
}
637-
638-
// 步骤4:验证码验证通过后,删除缓存(防止重复使用)
639-
memoryCache.Remove(cacheKey);
640-
return null;
641-
}
642610

643611
private async Task<ApiResult> CheckUserAvailableAsync(User user)
644612
{

src/SecurityTokenService/IdentityServer/ResourceOwnerPasswordValidator.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,49 @@
22
using IdentityModel;
33
using IdentityServer4.Models;
44
using IdentityServer4.Validation;
5+
using Microsoft.AspNetCore.Http;
56
using Microsoft.AspNetCore.Identity;
67
using Microsoft.EntityFrameworkCore;
8+
using Microsoft.Extensions.Caching.Memory;
9+
using Microsoft.Extensions.Logging;
710
using Microsoft.Extensions.Options;
811
using SecurityTokenService.Identity;
12+
using SecurityTokenService.Utils;
913

1014
namespace SecurityTokenService.IdentityServer;
1115

1216
public class ResourceOwnerPasswordValidator(
1317
IOptionsMonitor<IdentityExtensionOptions> identityExtensionOptions,
1418
UserManager<User> userManager,
19+
IMemoryCache memoryCache,
20+
ILogger<ResourceOwnerPasswordValidator> logger,
21+
IHttpContextAccessor contextAccessor,
1522
SignInManager<User> signInManager)
1623
: IResourceOwnerPasswordValidator
1724
{
1825
private readonly IdentityExtensionOptions _identityExtensionOptions = identityExtensionOptions.CurrentValue;
1926

2027
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
2128
{
29+
if (contextAccessor.HttpContext == null)
30+
{
31+
context.Result = new GrantValidationResult(
32+
TokenRequestErrors.InvalidRequest,
33+
"invalid_request");
34+
return;
35+
}
36+
37+
var request = contextAccessor.HttpContext.Request;
38+
var captchaCode = context.Request.Raw["CaptchaCode"];
39+
var checkCaptchaResult = Util.CheckCaptcha(memoryCache, logger, request, captchaCode);
40+
if (checkCaptchaResult != null)
41+
{
42+
context.Result = new GrantValidationResult(
43+
TokenRequestErrors.InvalidRequest,
44+
"invalid_captcha_code");
45+
return;
46+
}
47+
2248
User user;
2349
if (string.IsNullOrWhiteSpace(_identityExtensionOptions.SoftDeleteColumn))
2450
{
@@ -34,15 +60,15 @@ public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
3460
(x.UserName == context.UserName || x.Email == context.UserName ||
3561
x.PhoneNumber == context.UserName));
3662
}
37-
63+
3864
if (user == null)
3965
{
4066
context.Result = new GrantValidationResult(
4167
TokenRequestErrors.InvalidGrant,
4268
"invalid_username");
4369
return;
4470
}
45-
71+
4672
var result = await signInManager.PasswordSignInAsync(user, context.Password,
4773
false, false);
4874
if (result.Succeeded)
@@ -52,6 +78,7 @@ public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
5278
OidcConstants.AuthenticationMethods.Password);
5379
return;
5480
}
81+
5582
context.Result = new GrantValidationResult(
5683
TokenRequestErrors.InvalidGrant,
5784
"invalid_password");

src/SecurityTokenService/Middlewares/DecryptRequestMiddleware.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public async Task InvokeAsync(HttpContext context, ILogger<DecryptRequestMiddlew
7070
return;
7171
}
7272

73-
using var ase = Util.CreateAesEcb(encryptKey);
73+
using var ase = Util.CreateAes(encryptKey);
7474
// 前端固定对称加密的 KEY,仅应用对 WF 对一些敏感数据的拦截。
7575
await DecryptV1Body(context, ase, logger);
7676
}
@@ -110,7 +110,7 @@ private static async Task DecryptV1Body(HttpContext context, Aes aes, ILogger<De
110110
// 处理兼容问题,替换掉双引号的字符串
111111
var replaceBodyContent = bodyContent.Replace("\"", "");
112112
// 解密请求body
113-
var decryptedBody = Util.AesEcbDecrypt(aes, replaceBodyContent);
113+
var decryptedBody = Util.AesDecrypt(aes, replaceBodyContent);
114114
logger.LogDebug($"DecryptRequestMiddleware_V1_解密:{System.Text.Encoding.UTF8.GetString(decryptedBody)}");
115115
context.Request.Body = new MemoryStream(decryptedBody);
116116
}

src/SecurityTokenService/Utils/Util.cs

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
using System.Security.Cryptography;
33
using System.Security.Cryptography.X509Certificates;
44
using System.Text;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.Extensions.Caching.Memory;
7+
using Microsoft.Extensions.Logging;
8+
using SecurityTokenService.Controllers;
9+
510

611
namespace SecurityTokenService.Utils;
712

@@ -25,6 +30,40 @@ public static class Util
2530
public const string PurposeLogin = "Login";
2631
public const string PurposeRegister = "Register";
2732

33+
public static ApiResult CheckCaptcha(IMemoryCache memoryCache, ILogger logger, HttpRequest request,
34+
string captchaCode)
35+
{
36+
var captchaId = request.Cookies["CaptchaId"];
37+
if (string.IsNullOrEmpty(captchaId))
38+
{
39+
captchaId = request.Headers["Z-CaptchaId"];
40+
}
41+
42+
if (string.IsNullOrEmpty(captchaId))
43+
{
44+
return new ApiResult { Code = Errors.VerifyCodeIsExpired, Success = false, Message = "验证码已过期, 请刷新" };
45+
}
46+
47+
var cacheKey = string.Format(CaptchaTtlKey, captchaId);
48+
var realCaptchaCode = memoryCache.Get<string>(cacheKey);
49+
// 步骤3:校验验证码
50+
if (string.IsNullOrEmpty(realCaptchaCode))
51+
{
52+
return new ApiResult { Code = Errors.VerifyCodeIsExpired, Success = false, Message = "验证码已过期, 请刷新" };
53+
}
54+
55+
if (!string.Equals(realCaptchaCode, captchaCode, StringComparison.OrdinalIgnoreCase))
56+
{
57+
logger.LogError("图形验证码校验失败, {CaptchaId} {RealCaptchaCode} {Actual}", captchaId, realCaptchaCode,
58+
captchaCode);
59+
return new ApiResult { Code = Errors.VerifyCodeIsInCorrect, Success = false, Message = "验证码错误" };
60+
}
61+
62+
// 步骤4:验证码验证通过后,删除缓存(防止重复使用)
63+
memoryCache.Remove(cacheKey);
64+
return null;
65+
}
66+
2867
public static void GenerateCertificate(string path)
2968
{
3069
// 设置证书的有效期
@@ -50,27 +89,29 @@ public static void GenerateCertificate(string path)
5089
System.IO.File.WriteAllBytes(path, certBytes);
5190
}
5291

53-
public static Aes CreateAesEcb(string key)
92+
public static Aes CreateAes(string key, CipherMode cipherMode = CipherMode.ECB,
93+
PaddingMode paddingMode = PaddingMode.PKCS7)
5494
{
5595
var keyArray = Encoding.UTF8.GetBytes(key);
5696
var aes = Aes.Create();
5797
aes.Key = keyArray;
58-
aes.Mode = CipherMode.ECB;
59-
aes.Padding = PaddingMode.PKCS7;
98+
aes.Mode = cipherMode;
99+
aes.Padding = paddingMode;
60100
return aes;
61101
}
62102

63-
public static string CreateAesKey()
103+
public static string CreateAesKey(CipherMode cipherMode = CipherMode.ECB,
104+
PaddingMode paddingMode = PaddingMode.PKCS7, int size = 128)
64105
{
65106
var aes = Aes.Create();
66-
aes.Mode = CipherMode.ECB;
67-
aes.Padding = PaddingMode.PKCS7;
68-
aes.KeySize = 128; // 可以设置为 128、192 或 256 位
107+
aes.Mode = cipherMode;
108+
aes.Padding = paddingMode;
109+
aes.KeySize = size; // 可以设置为 128、192 或 256 位
69110
aes.GenerateKey();
70111
return Convert.ToBase64String(aes.Key);
71112
}
72113

73-
public static byte[] AesEcbDecrypt(Aes aes, string text)
114+
public static byte[] AesDecrypt(Aes aes, string text)
74115
{
75116
var toEncryptArray = Convert.FromBase64String(text);
76117
using var decrypt = aes.CreateDecryptor();

src/SecurityTokenService/sts.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@
102102
"AllowAccessTokensViaBrowser": true,
103103
"RequireConsent": false,
104104
"AllowedGrantTypes": [
105-
"authorization_code"
105+
"authorization_code",
106+
"password"
106107
],
107108
"AllowOfflineAccess": true,
108109
"IdentityTokenLifetime": 108000,

0 commit comments

Comments
 (0)