♻️ Refactoring code. 架构重构支持 一套代码两种方式运行

This commit is contained in:
冷冷 committed 2024-04-18 18:01:40 +08:00
1 parent d5d48d369f
commit 159ec6d9d4
41 files changed
+1240 -1339

No files matched your search

@@ -16,7 +16,6 @@
package com.pig4cloud.pigx.auth;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import com.pig4cloud.pigx.common.feign.annotation.EnablePigxFeignClients;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -28,7 +27,7 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
*/
@EnablePigxFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@SpringBootApplication
public class PigxAuthApplication {
public static void main(String[] args) {
@@ -21,6 +21,8 @@ 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.PasswordDecoderFilter;
import com.pig4cloud.pigx.auth.support.filter.ValidateCodeFilter;
import com.pig4cloud.pigx.auth.support.handler.PigxAuthenticationFailureEventHandler;
import com.pig4cloud.pigx.auth.support.handler.PigxAuthenticationSuccessEventHandler;
import com.pig4cloud.pigx.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationConverter;
@@ -31,6 +33,7 @@ import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.security.handler.FormAuthenticationFailureHandler;
import com.pig4cloud.pigx.common.security.handler.SsoLogoutSuccessHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@@ -44,11 +47,15 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
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.*;
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
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;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -57,111 +64,122 @@ import java.util.Arrays;
/**
* @author lengleng
* @date 2022/5/27
*
* <p>
* 认证服务器配置
*/
@Configuration
@RequiredArgsConstructor
public class AuthorizationServerConfiguration {
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationService authorizationService;
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
PigxAuthenticationSuccessEventHandler successEventHandler,
PigxAuthenticationFailureEventHandler failureEventHandler) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
private final PasswordDecoderFilter passwordDecoderFilter;
http.with(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
.accessTokenResponseHandler(successEventHandler) // 登录成功处理器
.errorResponseHandler(failureEventHandler);// 登录失败处理器
}).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
oAuth2ClientAuthenticationConfigurer.errorResponseHandler(failureEventHandler))// 处理客户端认证异常
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)),Customizer.withDefaults());
private final ValidateCodeFilter validateCodeFilter;
AntPathRequestMatcher[] requestMatchers = new AntPathRequestMatcher[] {
AntPathRequestMatcher.antMatcher("/token/**"), AntPathRequestMatcher.antMatcher("/actuator/**"),
AntPathRequestMatcher.antMatcher("/css/**"), AntPathRequestMatcher.antMatcher("/error") };
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnProperty(value = "security.micro", matchIfMissing = true)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
PigxAuthenticationSuccessEventHandler successEventHandler,
PigxAuthenticationFailureEventHandler failureEventHandler) throws Exception {
// 增加验证码过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
// 增加密码解密过滤器
http.addFilterBefore(passwordDecoderFilter, UsernamePasswordAuthenticationFilter.class);
http.authorizeHttpRequests(authorizeRequests -> {
// 自定义接口、端点暴露
authorizeRequests.requestMatchers(requestMatchers).permitAll();
authorizeRequests.anyRequest().authenticated();
})
.with(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
.authorizationServerSettings(
AuthorizationServerSettings.builder().issuer(SecurityConstants.PIGX_LICENSE).build()), Customizer.withDefaults());
http.with(new FormIdentityLoginConfigurer(), Customizer.withDefaults());
DefaultSecurityFilterChain securityFilterChain = http.build();
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
http.with(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
.accessTokenResponseHandler(successEventHandler) // 登录成功处理器
.errorResponseHandler(failureEventHandler);// 登录失败处理器
}).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
oAuth2ClientAuthenticationConfigurer.errorResponseHandler(failureEventHandler))// 处理客户端认证异常
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)), Customizer.withDefaults());
// 注入自定义授权模式实现
addCustomOAuth2GrantAuthenticationProvider(http);
return securityFilterChain;
}
AntPathRequestMatcher[] requestMatchers = new AntPathRequestMatcher[]{
AntPathRequestMatcher.antMatcher("/token/**"), AntPathRequestMatcher.antMatcher("/actuator/**"),
AntPathRequestMatcher.antMatcher("/css/**"), AntPathRequestMatcher.antMatcher("/error")};
/**
* 令牌生成规则实现 </br>
* client:username:uuid
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator oAuth2TokenGenerator() {
CustomeOAuth2AccessTokenGenerator accessTokenGenerator = new CustomeOAuth2AccessTokenGenerator();
// 注入Token 增加关联用户信息
accessTokenGenerator.setAccessTokenCustomizer(new CustomeOAuth2TokenCustomizer());
return new DelegatingOAuth2TokenGenerator(accessTokenGenerator, new OAuth2RefreshTokenGenerator());
}
http.authorizeHttpRequests(authorizeRequests -> {
// 自定义接口、端点暴露
authorizeRequests.requestMatchers(requestMatchers).permitAll();
authorizeRequests.anyRequest().authenticated();
})
.with(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
.authorizationServerSettings(
AuthorizationServerSettings.builder().issuer(SecurityConstants.PIGX_LICENSE).build()), Customizer.withDefaults());
http.with(new FormIdentityLoginConfigurer(), Customizer.withDefaults());
DefaultSecurityFilterChain securityFilterChain = http.build();
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new FormAuthenticationFailureHandler();
}
// 注入自定义授权模式实现
addCustomOAuth2GrantAuthenticationProvider(http);
return securityFilterChain;
}
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new SsoLogoutSuccessHandler();
}
/**
* 令牌生成规则实现 </br>
* client:username:uuid
*
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator oAuth2TokenGenerator() {
CustomeOAuth2AccessTokenGenerator accessTokenGenerator = new CustomeOAuth2AccessTokenGenerator();
// 注入Token 增加关联用户信息
accessTokenGenerator.setAccessTokenCustomizer(new CustomeOAuth2TokenCustomizer());
return new DelegatingOAuth2TokenGenerator(accessTokenGenerator, new OAuth2RefreshTokenGenerator());
}
/**
* request -> xToken 注入请求转换器
* @return DelegatingAuthenticationConverter
*/
private AuthenticationConverter accessTokenRequestConverter() {
return new DelegatingAuthenticationConverter(Arrays.asList(
new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
new OAuth2ResourceOwnerSmsAuthenticationConverter(), new PigxOAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new FormAuthenticationFailureHandler();
}
/**
* 注入授权模式实现提供方
*
* 1. 密码模式 </br>
* 2. 短信登录 </br>
*
*/
@SuppressWarnings("unchecked")
private void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http) {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new SsoLogoutSuccessHandler();
}
OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
/**
* request -> xToken 注入请求转换器
*
* @return DelegatingAuthenticationConverter
*/
@Bean
public AuthenticationConverter accessTokenRequestConverter() {
return new DelegatingAuthenticationConverter(Arrays.asList(
new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
new OAuth2ResourceOwnerSmsAuthenticationConverter(), new PigxOAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
}
OAuth2ResourceOwnerSmsAuthenticationProvider resourceOwnerSmsAuthenticationProvider = new OAuth2ResourceOwnerSmsAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
/**
* 注入授权模式实现提供方
* <p>
* 1. 密码模式 </br>
* 2. 短信登录 </br>
*/
@SuppressWarnings("unchecked")
private void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http) {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
// 处理 UsernamePasswordAuthenticationToken
http.authenticationProvider(new PigxDaoAuthenticationProvider());
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
}
OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
OAuth2ResourceOwnerSmsAuthenticationProvider resourceOwnerSmsAuthenticationProvider = new OAuth2ResourceOwnerSmsAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
// 处理 UsernamePasswordAuthenticationToken
http.authenticationProvider(new PigxDaoAuthenticationProvider());
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
}
}
@@ -0,0 +1,47 @@
package com.pig4cloud.pigx.auth.endpoint;
import com.anji.captcha.service.CaptchaCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author lengleng
* @date 2020/8/27
* <p>
* 验证码 缓存提供支持集群,需要通过SPI
*/
public class CaptchaCacheServiceProvider implements CaptchaCacheService {
private static final String REDIS = "redis";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void set(String key, String value, long expiresInSeconds) {
stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
}
@Override
public boolean exists(String key) {
return stringRedisTemplate.hasKey(key);
}
@Override
public void delete(String key) {
stringRedisTemplate.delete(key);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public String type() {
return REDIS;
}
}
@@ -0,0 +1,84 @@
package com.pig4cloud.pigx.auth.endpoint;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
import io.springboot.captcha.ArithmeticCaptcha;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* 验证码相关的接口
*
* @author lengleng
* @date 2022/6/27
*/
@RestController
@RequestMapping("/code")
@RequiredArgsConstructor
public class ImageCodeEndpoint {
private static final Integer DEFAULT_IMAGE_WIDTH = 100;
private static final Integer DEFAULT_IMAGE_HEIGHT = 40;
private final StringRedisTemplate redisTemplate;
/**
* 创建图形验证码
*/
@SneakyThrows
@GetMapping("/image")
public void image(String randomStr, HttpServletResponse response) {
ArithmeticCaptcha captcha = new ArithmeticCaptcha(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT);
String result = captcha.text();
redisTemplate.opsForValue()
.set(CacheConstants.DEFAULT_CODE_KEY + randomStr, result, SecurityConstants.CODE_TIME, TimeUnit.SECONDS);
// 转换流信息写出
captcha.out(response.getOutputStream());
}
/**
* 行为
* 创建滑块图形验证码
*/
@SneakyThrows
@GetMapping("/create")
public R behavior() {
CaptchaVO vo = new CaptchaVO();
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
return R.ok(captchaService.get(vo));
}
/**
* 行为
* 创建滑块图形验证码
*/
@SneakyThrows
@PostMapping("/check")
public R check(HttpServletRequest request) {
CaptchaVO vo = new CaptchaVO();
vo.setPointJson(request.getParameter("pointJson"));
vo.setToken(request.getParameter("token"));
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
return R.ok(captchaService.check(vo));
}
}
@@ -0,0 +1,30 @@
package com.pig4cloud.pigx.auth.support.filter;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* @author lengleng
* @date 2020/10/4
* <p>
* 网关配置文件
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties("security")
public class AuthSecurityConfigProperties {
/**
* 是否是微服务架构
*/
private boolean isMicro;
/**
* 网关解密登录前端密码 秘钥
*/
private String encodeKey;
}
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pig4cloud.pigx.auth.support.filter;
import cn.hutool.core.util.ArrayUtil;
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.symmetric.AES;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.constant.enums.EncFlagTypeEnum;
import com.pig4cloud.pigx.common.core.util.RepeatBodyRequestWrapper;
import com.pig4cloud.pigx.common.core.util.WebUtils;
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.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.util.Map;
/**
* @author lengleng
* @date 2019 /2/1 密码解密工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PasswordDecoderFilter extends OncePerRequestFilter {
private final AuthSecurityConfigProperties authSecurityConfigProperties;
private final StringRedisTemplate redisTemplate;
private static final String PASSWORD = "password";
private static final String KEY_ALGORITHM = "AES";
static {
// 关闭hutool 强制关闭Bouncy Castle库的依赖
SecureUtil.disableBouncyCastle();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 不是登录请求,直接向下执行
if (!StrUtil.containsAnyIgnoreCase(request.getRequestURI(), SecurityConstants.OAUTH_TOKEN_URL)) {
chain.doFilter(request, response);
return;
}
// 2. 刷新token类型,直接向下执行
String grantType = request.getParameter("grant_type");
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
chain.doFilter(request, response);
return;
}
// 3. 判断客户端是否需要解密,明文传输直接向下执行
if (!isEncClient(request)) {
chain.doFilter(request, response);
return; }
// 将请求流转换为可多次读取的请求流
RepeatBodyRequestWrapper requestWrapper = new RepeatBodyRequestWrapper(request);
Map<String, String[]> parameterMap = requestWrapper.getParameterMap();
// 构建前端对应解密AES 因子
AES aes = new AES(Mode.CFB, Padding.NoPadding,
new SecretKeySpec(authSecurityConfigProperties.getEncodeKey().getBytes(), KEY_ALGORITHM),
new IvParameterSpec(authSecurityConfigProperties.getEncodeKey().getBytes()));
parameterMap.forEach((k, v) -> {
String[] values = parameterMap.get(k);
if (!PASSWORD.equals(k) || ArrayUtil.isEmpty(values)) {
return;
}
// 解密密码
String decryptPassword = aes.decryptStr(values[0]);
parameterMap.put(k, new String[]{decryptPassword});
});
chain.doFilter(requestWrapper, response);
}
/**
* 根据请求的clientId 查询客户端配置是否是加密传输
*
* @param request 请求上下文
* @return true 加密传输 、 false 原文传输
*/
private boolean isEncClient(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
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 = redisTemplate.opsForValue().get(key);
// 当配置不存在时,默认需要解密
if (val == null) {
return true;
}
JSONObject information = JSONUtil.parseObj(val);
return !StrUtil.equals(EncFlagTypeEnum.NO.getType(), information.getStr(CommonConstants.ENC_FLAG));
}
}
@@ -0,0 +1,179 @@
package com.pig4cloud.pigx.auth.support.filter;
/**
* 登录前处理器
*
* @author lengleng
* @date 2024/4/3
*/
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.common.core.constant.CacheConstants;
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.SpringContextHolder;
import com.pig4cloud.pigx.common.core.util.WebUtils;
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.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* @author lbw
* @date 2024-01-06
* <p>
* 登录前置处理器: 前端密码传输密文解密,验证码处理
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
private final StringRedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestUrl = request.getServletPath();
// 不是登录URL 请求直接跳过
if (!SecurityConstants.OAUTH_TOKEN_URL.equals(requestUrl)) {
filterChain.doFilter(request, response);
return;
}
// 如果登录URL 但是刷新token的请求,直接向下执行
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (StrUtil.containsAny(grantType, SecurityConstants.REFRESH_TOKEN)) {
filterChain.doFilter(request, response);
return;
}
// mobile模式, 如果请求不包含mobile 参数直接
String mobile = request.getParameter(SecurityConstants.GRANT_MOBILE);
if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && StrUtil.isBlank(mobile)) {
throw new OAuth2AuthenticationException(SecurityConstants.GRANT_MOBILE);
}
// mobile模式, 社交登录模式不校验验证码直接跳过
if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && !StrUtil.contains(mobile, "SMS")) {
filterChain.doFilter(request, response);
return;
}
// 判断客户端是否跳过检验
if (!isCheckCaptchaClient(request)) {
filterChain.doFilter(request, response);
return;
}
// 校验验证码 1. 客户端开启验证码 2. 短信模式
try {
checkCode();
filterChain.doFilter(request, response);
} catch (ValidateCodeException validateCodeException) {
throw new OAuth2AuthenticationException(validateCodeException.getMessage());
}
}
/**
* 校验验证码
*/
private void checkCode() throws ValidateCodeException {
HttpServletRequest request = WebUtils.getRequest();
String code = request.getParameter("code");
if (StrUtil.isBlank(code)) {
throw new ValidateCodeException("验证码不能为空");
}
String randomStr = request.getParameter("randomStr");
// https://gitee.com/log4j/pig/issues/IWA0D
String mobile = request.getParameter("mobile");
if (StrUtil.isNotBlank(mobile)) {
randomStr = mobile;
}
// 若是滑块登录
if (CommonConstants.IMAGE_CODE_TYPE.equalsIgnoreCase(randomStr)) {
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
CaptchaVO vo = new CaptchaVO();
vo.setCaptchaVerification(code);
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
if (!captchaService.verification(vo).isSuccess()) {
throw new ValidateCodeException("验证码不能为空");
}
return;
}
String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
throw new ValidateCodeException("验证码不合法");
}
Object codeObj = redisTemplate.opsForValue().get(key);
if (codeObj == null) {
throw new ValidateCodeException("验证码不合法");
}
String saveCode = codeObj.toString();
if (StrUtil.isBlank(saveCode)) {
redisTemplate.delete(key);
throw new ValidateCodeException("验证码不合法");
}
if (!StrUtil.equals(saveCode, code)) {
redisTemplate.delete(key);
throw new ValidateCodeException("验证码不合法");
}
redisTemplate.delete(key);
}
/**
* 是否需要校验客户端,根据client 查询客户端配置
*
* @param request 请求
* @return true 需要校验, false 不需要校验
*/
private boolean isCheckCaptchaClient(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
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 = redisTemplate.opsForValue().get(key);
// 当配置不存在时,不用校验
if (val == null) {
return false;
}
JSONObject information = JSONUtil.parseObj(val);
return !StrUtil.equals(CaptchaFlagTypeEnum.OFF.getType(), information.getStr(CommonConstants.CAPTCHA_FLAG));
}
}
@@ -23,7 +23,6 @@ import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.core.util.WebUtils;
import com.pig4cloud.pigx.common.data.resolver.ParamResolver;
@@ -163,11 +162,6 @@ public class PigxAuthenticationFailureEventHandler implements AuthenticationFail
errorMessage = exception.getLocalizedMessage();
}
// 手机号登录
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (SecurityConstants.APP.equals(grantType)) {
errorMessage = MsgUtils.getSecurityMessage("AbstractUserDetailsAuthenticationProvider.smsBadCredentials");
}
this.errorHttpResponseConverter.write(R.failed(errorMessage), MediaType.APPLICATION_JSON, httpResponse);
}
@@ -0,0 +1 @@
com.pig4cloud.pigx.auth.endpoint.CaptchaCacheServiceProvider
@@ -79,6 +79,12 @@
<logger name="com.alibaba.nacos" level="OFF">
<appender-ref ref="error"/>
</logger>
<!--AJ 验证码INFO 屏蔽-->
<logger name="com.anji.captcha" level="OFF">
<appender-ref ref="error"/>
</logger>
<!-- Level: FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7 -->
<root level="INFO">
<appender-ref ref="console"/>