diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/AuthorizationServerConfiguration.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/AuthorizationServerConfiguration.java index e7f454003..6a6a9ca31 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/AuthorizationServerConfiguration.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/config/AuthorizationServerConfiguration.java @@ -21,6 +21,7 @@ import com.pig4cloud.pigx.auth.support.core.CustomeOAuth2TokenCustomizer; import com.pig4cloud.pigx.auth.support.core.FormIdentityLoginConfigurer; import com.pig4cloud.pigx.auth.support.core.PigxDaoAuthenticationProvider; import com.pig4cloud.pigx.auth.support.core.PigxOAuth2RefreshTokenAuthenticationConverter; +import com.pig4cloud.pigx.auth.support.filter.FormLoginValidateCodeFilter; import com.pig4cloud.pigx.auth.support.filter.PasswordDecoderFilter; import com.pig4cloud.pigx.auth.support.filter.ValidateCodeFilter; import com.pig4cloud.pigx.auth.support.handler.PigxAuthenticationFailureEventHandler; @@ -41,14 +42,13 @@ import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter; -import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.SecurityFilterChain; @@ -78,6 +78,8 @@ public class AuthorizationServerConfiguration { private final ValidateCodeFilter validateCodeFilter; + private final FormLoginValidateCodeFilter formLoginValidateCodeFilter; + private final PigxBootCorsProperties corsProperties; @Bean @@ -91,6 +93,8 @@ public class AuthorizationServerConfiguration { // 增加验证码过滤器 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class); + // 增加授权码登录表单验证码过滤器 + http.addFilterBefore(formLoginValidateCodeFilter, UsernamePasswordAuthenticationFilter.class); // 增加密码解密过滤器 http.addFilterBefore(passwordDecoderFilter, UsernamePasswordAuthenticationFilter.class); @@ -163,15 +167,13 @@ public class AuthorizationServerConfiguration { * * @return DelegatingAuthenticationConverter */ - @Bean - public AuthenticationConverter accessTokenRequestConverter() { + private AuthenticationConverter accessTokenRequestConverter() { return new DelegatingAuthenticationConverter( Arrays.asList(new OAuth2ResourceOwnerPasswordAuthenticationConverter(), new OAuth2ResourceOwnerSmsAuthenticationConverter(), new PigxOAuth2RefreshTokenAuthenticationConverter(), new OAuth2ClientCredentialsAuthenticationConverter(), - new OAuth2AuthorizationCodeAuthenticationConverter(), - new OAuth2AuthorizationCodeRequestAuthenticationConverter())); + new OAuth2AuthorizationCodeAuthenticationConverter())); } /** diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/CaptchaCacheServiceProvider.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/CaptchaCacheServiceProvider.java deleted file mode 100644 index 1a9945e34..000000000 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/CaptchaCacheServiceProvider.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.pig4cloud.pigx.auth.endpoint; - -import com.anji.captcha.service.CaptchaCacheService; -import com.pig4cloud.pigx.common.data.cache.RedisUtils; - -/** - * @author lengleng - * @date 2020/8/27 - *
- * 验证码 缓存提供支持集群,需要通过SPI
- */
-public class CaptchaCacheServiceProvider implements CaptchaCacheService {
-
- private static final String REDIS = "redis";
-
- @Override
- public void set(String key, String value, long expiresInSeconds) {
- RedisUtils.set(key, value, expiresInSeconds);
- }
-
- @Override
- public boolean exists(String key) {
- return RedisUtils.hasKey(key);
- }
-
- @Override
- public void delete(String key) {
- RedisUtils.delete(key);
- }
-
- @Override
- public String get(String key) {
- return RedisUtils.get(key);
- }
-
- @Override
- public String type() {
- return REDIS;
- }
-
- @Override
- public Long increment(String key, long val) {
- return RedisUtils.increment(key, val);
- }
-
-}
diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/ImageCodeEndpoint.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/ImageCodeEndpoint.java
index c38778b9a..cf5a53e55 100644
--- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/ImageCodeEndpoint.java
+++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/ImageCodeEndpoint.java
@@ -78,6 +78,6 @@ public class ImageCodeEndpoint {
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
return R.ok(captchaService.check(vo));
- }
+ }
}
diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java
index dc7adfdcf..27f6c3e79 100644
--- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java
+++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java
@@ -19,6 +19,7 @@ package com.pig4cloud.pigx.auth.endpoint;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.map.MapUtil;
+import cn.hutool.json.JSONUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.admin.api.entity.SysOauthClientDetails;
@@ -26,6 +27,7 @@ import com.pig4cloud.pigx.admin.api.entity.SysTenant;
import com.pig4cloud.pigx.admin.api.feign.RemoteClientDetailsService;
import com.pig4cloud.pigx.admin.api.feign.RemoteTenantService;
import com.pig4cloud.pigx.admin.api.vo.TokenVO;
+import com.pig4cloud.pigx.auth.support.core.AuthCaptchaSupport;
import com.pig4cloud.pigx.auth.support.core.AuthErrorCodes;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
@@ -61,6 +63,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.security.Principal;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -76,6 +79,8 @@ import java.util.Set;
@RequiredArgsConstructor
public class PigxTokenEndpoint {
+ private static final String DEFAULT_FORM_CLIENT_ID = "pig";
+
private final OAuth2AuthorizationService authorizationService;
private final RemoteClientDetailsService clientDetailsService;
@@ -84,6 +89,8 @@ public class PigxTokenEndpoint {
private final CacheManager cacheManager;
+ private final AuthCaptchaSupport authCaptchaSupport;
+
/**
* 认证页面
*
@@ -92,13 +99,24 @@ public class PigxTokenEndpoint {
* @return ModelAndView
*/
@GetMapping("/token/login")
- public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
+ public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error,
+ HttpServletRequest request, HttpServletResponse response) {
modelAndView.setViewName("ftl/login");
// Note: XSS prevention is handled by FreeMarker template using ?html directive
modelAndView.addObject("error", error);
- R> tenantList = tenantService.list();
- modelAndView.addObject("tenantList", tenantList.getData());
+ List
+ * AES:使用 16 字节 UTF-8 字符串作为密钥,CFB / NoPadding 模式,IV 等于密钥。
+ * SM4:国密 SM4 算法,使用 32 位 HEX 字符串作为密钥(解码后 16 字节)。
+ */
+ public enum EncodeType {
+
+ /** AES 算法(默认) */
+ AES,
+ /** 国密 SM4 算法 */
+ SM4
+
+ }
+
}
diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/FormLoginValidateCodeFilter.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/FormLoginValidateCodeFilter.java
new file mode 100644
index 000000000..2f236b15b
--- /dev/null
+++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/FormLoginValidateCodeFilter.java
@@ -0,0 +1,65 @@
+package com.pig4cloud.pigx.auth.support.filter;
+
+import com.pig4cloud.pigx.auth.support.core.AuthCaptchaSupport;
+import com.pig4cloud.pigx.auth.support.handler.FormAuthenticationFailureHandler;
+import com.pig4cloud.pigx.common.core.constant.CommonConstants;
+import com.pig4cloud.pigx.common.core.exception.ValidateCodeException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * 授权码登录表单验证码过滤器
+ *
+ *
+ * 仅处理 /oauth2/form 请求,复用现有图形验证码能力,对授权码登录页补充验证码校验。 + *
+ * + * @author pig + * @date 2026-03-20 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FormLoginValidateCodeFilter extends OncePerRequestFilter { + + private static final String FORM_LOGIN_URL = "/oauth2/form"; + + private final AuthCaptchaSupport authCaptchaSupport; + + private final FormAuthenticationFailureHandler failureHandler = new FormAuthenticationFailureHandler(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!FORM_LOGIN_URL.equals(request.getServletPath())) { + filterChain.doFilter(request, response); + return; + } + + String authClientId = authCaptchaSupport.resolveAuthorizationClientId(request, response, true); + String tenantId = authCaptchaSupport.resolveAuthorizationTenantId(request, response, true); + + if (!authCaptchaSupport.isCaptchaEnabled(tenantId, authClientId)) { + filterChain.doFilter(request, response); + return; + } + + try { + authCaptchaSupport.validateCode(request); + filterChain.doFilter(request, response); + } catch (ValidateCodeException e) { + log.debug("授权码登录验证码校验失败: {}", e.getMessage()); + failureHandler.onAuthenticationFailure(request, response, new AuthenticationServiceException(e.getMessage())); + } + } + +} diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/PasswordDecoderFilter.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/PasswordDecoderFilter.java index f6ebeece7..2704c6829 100755 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/PasswordDecoderFilter.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/PasswordDecoderFilter.java @@ -17,10 +17,12 @@ package com.pig4cloud.pigx.auth.support.filter; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.HexUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.SmUtil; import cn.hutool.crypto.symmetric.AES; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; @@ -39,6 +41,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; +import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import javax.crypto.spec.IvParameterSpec; @@ -59,6 +62,10 @@ public class PasswordDecoderFilter extends OncePerRequestFilter { private static final String KEY_ALGORITHM = "AES"; + private static final int AES_KEY_LENGTH = 16; + + private static final int SM4_HEX_KEY_LENGTH = 32; + static { // 关闭hutool 强制关闭Bouncy Castle库的依赖 SecureUtil.disableBouncyCastle(); @@ -92,28 +99,72 @@ public class PasswordDecoderFilter extends OncePerRequestFilter { RepeatBodyRequestWrapper requestWrapper = new RepeatBodyRequestWrapper(request); Map+ * 包级可见以便单元测试直接覆盖;当配置为空时按 AES 兜底,避免老配置升级后无法登录。 + * + * @param password 前端加密后的密码 + * @return 解密后的明文密码 + * @throws IllegalArgumentException 当配置的密钥长度不符合所选算法要求 + */ + String decryptPassword(String password) { + AuthSecurityConfigProperties.EncodeType encodeType = authSecurityConfigProperties.getEncodeType(); + if (encodeType == null) { + encodeType = AuthSecurityConfigProperties.EncodeType.AES; + } + return switch (encodeType) { + case AES -> decryptByAes(password); + case SM4 -> decryptBySm4(password); + }; + } + + /** + * 使用 AES(CFB / NoPadding,IV 等于密钥)解密密码。 + * + * @param password 前端 AES 加密后的密码 + * @return 明文密码 + */ + private String decryptByAes(String password) { + String encodeKey = authSecurityConfigProperties.getEncodeKey(); + Assert.hasText(encodeKey, "AES encode-key must not be empty (AES登录密码解密密钥不能为空)"); + byte[] keyBytes = encodeKey.getBytes(); + Assert.isTrue(keyBytes.length == AES_KEY_LENGTH, + "AES encode-key must be 16 bytes (AES登录密码解密密钥必须为16字节)"); + + // 构建前端对应解密 AES 因子 + AES aes = new AES(Mode.CFB, Padding.NoPadding, new SecretKeySpec(keyBytes, KEY_ALGORITHM), + new IvParameterSpec(keyBytes)); + return aes.decryptStr(password); + } + + /** + * 使用国密 SM4 解密密码,密钥需为 32 位 HEX 字符串。 + * + * @param password 前端 SM4 加密后的密码 + * @return 明文密码 + */ + private String decryptBySm4(String password) { + String encodeKey = authSecurityConfigProperties.getEncodeKey(); + Assert.hasText(encodeKey, "SM4 encode-key must not be empty (SM4登录密码解密密钥不能为空)"); + Assert.isTrue(StrUtil.length(encodeKey) == SM4_HEX_KEY_LENGTH && HexUtil.isHexNumber(encodeKey), + "SM4 encode-key must be 32 hex characters (SM4登录密码解密密钥必须为32位HEX字符串)"); + return SmUtil.sm4(HexUtil.decodeHex(encodeKey)).decryptStr(password); + } + + /** + * 根据请求的clientId 查询客户端配置是否是加密传输 * @param request 请求上下文 - * @return true 加密传输 、 false 原文传输 + * @return true 加密传输,false 明文传输 */ private boolean isEncClient(HttpServletRequest request) { String header = request.getHeader(HttpHeaders.AUTHORIZATION); diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/ValidateCodeFilter.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/ValidateCodeFilter.java index 626a5fba3..ed46d6753 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/ValidateCodeFilter.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/ValidateCodeFilter.java @@ -8,20 +8,11 @@ package com.pig4cloud.pigx.auth.support.filter; */ import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import com.anji.captcha.model.vo.CaptchaVO; -import com.anji.captcha.service.CaptchaService; -import com.pig4cloud.pigx.auth.support.core.AuthErrorCodes; -import com.pig4cloud.pigx.common.core.constant.CacheConstants; +import com.pig4cloud.pigx.auth.support.core.AuthCaptchaSupport; import com.pig4cloud.pigx.common.core.constant.CommonConstants; import com.pig4cloud.pigx.common.core.constant.SecurityConstants; -import com.pig4cloud.pigx.common.core.constant.enums.CaptchaFlagTypeEnum; import com.pig4cloud.pigx.common.core.exception.ValidateCodeException; -import com.pig4cloud.pigx.common.core.util.MsgUtils; -import com.pig4cloud.pigx.common.core.util.SpringContextHolder; import com.pig4cloud.pigx.common.core.util.WebUtils; -import com.pig4cloud.pigx.common.data.cache.RedisUtils; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -47,6 +38,8 @@ import java.io.IOException; @RequiredArgsConstructor public class ValidateCodeFilter extends OncePerRequestFilter { + private final AuthCaptchaSupport authCaptchaSupport; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { @@ -97,55 +90,7 @@ public class ValidateCodeFilter extends OncePerRequestFilter { * 校验验证码 */ private void checkCode() throws ValidateCodeException { - HttpServletRequest request = WebUtils.getRequest(); - String code = request.getParameter("code"); - - if (StrUtil.isBlank(code)) { - throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_EMPTY)); - } - - String randomStr = request.getParameter("randomStr"); - - // https://gitee.com/log4j/pig/issues/IWA0D - String mobile = request.getParameter("mobile"); - if (StrUtil.isNotBlank(mobile)) { - randomStr = mobile; - } - - // 若是滑块登录 - if (StrUtil.equalsAnyIgnoreCase(randomStr, CommonConstants.IMAGE_CODE_BLOCK_PUZZLE, CommonConstants.IMAGE_CODE_CLICK_WORD)) { - CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class); - CaptchaVO vo = new CaptchaVO(); - vo.setCaptchaVerification(code); - vo.setCaptchaType(randomStr); - if (!captchaService.verification(vo).isSuccess()) { - throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_EMPTY)); - } - return; - } - - String key = CacheConstants.DEFAULT_CODE_KEY + randomStr; - if (!RedisUtils.hasKey(key)) { - throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID)); - } - - String codeObj = RedisUtils.get(key); - - if (codeObj == null) { - throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID)); - } - - if (StrUtil.isBlank(codeObj)) { - RedisUtils.delete(key); - throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID)); - } - - if (!StrUtil.equals(codeObj, code)) { - RedisUtils.delete(key); - throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID)); - } - - RedisUtils.delete(key); + authCaptchaSupport.validateCode(WebUtils.getRequest()); } /** @@ -159,18 +104,7 @@ public class ValidateCodeFilter extends OncePerRequestFilter { String clientId = WebUtils.extractClientId(header).orElse(null); // 获取租户拼接区分租户的key String tenantId = request.getHeader(CommonConstants.TENANT_ID); - String key = String.format("%s:%s:%s", StrUtil.isBlank(tenantId) ? CommonConstants.TENANT_ID_1 : tenantId, - CacheConstants.CLIENT_FLAG, clientId); - - String val = RedisUtils.get(key); - - // 当配置不存在时,不用校验 - if (val == null) { - return false; - } - - JSONObject information = JSONUtil.parseObj(val); - return !StrUtil.equals(CaptchaFlagTypeEnum.OFF.getType(), information.getStr(CommonConstants.CAPTCHA_FLAG)); + return authCaptchaSupport.isCaptchaEnabled(tenantId, clientId); } } diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/FormAuthenticationFailureHandler.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/FormAuthenticationFailureHandler.java index 3f7676e1f..917e8e78a 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/FormAuthenticationFailureHandler.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/FormAuthenticationFailureHandler.java @@ -16,8 +16,11 @@ package com.pig4cloud.pigx.auth.support.handler; +import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.CharsetUtil; import cn.hutool.http.HttpUtil; +import com.pig4cloud.pigx.common.core.constant.CommonConstants; +import com.pig4cloud.pigx.common.core.constant.SecurityConstants; import com.pig4cloud.pigx.common.core.util.WebUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -51,15 +54,22 @@ public class FormAuthenticationFailureHandler implements AuthenticationFailureHa // 获取当前请求的context-path String contextPath = request.getContextPath(); + String tenantId = request.getParameter(CommonConstants.TENANT_ID); + String clientId = request.getParameter(SecurityConstants.CLIENT_ID); + String redirectUrl = String.format("%s/token/login?error=%s", contextPath, exception.getMessage()); + if (StrUtil.isNotBlank(tenantId)) { + redirectUrl = String.format("%s&%s=%s", redirectUrl, CommonConstants.TENANT_ID, tenantId); + } + if (StrUtil.isNotBlank(clientId)) { + redirectUrl = String.format("%s&%s=%s", redirectUrl, SecurityConstants.CLIENT_ID, clientId); + } // 构建重定向URL,加入context-path - String url = HttpUtil.encodeParams( - String.format("%s/token/login?error=%s", contextPath, exception.getMessage()), - CharsetUtil.CHARSET_UTF_8); + String url = HttpUtil.encodeParams(redirectUrl, CharsetUtil.CHARSET_UTF_8); try { WebUtils.getResponse().sendRedirect(url); - } catch (IOException e) { + } catch (IOException e) { log.error("重定向失败", e); } } diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxAuthenticationFailureEventHandler.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxAuthenticationFailureEventHandler.java index fbc22a940..0deefc769 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxAuthenticationFailureEventHandler.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxAuthenticationFailureEventHandler.java @@ -39,7 +39,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -50,15 +49,19 @@ import org.springframework.stereotype.Component; import java.io.IOException; /** + * 认证失败处理器:处理用户认证失败事件 + * * @author lengleng - * @date 2022-06-02 + * @date 2025/08/19 */ @Slf4j @Component @RequiredArgsConstructor +@SuppressWarnings("removal") public class PigxAuthenticationFailureEventHandler implements AuthenticationFailureHandler { - private static final MappingJackson2HttpMessageConverter errorHttpResponseConverter = new MappingJackson2HttpMessageConverter(); + private static final org.springframework.http.converter.json.MappingJackson2HttpMessageConverter errorHttpResponseConverter = + new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(); private final ApplicationEventPublisher publisher; @@ -81,13 +84,11 @@ public class PigxAuthenticationFailureEventHandler implements AuthenticationFail log.info("登录失败,异常:{}", exception.getLocalizedMessage()); // 密码模式记录错误信息 - if (OAuth2ParameterNames.PASSWORD.equals(grantType)) { - String username = request.getParameter(OAuth2ParameterNames.USERNAME); + if (SecurityConstants.PASSWORD.equals(grantType)) { + String username = request.getParameter(SecurityConstants.DETAILS_USERNAME); - // 密码错误记录错误次数 (TOC用户不记录) - String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC); - if (exception instanceof OAuth2AuthenticationException - && !SecurityConstants.HEADER_TOC_YES.equals(header)) { + // 密码错误记录错误次数 + if (exception instanceof OAuth2AuthenticationException) { recordLoginFailureTimes(username); } @@ -126,12 +127,17 @@ public class PigxAuthenticationFailureEventHandler implements AuthenticationFail } /** - * 记录登录失败此处,如果操作阈值 调用接口锁定用户 - * @param username username + * 记录登录失败次数,超过阈值时调用接口锁定用户。 + *
若系统参数 LOGIN_ERROR_TIMES {@literal <=} 0,则禁用锁定功能并清除已有失败计数 key。
+ * @param username 用户名 */ private void recordLoginFailureTimes(String username) { String key = String.format("%s%s:%s", CacheConstants.GLOBALLY, CacheConstants.LOGIN_ERROR_TIMES, username); Long deltaTimes = ParamResolver.getLong("LOGIN_ERROR_TIMES", 5L); + if (!isLoginFailureLockEnabled(deltaTimes)) { + RedisUtils.delete(key); + return; + } // 使用 RedisUtils 执行原生 Redis 命令进行递增操作 Long times = RedisUtils.increment(key, 1L);// 增加登录失败次数 @@ -145,6 +151,24 @@ public class PigxAuthenticationFailureEventHandler implements AuthenticationFail } } + /** + * 判断登录失败锁定功能是否启用(参数值大于 0 表示启用) + * @param deltaTimes 系统参数 LOGIN_ERROR_TIMES,0 或 null 表示禁用 + * @return 启用返回 true,禁用返回 false + */ + static boolean isLoginFailureLockEnabled(Long deltaTimes) { + return deltaTimes != null && deltaTimes > 0; + } + + /** + * 写出认证失败响应。 + *
+ * OAuth2 认证异常携带标准错误码和描述,需要保留错误码给前端识别;其他认证异常只返回本地化错误信息。
+ * @param request 当前认证请求
+ * @param response 当前认证响应
+ * @param exception 认证失败异常
+ * @throws IOException 响应写出失败时抛出
+ */
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
@@ -153,6 +177,7 @@ public class PigxAuthenticationFailureEventHandler implements AuthenticationFail
if (exception instanceof OAuth2AuthenticationException) {
OAuth2AuthenticationException authorizationException = (OAuth2AuthenticationException) exception;
+ // 优先使用 OAuth2 错误描述;没有描述时回退到标准错误码。
errorMessage = StrUtil.isBlank(authorizationException.getError().getDescription())
? authorizationException.getError().getErrorCode()
: authorizationException.getError().getDescription();
@@ -163,6 +188,7 @@ public class PigxAuthenticationFailureEventHandler implements AuthenticationFail
}
else {
+ // 非 OAuth2 异常没有标准错误码,直接返回 Spring Security 的本地化异常消息。
errorMessage = exception.getLocalizedMessage();
this.errorHttpResponseConverter.write(R.failed(errorMessage), MediaType.APPLICATION_JSON, httpResponse);
}
diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxAuthenticationSuccessEventHandler.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxAuthenticationSuccessEventHandler.java
index 918edcce4..12a825855 100644
--- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxAuthenticationSuccessEventHandler.java
+++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxAuthenticationSuccessEventHandler.java
@@ -52,8 +52,10 @@ import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
+ * 认证成功事件处理器:处理用户认证成功后的相关操作
+ *
* @author lengleng
- * @date 2022-06-02
+ * @date 2025/08/19
*/
@Slf4j
@Component
diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxLogoutSuccessEventHandler.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxLogoutSuccessEventHandler.java
index 152d960ac..c2e6f504b 100644
--- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxLogoutSuccessEventHandler.java
+++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/handler/PigxLogoutSuccessEventHandler.java
@@ -17,16 +17,26 @@
package com.pig4cloud.pigx.auth.support.handler;
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
+import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.util.WebUtils;
+import com.pig4cloud.pigx.common.data.cache.RedisUtils;
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationListener;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component;
+import java.util.Set;
+
/**
* @author zhangran
* @date 2022-06-02
@@ -35,12 +45,22 @@ import org.springframework.stereotype.Component;
*/
@Slf4j
@Component
+@RequiredArgsConstructor
public class PigxLogoutSuccessEventHandler implements ApplicationListener