提交 a4fd8139 编写于 作者: R Ray Hao

refactor: 项目升级重构

上级 f76909ef
......@@ -64,25 +64,24 @@
youlai-mall
├── docs
├── nacos # Nacos配置
├── nacos_config.zip # Nacos脚本
├── sql # SQL脚本
├── mysql5 # MySQL5 脚本
├── mysql8 # MySQL8 脚本
├── mysql5 # MySQL5脚本
├── mysql8 # MySQL8脚本
├── mall-oms # 订单服务
├── mall-pms # 商品服务
├── mall-sms # 营销服务
├── mall-ums # 会员服务
├── youlai-auth # OAuth2认证授权中心
├── youlai-auth # 认证授权中心
├── youlai-common # 公共模块
├── common-core # 核心依赖
├── common-file # 文件公共模块
├── common-core # 基础依赖
├── common-log # 日志公共模块
├── common-mybatis # mybatis 公共模块
├── common-rabbitmq # rabbitmq 公共模块
├── common-redis # redis 公共模块
├── common-seata # seata 公共模块
├── common-mybatis # Mybatis 公共模块
├── common-rabbitmq # RabbitMQ 公共模块
├── common-redis # Redis 公共模块
├── common-seata # Seata 公共模块
├── common-security # 资源服务器安全公共模块
├── common-sms # 短信公共模块
├── common-web # web 公共模块
├── common-web # Web 公共模块
├── youlai-gateway # 网关
├── youlai-system # 系统服务
├── system-api # 系统Feign接口
......
......@@ -95,7 +95,7 @@ public class CartServiceImpl implements CartService {
try {
memberId = SecurityUtils.getMemberId();
} catch (Exception e) {
throw new BizException(ResultCode.INVALID_TOKEN);
throw new BizException(ResultCode.TOKEN_INVALID);
}
BoundHashOperations cartHashOperations = getCartHashOperations(memberId);
String hKey = cartItem.getSkuId() + "";
......@@ -121,7 +121,7 @@ public class CartServiceImpl implements CartService {
try {
memberId = SecurityUtils.getMemberId();
} catch (Exception e) {
throw new BizException(ResultCode.INVALID_TOKEN);
throw new BizException(ResultCode.TOKEN_INVALID);
}
BoundHashOperations cartHashOperations = getCartHashOperations(memberId);
String hKey = skuId + "";
......@@ -139,7 +139,7 @@ public class CartServiceImpl implements CartService {
try {
memberId = SecurityUtils.getMemberId();
} catch (Exception e) {
throw new BizException(ResultCode.INVALID_TOKEN);
throw new BizException(ResultCode.TOKEN_INVALID);
}
BoundHashOperations cartHashOperations = getCartHashOperations(memberId);
for (Object value : cartHashOperations.values()) {
......@@ -161,7 +161,7 @@ public class CartServiceImpl implements CartService {
public boolean removeCheckedItem() {
Long memberId = SecurityUtils.getMemberId();
if (memberId == null) {
throw new BizException(ResultCode.INVALID_TOKEN);
throw new BizException(ResultCode.TOKEN_INVALID);
}
BoundHashOperations cartHashOperations = getCartHashOperations(memberId);
for (Object value : cartHashOperations.values()) {
......
......@@ -53,7 +53,7 @@
<swagger.version>2.1.0</swagger.version>
<!-- 工具 -->
<hutool.version>5.8.25</hutool.version>
<hutool.version>5.8.26</hutool.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<weixin-java.version>4.1.5.B</weixin-java.version>
<easyexcel.version>3.3.2</easyexcel.version>
......@@ -214,12 +214,6 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-captcha</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
......
......@@ -12,10 +12,17 @@
<artifactId>youlai-auth</artifactId>
<dependencies>
<!-- OAuth2 认证服务器-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>
<!-- OAuth2 客户端(第三方登录 微信、Gitee、Github、QQ)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!--Spring Cloud & Alibaba -->
......@@ -36,18 +43,11 @@
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- OAuth2 认证服务器-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>ums-api</artifactId>
......@@ -80,12 +80,18 @@
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-sms</artifactId>
<artifactId>common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-mybatis</artifactId>
<artifactId>common-apidoc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
......
package com.youlai.auth.authentication.captcha;
import cn.hutool.core.util.StrUtil;
import com.youlai.auth.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 密码认证参数解析器
* <p>
* 解析请求参数中的用户名和密码,并构建相应的身份验证(Authentication)对象
*
* @author haoxr
* @since 3.0.0
*/
public class CaptchaAuthenticationConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
// 授权类型 (必需)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!CaptchaAuthenticationToken.CAPTCHA.getValue().equals(grantType)) {
return null;
}
// 客户端信息
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// 参数提取验证
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// 令牌申请访问范围验证 (可选)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// 用户名验证(必需)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (StrUtil.isBlank(username)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.USERNAME,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
);
}
// 密码验证(必需)
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (StrUtil.isBlank(password)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.PASSWORD,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
);
}
// 验证码(必需)
String verifyCode = parameters.getFirst(CaptchaParameterNames.CODE);
if (StrUtil.isBlank(verifyCode)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
CaptchaParameterNames.CODE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
);
}
// 验证码缓存Key(必需)
String verifyKey = parameters.getFirst(CaptchaParameterNames.KEY);
if (StrUtil.isBlank(verifyKey)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
CaptchaParameterNames.KEY,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
);
}
// 附加参数(保存用户名/密码传递给 CaptchaAuthenticationProvider 用于身份认证)
Map<String, Object> additionalParameters = parameters
.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
!e.getKey().equals(OAuth2ParameterNames.SCOPE)
).collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
return new CaptchaAuthenticationToken(
clientPrincipal,
requestedScopes,
additionalParameters
);
}
}
package com.youlai.auth.authentication.captcha;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.core.lang.Assert;
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
import com.youlai.common.constant.SecurityConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jwt.Jwt;
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.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.CollectionUtils;
import java.security.Principal;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 验证码模式身份验证提供者
* <p>
* 处理基于用户名和密码的身份验证
*
* @author haoxr
* @see org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider
* @since 3.0.0
*/
@Slf4j
public class CaptchaAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private final AuthenticationManager authenticationManager;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private final StringRedisTemplate redisTemplate;
/**
* Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using the provided parameters.
*
* @param authenticationManager the authentication manager
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public CaptchaAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
StringRedisTemplate redisTemplate
) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authenticationManager = authenticationManager;
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
this.redisTemplate = redisTemplate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
CaptchaAuthenticationToken captchaAuthenticationToken = (CaptchaAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
.getAuthenticatedClientElseThrowInvalidClient(captchaAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 验证客户端是否支持授权类型(grant_type=password)
if (!registeredClient.getAuthorizationGrantTypes().contains(CaptchaAuthenticationToken.CAPTCHA)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
// 验证码校验
Map<String, Object> additionalParameters = captchaAuthenticationToken.getAdditionalParameters();
String verifyCode = (String) additionalParameters.get(CaptchaParameterNames.CODE);
String verifyCodeKey = (String) additionalParameters.get(CaptchaParameterNames.KEY);
String cacheCode = redisTemplate.opsForValue().get(SecurityConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey);
// 验证码比对
MathGenerator mathGenerator = new MathGenerator();
if (!mathGenerator.verify(cacheCode, verifyCode)) {
throw new OAuth2AuthenticationException("验证码错误");
}
// 生成用户名密码身份验证令牌
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 用户名密码身份验证,成功后返回带有权限的认证信息
Authentication usernamePasswordAuthentication;
try {
usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
} catch (Exception e) {
// 需要将其他类型的异常转换为 OAuth2AuthenticationException 才能被自定义异常捕获处理,逻辑源码 OAuth2TokenEndpointFilter#doFilterInternal
throw new OAuth2AuthenticationException(e.getCause() != null ? e.getCause().getMessage() : e.getMessage());
}
// 验证申请访问范围(Scope)
Set<String> authorizedScopes = registeredClient.getScopes();
Set<String> requestedScopes = captchaAuthenticationToken.getScopes();
if (!CollectionUtils.isEmpty(requestedScopes)) {
Set<String> unauthorizedScopes = requestedScopes.stream()
.filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope))
.collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
authorizedScopes = new LinkedHashSet<>(requestedScopes);
}
// 访问令牌(Access Token) 构造器
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication) // 身份验证成功的认证信息(用户名、权限等信息)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) // 授权方式
.authorizationGrant(captchaAuthenticationToken) // 授权具体对象
;
// 生成访问令牌(Access Token)
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType((OAuth2TokenType.ACCESS_TOKEN)).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA)
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
// 生成刷新令牌(Refresh Token)
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
// ----- ID token -----
OidcIdToken idToken;
if (requestedScopes.contains(OidcScopes.OPENID)) {
// @formatter:off
tokenContext = tokenContextBuilder
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token
.build();
// @formatter:on
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedIdToken instanceof Jwt)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the ID token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (log.isTraceEnabled()) {
log.trace("Generated id token");
}
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
authorizationBuilder.token(idToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
} else {
idToken = null;
}
// 持久化令牌发放记录到数据库
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
additionalParameters = (idToken != null)
? Collections.singletonMap(OidcParameterNames.ID_TOKEN, idToken.getTokenValue())
: Collections.emptyMap();
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}
@Override
public boolean supports(Class<?> authentication) {
return CaptchaAuthenticationToken.class.isAssignableFrom(authentication);
}
}
package com.youlai.auth.authentication.captcha;
import jakarta.annotation.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 验证码模式身份验证令牌(包含用户名、密码、验证码)
*
* @author haoxr
* @see org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken
* @since 3.0.0
*/
public class CaptchaAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
/**
* 令牌申请访问范围
*/
private final Set<String> scopes;
/**
* 授权类型(验证码: captcha)
*/
public static final AuthorizationGrantType CAPTCHA = new AuthorizationGrantType("captcha");
/**
* 验证码模式身份验证令牌
*
* @param clientPrincipal 客户端信息
* @param scopes 令牌申请访问范围
* @param additionalParameters 自定义额外参数(用户名、密码、验证码)
*/
public CaptchaAuthenticationToken(
Authentication clientPrincipal,
Set<String> scopes,
@Nullable Map<String, Object> additionalParameters
) {
super(CAPTCHA, clientPrincipal, additionalParameters);
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}
/**
* 用户凭证(密码)
*/
@Override
public Object getCredentials() {
return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD);
}
public Set<String> getScopes() {
return scopes;
}
}
package com.youlai.auth.authentication.captcha;
/**
* 验证码模式请求参数名称常量
*
* @author haoxr
* @since 3.0.0
*/
public final class CaptchaParameterNames {
/**
* 验证码缓存Key (唯一标识) 用于从Redis获取验证码Code
*/
public static final String KEY = "captchaKey";
/**
* 验证码 Code
*/
public static final String CODE = "captchaCode";
private CaptchaParameterNames() {
}
}
......@@ -2,6 +2,7 @@
package com.youlai.auth.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
......@@ -10,23 +11,20 @@ import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.youlai.auth.authentication.captcha.CaptchaAuthenticationConverter;
import com.youlai.auth.authentication.captcha.CaptchaAuthenticationProvider;
import com.youlai.auth.authentication.captcha.CaptchaAuthenticationToken;
import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationConverter;
import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationProvider;
import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationToken;
import com.youlai.auth.authentication.password.PasswordAuthenticationConverter;
import com.youlai.auth.authentication.password.PasswordAuthenticationProvider;
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationConverter;
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationProvider;
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationToken;
import com.youlai.auth.handler.MyAuthenticationFailureHandler;
import com.youlai.auth.handler.MyAuthenticationSuccessHandler;
import com.youlai.auth.service.MemberDetailsService;
import com.youlai.auth.model.SysUserDetails;
import com.youlai.auth.jackson.SysUserMixin;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.auth.oauth2.extension.miniapp.WxMiniAppAuthenticationConverter;
import com.youlai.auth.oauth2.extension.miniapp.WxMiniAppAuthenticationProvider;
import com.youlai.auth.oauth2.extension.miniapp.WxMiniAppAuthenticationToken;
import com.youlai.auth.oauth2.extension.password.PasswordAuthenticationConverter;
import com.youlai.auth.oauth2.extension.password.PasswordAuthenticationProvider;
import com.youlai.auth.oauth2.extension.sms.SmsAuthenticationConverter;
import com.youlai.auth.oauth2.extension.sms.SmsAuthenticationProvider;
import com.youlai.auth.oauth2.extension.sms.SmsAuthenticationToken;
import com.youlai.auth.oauth2.handler.MyAuthenticationFailureHandler;
import com.youlai.auth.oauth2.handler.MyAuthenticationSuccessHandler;
import com.youlai.auth.oauth2.jackson.SysUserMixin;
import com.youlai.auth.service.MemberDetailsService;
import com.youlai.common.constant.RedisConstants;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
......@@ -34,7 +32,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.security.authentication.AuthenticationManager;
......@@ -64,7 +64,8 @@ import org.springframework.security.oauth2.server.authorization.settings.ClientS
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
......@@ -78,6 +79,7 @@ import java.util.UUID;
* 授权服务器配置
*
* @author haoxr
* @see <a href="https://github.com/spring-projects/spring-authorization-server/blob/49b199c5b41b5f9279d9758fc2f5d24ed1fe4afa/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java#L112">AuthorizationServerConfig</a>
* @since 3.0.0
*/
@Configuration
......@@ -86,9 +88,13 @@ import java.util.UUID;
public class AuthorizationServerConfig {
private final WxMaService wxMaService;
private final StringRedisTemplate redisTemplate;
private final RedisTemplate<String, String> redisTemplate;
private final MemberDetailsService memberDetailsService;
private final OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent"; // 自定义授权页
private static final String CUSTOM_LOGIN_PAGE_URI = "/login"; // 自定义登录页
private final CodeGenerator codeGenerator;
/**
* 授权服务器端点配置
......@@ -102,46 +108,52 @@ public class AuthorizationServerConfig {
OAuth2TokenGenerator<?> tokenGenerator
) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
authorizationServerConfigurer
.oidc(Customizer.withDefaults())
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverters(
authenticationConverters ->// <1>
// 自定义授权模式转换器(Converter)
authenticationConverters.addAll(
List.of(
new PasswordAuthenticationConverter(),
new CaptchaAuthenticationConverter(),
new WxMiniAppAuthenticationConverter(),
new SmsCodeAuthenticationConverter()
)
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)) // 自定义授权页
.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
// 自定义授权模式转换器(Converter)
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.accessTokenRequestConverters(
authenticationConverters ->// <1>
// 自定义授权模式转换器(Converter)
authenticationConverters.addAll(
List.of(
new PasswordAuthenticationConverter(),
new WxMiniAppAuthenticationConverter(),
new SmsAuthenticationConverter()
)
)
.authenticationProviders(authenticationProviders ->// <2>
)
)
.authenticationProviders(
authenticationProviders ->// <2>
// 自定义授权模式提供者(Provider)
authenticationProviders.addAll(
List.of(
new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator),
new CaptchaAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator, redisTemplate),
new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator, redisTemplate, codeGenerator),
new WxMiniAppAuthenticationProvider(authorizationService, tokenGenerator, memberDetailsService, wxMaService),
new SmsCodeAuthenticationProvider(authorizationService, tokenGenerator, memberDetailsService, redisTemplate)
new SmsAuthenticationProvider(authorizationService, tokenGenerator, memberDetailsService, redisTemplate)
)
)
)
.accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定义成功响应
.errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定义失败响应
)
.accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定义成功响应
.errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定义失败响应
);
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http.securityMatcher(endpointsMatcher)
.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
http
// 当用户未登录且尝试访问需要认证的端点时,重定向至登录页面
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint(CUSTOM_LOGIN_PAGE_URI),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// 处理 OIDC 获取用户信息端点
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
......@@ -155,7 +167,7 @@ public class AuthorizationServerConfig {
public JWKSource<SecurityContext> jwkSource() {
// 尝试从Redis中获取JWKSet(JWT密钥对,包含非对称加密的公钥和私钥)
String jwkSetStr = redisTemplate.opsForValue().get(SecurityConstants.JWK_SET_KEY);
String jwkSetStr = redisTemplate.opsForValue().get(RedisConstants.JWK_SET_KEY);
if (StrUtil.isNotBlank(jwkSetStr)) {
// 如果存在,解析JWKSet并返回
JWKSet jwkSet = JWKSet.parse(jwkSetStr);
......@@ -176,7 +188,7 @@ public class AuthorizationServerConfig {
JWKSet jwkSet = new JWKSet(rsaKey);
// 将JWKSet存储在Redis中
redisTemplate.opsForValue().set(SecurityConstants.JWK_SET_KEY, jwkSet.toString(Boolean.FALSE));
redisTemplate.opsForValue().set(RedisConstants.JWK_SET_KEY, jwkSet.toString(Boolean.FALSE));
return new ImmutableJWKSet<>(jwkSet);
}
......@@ -282,7 +294,6 @@ public class AuthorizationServerConfig {
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
......@@ -313,7 +324,6 @@ public class AuthorizationServerConfig {
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.PASSWORD) // 密码模式
.authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) // 验证码模式
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
.scope(OidcScopes.OPENID)
......@@ -348,7 +358,7 @@ public class AuthorizationServerConfig {
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(WxMiniAppAuthenticationToken.WECHAT_MINI_APP) // 微信小程序模式
.authorizationGrantType(SmsCodeAuthenticationToken.SMS_CODE) // 短信验证码模式
.authorizationGrantType(SmsAuthenticationToken.SMS_CODE) // 短信验证码模式
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
.scope(OidcScopes.OPENID)
......
package com.youlai.common.captcha.config;
package com.youlai.auth.config;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import com.youlai.common.enums.CaptchaCodeTypeEnum;
import com.youlai.auth.enums.CaptchaCodeTypeEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......
package com.youlai.common.captcha.config;
package com.youlai.auth.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
......
package com.youlai.auth.config;
import cn.hutool.core.collection.CollectionUtil;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
......@@ -8,13 +11,24 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import java.util.List;
/**
* 默认安全配置
*/
@EnableWebSecurity
@ConfigurationProperties(prefix = "security")
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {
@EnableWebSecurity
public class SecurityConfig {
/**
* 白名单路径列表
*/
@Setter
private List<String> whitelistPaths;
/**
* Spring Security 安全过滤器链配置
......@@ -24,10 +38,17 @@ public class DefaultSecurityConfig {
*/
@Bean
@Order(0)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize.anyRequest().authenticated()
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
http.authorizeHttpRequests((requests) ->
{
if (CollectionUtil.isNotEmpty(whitelistPaths)) {
for (String whitelistPath : whitelistPaths) {
requests.requestMatchers(mvcMatcherBuilder.pattern(whitelistPath)).permitAll();
}
}
requests.anyRequest().authenticated();
}
)
.csrf(AbstractHttpConfigurer::disable)
.formLogin(Customizer.withDefaults());
......
package com.youlai.auth.controller;
import com.youlai.auth.model.CaptchaResult;
import com.youlai.auth.service.AuthService;
import com.youlai.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 认证控制器
* <p>
* 获取验证码、退出登录等接口
* 注:登录接口不在此控制器,在过滤器OAuth2TokenEndpointFilter拦截端点(/oauth2/token)处理
*
* @author haoxr
* @since 3.1.0
*/
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@Operation(summary = "获取验证码")
@GetMapping("/captcha")
public Result<CaptchaResult> getCaptcha() {
CaptchaResult captchaResult = authService.getCaptcha();
return Result.success(captchaResult);
}
@Operation(summary = "注销登出")
@DeleteMapping("/logout")
public Result logout() {
boolean result = authService.logout();
return Result.judge(result);
}
}
package com.youlai.auth.enums;
import com.youlai.common.base.IBaseEnum;
import lombok.Getter;
/**
* 验证码字符类型枚举
*
* @author haoxr
* @since 3.1.0
*/
public enum CaptchaCodeTypeEnum {
MATH,
RANDOM;
}
package com.youlai.gateway.model;
package com.youlai.auth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 验证码响应对象
*
* @author haoxr
* @since 2023/03/24
* @since 3.1.0
*/
@Schema(description ="验证码响应对象")
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CaptchaResult {
@Schema(description = "验证码唯一标识(用于从Redis获取验证码Code)")
/**
* 验证码唯一标识(用于从Redis获取验证码Code)
*/
private String captchaId;
@Schema(description = "验证码图片Base64字符串")
/**
* 验证码图片Base64字符串
*/
private String captchaBase64;
}
package com.youlai.auth.model;
import lombok.Data;
/**
* @author haoxr
* @since 2024/1/30
*/
@Data
public class LoginUserInfo {
private Long id;
}
package com.youlai.auth.authentication.miniapp;
package com.youlai.auth.oauth2.extension.miniapp;
import cn.hutool.core.util.StrUtil;
import com.youlai.auth.util.OAuth2EndpointUtils;
......
package com.youlai.auth.authentication.miniapp;
package com.youlai.auth.oauth2.extension.miniapp;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
......
package com.youlai.auth.authentication.miniapp;
package com.youlai.auth.oauth2.extension.miniapp;
import jakarta.annotation.Nullable;
import org.springframework.security.core.Authentication;
......
package com.youlai.auth.authentication.password;
package com.youlai.auth.oauth2.extension.password;
import cn.hutool.core.util.StrUtil;
import com.youlai.common.constant.OAuth2Constants;
import com.youlai.auth.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
......@@ -78,6 +79,26 @@ public class PasswordAuthenticationConverter implements AuthenticationConverter
);
}
// 验证码ID(必需)
String verifyKey = parameters.getFirst(OAuth2Constants.CAPTCHA_ID);
if (StrUtil.isBlank(verifyKey)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2Constants.CAPTCHA_ID,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
);
}
// 验证码Code(必需)
String verifyCode = parameters.getFirst(OAuth2Constants.CAPTCHA_CODE);
if (StrUtil.isBlank(verifyCode)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2Constants.CAPTCHA_CODE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
);
}
// 附加参数(保存用户名/密码传递给 PasswordAuthenticationProvider 用于身份认证)
Map<String, Object> additionalParameters = parameters
.entrySet()
......
package com.youlai.auth.authentication.password;
package com.youlai.auth.oauth2.extension.password;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.core.lang.Assert;
import com.youlai.common.constant.OAuth2Constants;
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
import com.youlai.common.constant.RedisConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
......@@ -51,6 +57,10 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private final RedisTemplate<String,String> redisTemplate;
private final CodeGenerator codeGenerator;
/**
* Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using the provided parameters.
*
......@@ -61,21 +71,25 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
*/
public PasswordAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
RedisTemplate<String,String> redisTemplate,
CodeGenerator codeGenerator
) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authenticationManager = authenticationManager;
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
this.redisTemplate = redisTemplate;
this.codeGenerator = codeGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PasswordAuthenticationToken resourceOwnerPasswordAuthentication = (PasswordAuthenticationToken) authentication;
PasswordAuthenticationToken passwordAuthenticationToken = (PasswordAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
.getAuthenticatedClientElseThrowInvalidClient(resourceOwnerPasswordAuthentication);
.getAuthenticatedClientElseThrowInvalidClient(passwordAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 验证客户端是否支持授权类型(grant_type=password)
......@@ -83,8 +97,21 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
Map<String, Object> additionalParameters = passwordAuthenticationToken.getAdditionalParameters();
// 验证码校验
String captchaId = (String) additionalParameters.get(OAuth2Constants.CAPTCHA_ID);
String captchaCode = (String) additionalParameters.get(OAuth2Constants.CAPTCHA_CODE);
String cacheCode = redisTemplate.opsForValue().get(RedisConstants.CAPTCHA_CODE_PREFIX + captchaId);
// 验证码比对
if (!codeGenerator.verify(cacheCode, captchaCode)) {
throw new OAuth2AuthenticationException("验证码错误");
}
// 生成用户名密码身份验证令牌
Map<String, Object> additionalParameters = resourceOwnerPasswordAuthentication.getAdditionalParameters();
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
......@@ -101,7 +128,7 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
// 验证申请访问范围(Scope)
Set<String> authorizedScopes = registeredClient.getScopes();
Set<String> requestedScopes = resourceOwnerPasswordAuthentication.getScopes();
Set<String> requestedScopes = passwordAuthenticationToken.getScopes();
if (!CollectionUtils.isEmpty(requestedScopes)) {
Set<String> unauthorizedScopes = requestedScopes.stream()
.filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope))
......@@ -119,7 +146,7 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(AuthorizationGrantType.PASSWORD) // 授权方式
.authorizationGrant(resourceOwnerPasswordAuthentication) // 授权具体对象
.authorizationGrant(passwordAuthenticationToken) // 授权具体对象
;
// 生成访问令牌(Access Token)
......
package com.youlai.auth.authentication.password;
package com.youlai.auth.oauth2.extension.password;
import jakarta.annotation.Nullable;
import org.springframework.security.core.Authentication;
......
package com.youlai.auth.authentication.smscode;
package com.youlai.auth.oauth2.extension.sms;
import cn.hutool.core.util.StrUtil;
import com.youlai.auth.util.OAuth2EndpointUtils;
......@@ -26,13 +26,13 @@ import java.util.stream.Collectors;
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter
* @since 3.0.0
*/
public class SmsCodeAuthenticationConverter implements AuthenticationConverter {
public class SmsAuthenticationConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
// 授权类型 (必需)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!SmsCodeAuthenticationToken.SMS_CODE.getValue().equals(grantType)) {
if (!SmsAuthenticationToken.SMS_CODE.getValue().equals(grantType)) {
return null;
}
......@@ -57,20 +57,20 @@ public class SmsCodeAuthenticationConverter implements AuthenticationConverter {
}
// 手机号(必需)
String mobile = parameters.getFirst(SmsCodeParameterNames.MOBILE);
String mobile = parameters.getFirst(SmsParameterNames.MOBILE);
if (StrUtil.isBlank(mobile)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
SmsCodeParameterNames.MOBILE,
SmsParameterNames.MOBILE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 验证码(必需)
String verifyCode = parameters.getFirst(SmsCodeParameterNames.VERIFY_CODE);
String verifyCode = parameters.getFirst(SmsParameterNames.VERIFY_CODE);
if (StrUtil.isBlank(verifyCode)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
SmsCodeParameterNames.VERIFY_CODE,
SmsParameterNames.VERIFY_CODE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
......@@ -85,7 +85,7 @@ public class SmsCodeAuthenticationConverter implements AuthenticationConverter {
)
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
return new SmsCodeAuthenticationToken(
return new SmsAuthenticationToken(
clientPrincipal,
requestedScopes,
additionalParameters
......
package com.youlai.auth.authentication.smscode;
package com.youlai.auth.oauth2.extension.sms;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.youlai.auth.service.MemberDetailsService;
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.constant.RedisConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
......@@ -34,7 +34,7 @@ import java.util.Map;
* @since 3.0.0
*/
@Slf4j
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
public class SmsAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
......@@ -52,7 +52,7 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public SmsCodeAuthenticationProvider(
public SmsAuthenticationProvider(
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
MemberDetailsService memberDetailsService,
......@@ -72,24 +72,24 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;
SmsAuthenticationToken smsAuthenticationToken = (SmsAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
.getAuthenticatedClientElseThrowInvalidClient(smsCodeAuthenticationToken);
.getAuthenticatedClientElseThrowInvalidClient(smsAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 验证客户端是否支持授权类型(grant_type=wechat_mini_app)
if (!registeredClient.getAuthorizationGrantTypes().contains(SmsCodeAuthenticationToken.SMS_CODE)) {
if (!registeredClient.getAuthorizationGrantTypes().contains(SmsAuthenticationToken.SMS_CODE)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
// 短信验证码校验
Map<String, Object> additionalParameters = smsCodeAuthenticationToken.getAdditionalParameters();
String mobile = (String) additionalParameters.get(SmsCodeParameterNames.MOBILE);
String verifyCode = (String) additionalParameters.get(SmsCodeParameterNames.VERIFY_CODE);
Map<String, Object> additionalParameters = smsAuthenticationToken.getAdditionalParameters();
String mobile = (String) additionalParameters.get(SmsParameterNames.MOBILE);
String verifyCode = (String) additionalParameters.get(SmsParameterNames.VERIFY_CODE);
if (!verifyCode.equals("666666")) { // 666666 是后门,因为短信收费,正式环境删除这个if
String codeKey = SecurityConstants.SMS_CODE_PREFIX + mobile;
String codeKey = RedisConstants.LOGIN_SMS_CODE_PREFIX + mobile;
String cacheCode = (String) redisTemplate.opsForValue().get(codeKey);
if (!StrUtil.equals(verifyCode, cacheCode)) {
......@@ -107,8 +107,8 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(SmsCodeAuthenticationToken.SMS_CODE)
.authorizationGrant(smsCodeAuthenticationToken);
.authorizationGrantType(SmsAuthenticationToken.SMS_CODE)
.authorizationGrant(smsAuthenticationToken);
// 生成访问令牌(Access Token)
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
......@@ -124,7 +124,7 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(userDetails.getUsername())
.authorizationGrantType(SmsCodeAuthenticationToken.SMS_CODE)
.authorizationGrantType(SmsAuthenticationToken.SMS_CODE)
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
......@@ -159,7 +159,7 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
package com.youlai.auth.authentication.smscode;
package com.youlai.auth.oauth2.extension.sms;
import jakarta.annotation.Nullable;
import org.springframework.security.core.Authentication;
......@@ -17,7 +17,7 @@ import java.util.Set;
* @author haoxr
* @since 3.0.0
*/
public class SmsCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
public class SmsAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
/**
* 令牌申请访问范围
......@@ -27,15 +27,15 @@ public class SmsCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenti
/**
* 授权类型(短信验证码: sms_code)
*/
public static final AuthorizationGrantType SMS_CODE = new AuthorizationGrantType("sms_code");
public static final AuthorizationGrantType SMS_CODE = new AuthorizationGrantType("sms");
protected SmsCodeAuthenticationToken(
protected SmsAuthenticationToken(
Authentication clientPrincipal,
Set<String> scopes,
@Nullable Map<String, Object> additionalParameters
) {
super(SmsCodeAuthenticationToken.SMS_CODE, clientPrincipal, additionalParameters);
super(SmsAuthenticationToken.SMS_CODE, clientPrincipal, additionalParameters);
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}
......
......@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.youlai.auth.authentication.smscode;
package com.youlai.auth.oauth2.extension.sms;
/**
* 短信验证码模式参数名称常量
......@@ -22,7 +22,7 @@ package com.youlai.auth.authentication.smscode;
* @author haoxr
* @since 3.0.0
*/
public final class SmsCodeParameterNames {
public final class SmsParameterNames {
/**
* 手机号
......@@ -35,7 +35,7 @@ public final class SmsCodeParameterNames {
public static final String VERIFY_CODE = "verifyCode";
private SmsCodeParameterNames() {
private SmsParameterNames() {
}
}
package com.youlai.auth.handler;
package com.youlai.auth.oauth2.handler;
import com.youlai.common.result.Result;
import jakarta.servlet.ServletException;
......
package com.youlai.auth.handler;
package com.youlai.auth.oauth2.handler;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.result.Result;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
......@@ -74,7 +73,7 @@ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHand
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
String clientId = accessTokenAuthentication.getRegisteredClient().getClientId();
if (SecurityConstants.TEST_OAUTH2_CLIENT_ID.equals(clientId)) {
if ("client".equals(clientId)) {
// Knife4j测试客户端ID(Knife4j自动填充的 access_token 须原生返回,不能被包装成业务码数据格式)
this.accessTokenHttpResponseConverter.write(tokenResponseParameters, null, httpResponse);
} else {
......
package com.youlai.auth.jackson;
package com.youlai.auth.oauth2.jackson;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
......
package com.youlai.auth.jackson;
package com.youlai.auth.oauth2.jackson;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
......
package com.youlai.auth.config;
package com.youlai.auth.oauth2.token;
import com.youlai.auth.model.MemberDetails;
import com.youlai.auth.model.SysUserDetails;
import com.youlai.common.constant.JwtClaimConstants;
import com.youlai.common.constant.SecurityConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
......@@ -18,7 +14,6 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
......@@ -29,7 +24,7 @@ import java.util.stream.Collectors;
* @since 3.0.0
*/
@Configuration
public class JwtTokenClaimsConfig {
public class JwtTokenConfig {
/**
* JWT 自定义字段
......
package com.youlai.auth.service;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.core.util.IdUtil;
import com.youlai.auth.config.CaptchaProperties;
import com.youlai.auth.model.CaptchaResult;
import com.youlai.auth.util.SecurityUtils;
import com.youlai.common.constant.RedisConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* 认证服务
*
* @author Ray Hao
* @since 3.1.0
*/
@Service
@RequiredArgsConstructor
public class AuthService {
private final CaptchaService captchaService;
private final RedisTemplate<String, String> redisTemplate;
private final CaptchaProperties captchaProperties;
/**
* 获取图形验证码
*
* @return Result<CaptchaResult>
*/
public CaptchaResult getCaptcha() {
AbstractCaptcha captcha = captchaService.generate();
// 验证码文本缓存至Redis,用于登录校验
String captchaId = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(
RedisConstants.CAPTCHA_CODE_PREFIX + captchaId,
captcha.getCode(),
captchaProperties.getExpireSeconds(),
TimeUnit.SECONDS
);
CaptchaResult captchaResult = CaptchaResult.builder()
.captchaId(captchaId)
.captchaBase64(captcha.getImageBase64Data())
.build();
return captchaResult;
}
/**
* 注销登出
*
* @return Result
*/
public boolean logout() {
String jti = SecurityUtils.getJti();
Optional<Long> expireTimeOpt = Optional.ofNullable(SecurityUtils.getExp());
long currentTimeInSeconds = System.currentTimeMillis() / 1000; // 当前时间(单位:秒)
expireTimeOpt.ifPresent(expireTime -> {
if (expireTime > currentTimeInSeconds) {
// token未过期,添加至缓存作为黑名单,缓存时间为token剩余的有效时间
long remainingTimeInSeconds = expireTime - currentTimeInSeconds;
redisTemplate.opsForValue().set(RedisConstants.TOKEN_BLACKLIST_PREFIX + jti, "", remainingTimeInSeconds, TimeUnit.SECONDS);
}
});
if (expireTimeOpt.isEmpty()) {
// token 永不过期则永久加入黑名单
redisTemplate.opsForValue().set(RedisConstants.TOKEN_BLACKLIST_PREFIX + jti, "");
}
return true;
}
}
package com.youlai.common.captcha.component;
package com.youlai.auth.service;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.generator.CodeGenerator;
import com.youlai.common.captcha.config.CaptchaProperties;
import com.youlai.common.captcha.enums.CaptchaTypeEnum;
import com.youlai.auth.config.CaptchaProperties;
import com.youlai.auth.enums.CaptchaTypeEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
......@@ -18,12 +18,13 @@ import java.awt.*;
*/
@Component
@RequiredArgsConstructor
public class CaptchaGenerator {
public class CaptchaService {
private final CodeGenerator codeGenerator;
private final Font captchaFont;
private final CaptchaProperties captchaProperties;
/**
* 生成图形验证码
*/
......
package com.youlai.auth.service;
import cn.hutool.core.lang.Assert;
import com.youlai.auth.model.LoginUserInfo;
import com.youlai.auth.model.SysUserDetails;
import com.youlai.common.enums.StatusEnum;
import com.youlai.system.api.UserFeignClient;
......@@ -44,4 +45,10 @@ public class SysUserDetailsService implements UserDetailsService {
return new SysUserDetails(userAuthInfo);
}
public LoginUserInfo getLoginUserInfo() {
LoginUserInfo loginUserInfo = new LoginUserInfo();
loginUserInfo.setId(123L);
return loginUserInfo;
}
}
......@@ -15,9 +15,11 @@ import java.util.HashMap;
import java.util.Map;
/**
* @author haoxr
* OAuth2 Endpoint 工具类
*
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2EndpointUtils
* @since 2023/6/8
* @author Ray Hao
* @since 3.0.0
*/
public class OAuth2EndpointUtils {
......
package com.youlai.auth.util;
import cn.hutool.core.convert.Convert;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import java.util.Map;
/**
* Spring Security 工具类
*
* @since 3.1.0
* @author Ray Hao
*/
public class SecurityUtils {
public static Map<String, Object> getTokenAttributes() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
return jwtAuthenticationToken.getTokenAttributes();
}
public static String getJti() {
return String.valueOf(getTokenAttributes().get("jti"));
}
public static Long getExp() {
return Convert.toLong(getTokenAttributes().get("exp"));
}
}
package com.youlai.auth.authentication;
package com.youlai.auth.oauth2;
import com.youlai.auth.authentication.captcha.CaptchaParameterNames;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
......
package com.youlai.auth.authentication;
package com.youlai.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
......
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.youlai</groupId>
<artifactId>youlai-common</artifactId>
<version>3.0.1</version>
</parent>
<artifactId>common-captcha</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-core</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
com.youlai.common.captcha.config.CaptchaConfig
com.youlai.common.captcha.config.CaptchaProperties
com.youlai.common.captcha.component.CaptchaGenerator
......@@ -9,35 +9,22 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-base</artifactId>
<artifactId>common-core</artifactId>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<!-- Mybatis 分页参数 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
</dependencies>
</project>
package com.youlai.common.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
......@@ -19,12 +17,8 @@ public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@TableField(fill = FieldFill.INSERT)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}
package com.youlai.common.constant;
/**
* OAuth2 常量
*
* @author haoxr
* @since 3.1.0
*/
public interface OAuth2Constants {
/**
* 验证码唯一标识, 用于从Redis获取验证码Code和输入的验证码进行比对
*/
String CAPTCHA_ID = "captchaId";
/**
* 验证码 Code
*/
String CAPTCHA_CODE = "captchaCode";
}
......@@ -27,7 +27,7 @@ public enum ResultCode implements IResultCode, Serializable {
USERNAME_OR_PASSWORD_ERROR("A0210", "用户名或密码错误"),
PASSWORD_ENTER_EXCEED_LIMIT("A0211", "用户输入密码次数超限"),
CLIENT_AUTHENTICATION_FAILED("A0212", "客户端认证失败"),
INVALID_TOKEN("A0230", "token无效或已过期"),
TOKEN_INVALID("A0230", "token无效或已过期"),
TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"),
AUTHORIZED_ERROR("A0300", "访问权限异常"),
......
package com.youlai.common.enums;
package com.youlai.common.mybatis.enums;
import com.youlai.common.base.IBaseEnum;
import lombok.Getter;
......
......@@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.youlai.common.base.IBaseEnum;
import com.youlai.common.enums.DataScopeEnum;
import com.youlai.common.mybatis.enums.DataScopeEnum;
import com.youlai.common.mybatis.annotation.DataPermission;
import com.youlai.common.security.util.SecurityUtils;
import lombok.SneakyThrows;
......
......@@ -9,7 +9,7 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-resourceserver</artifactId>
<artifactId>common-security</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
......
......@@ -3,7 +3,6 @@ package com.youlai.common.security.config;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONUtil;
import com.youlai.common.constant.JwtClaimConstants;
import com.youlai.common.constant.SecurityConstants;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
......@@ -11,7 +10,6 @@ import org.apache.logging.log4j.util.Strings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
......@@ -27,10 +25,8 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import java.util.Arrays;
import java.util.List;
/**
......
......@@ -12,7 +12,7 @@ import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* token 无效自定义异常
* 自定义 token 无效异常
*
* @author haoxr
* @date 2022/11/13
......@@ -31,7 +31,7 @@ public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
mapper.writeValue(response.getOutputStream(), Result.failed(ResultCode.RESOURCE_NOT_FOUND));
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
mapper.writeValue(response.getOutputStream(), Result.failed(ResultCode.INVALID_TOKEN));
mapper.writeValue(response.getOutputStream(), Result.failed(ResultCode.TOKEN_INVALID));
}
}
......
......@@ -2,7 +2,7 @@ package com.youlai.common.security.service;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.constant.RedisConstants;
import com.youlai.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
......@@ -81,7 +81,7 @@ public class PermissionService {
Set<String> perms = new HashSet<>();
// 从缓存中一次性获取所有角色的权限
Collection<Object> roleCodesAsObjects = new ArrayList<>(roleCodes);
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(SecurityConstants.ROLE_PERMS_PREFIX, roleCodesAsObjects);
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.ROLE_PERMS_PREFIX, roleCodesAsObjects);
for (Object rolePermsObj : rolePermsList) {
if (rolePermsObj instanceof Set) {
......
package com.youlai.common.security.util;
import cn.hutool.core.convert.Convert;
import com.youlai.common.constant.GlobalConstants;
import com.youlai.common.constant.SystemConstants;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
......@@ -14,6 +14,9 @@ import java.util.stream.Collectors;
/**
* Spring Security 工具类
*
* @author Ray Hao
* @since 2.1.0
*/
public class SecurityUtils {
......@@ -52,7 +55,7 @@ public class SecurityUtils {
}
public static boolean isRoot() {
return getRoles().contains(GlobalConstants.ROOT_ROLE_CODE);
return getRoles().contains(SystemConstants.ROOT_ROLE_CODE);
}
public static String getJti() {
......@@ -67,7 +70,8 @@ public class SecurityUtils {
/**
* 获取数据权限范围
*
* @return
* @return 数据权限范围
* @see com.youlai.common.mybatis.enums.DataScopeEnum
*/
public static Integer getDataScope() {
return Convert.toInt(getTokenAttributes().get("dataScope"));
......
......@@ -23,6 +23,5 @@
<module>common-log</module>
<module>common-security</module>
<module>common-seata</module>
<module>common-captcha</module>
</modules>
</project>
......@@ -96,10 +96,6 @@
<artifactId>common-redis</artifactId>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-captcha</artifactId>
</dependency>
</dependencies>
<build>
......
......@@ -10,24 +10,20 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
import java.util.List;
/**
* Spring Security 配置
* 客户端配置
*
* @author haoxr
* @since 2022/8/28
*/
@ConfigurationProperties(prefix = "security")
@Configuration
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
@Slf4j
public class OAuth2ClientConfig {
public class SecurityConfig {
/**
* 黑名单请求路径列表
......@@ -35,6 +31,7 @@ public class OAuth2ClientConfig {
@Setter
private List<String> blacklistPaths;
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
http
......@@ -50,28 +47,7 @@ public class OAuth2ClientConfig {
return http.build();
}
/**
* 跨域共享配置
*
* @return CorsConfigurationSource
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
CorsConfiguration corsConfig = new CorsConfiguration();
// 允许所有请求方法
corsConfig.addAllowedMethod("*");
// 允许所有域,当请求头
corsConfig.addAllowedOriginPattern("*");
// 允许全部请求头
corsConfig.addAllowedHeader("*");
// 允许携带 Authorization 头
corsConfig.setAllowCredentials(true);
// 允许全部请求路径
source.registerCorsConfiguration("/**", corsConfig);
return source;
}
}
\ No newline at end of file
package com.youlai.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.youlai.common.result.ResultCode;
import jakarta.annotation.PostConstruct;
......
package com.youlai.gateway.filter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWTPayload;
import com.nimbusds.jose.JWSObject;
import com.youlai.common.constant.RedisConstants;
import com.youlai.common.result.ResultCode;
import com.youlai.gateway.util.WebFluxUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.text.ParseException;
/**
* Token 验证全局过滤器
*
* @author Ray Hao
* @since 3.1.0
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class TokenValidationGlobalFilter implements GlobalFilter, Ordered {
private final RedisTemplate<String, Object> redisTemplate;
private static final String BEARER_PREFIX = "Bearer ";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StrUtil.isBlank(authorization) || !StrUtil.startWithIgnoreCase(authorization, BEARER_PREFIX)) {
return chain.filter(exchange);
}
try {
String token = authorization.substring(BEARER_PREFIX.length());
JWSObject jwsObject = JWSObject.parse(token);
String jti = JSONUtil.parseObj(jwsObject.getPayload()).get(JWTPayload.JWT_ID, String.class);
Boolean isBlackToken = redisTemplate.hasKey(RedisConstants.TOKEN_BLACKLIST_PREFIX + jti);
if (Boolean.TRUE.equals(isBlackToken)) {
return WebFluxUtils.writeErrorResponse(response, ResultCode.TOKEN_ACCESS_FORBIDDEN);
}
} catch (ParseException e) {
log.error("Parsing token failed in TokenValidationGlobalFilter", e);
return WebFluxUtils.writeErrorResponse(response, ResultCode.TOKEN_INVALID);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
package com.youlai.gateway.handler;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.result.Result;
import com.youlai.gateway.config.CaptchaProperties;
import com.youlai.gateway.enums.CaptchaTypeEnum;
import com.youlai.gateway.model.CaptchaResult;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.awt.*;
import java.util.concurrent.TimeUnit;
/**
* 验证码处理器
*
* @author haoxr
* @since 2.4.1
*/
@Component
@RequiredArgsConstructor
public class CaptchaHandler implements HandlerFunction<ServerResponse> {
private final StringRedisTemplate redisTemplate;
private final CodeGenerator codeGenerator;
private final Font captchaFont;
private final CaptchaProperties captchaProperties;
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth();
int height = captchaProperties.getHeight();
int interfereCount = captchaProperties.getInterfereCount();
int codeLength = captchaProperties.getCode().getLength();
AbstractCaptcha captcha;
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
} else {
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
}
captcha.setGenerator(codeGenerator);
captcha.setTextAlpha(captchaProperties.getTextAlpha());
captcha.setFont(captchaFont);
String captchaCode = captcha.getCode();
String imageBase64Data = captcha.getImageBase64Data();
// 验证码文本缓存至Redis,用于登录校验
String captchaId = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaId, captchaCode,
captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
CaptchaResult captchaResult = CaptchaResult.builder()
.captchaId(captchaId)
.captchaBase64(imageBase64Data)
.build();
return ServerResponse.ok().body(BodyInserters.fromValue(Result.success(captchaResult)));
}
}
package com.youlai.gateway.router;
import com.youlai.gateway.handler.CaptchaHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
/**
* 验证码路由
*
* @author haoxr
* @since 2.4.1
*/
@Configuration
@RequiredArgsConstructor
public class CaptchaRouter {
private final CaptchaHandler captchaHandler;
@Bean
public RouterFunction<ServerResponse> captchaRouterFunction() {
return RouterFunctions
.route(RequestPredicates.GET("/captcha")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), captchaHandler);
}
}
package com.youlai.gateway.util;
import cn.hutool.json.JSONUtil;
import com.youlai.common.result.Result;
import com.youlai.common.result.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* WebFlux 响应处理器
*
* @author Ray Hao
* @since 3.1.0
*/
@Slf4j
public class WebFluxUtils {
public static Mono<Void> writeErrorResponse(ServerHttpResponse response, ResultCode resultCode) {
HttpStatus status = determineHttpStatus(resultCode);
response.setStatusCode(status);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.getHeaders().setAccessControlAllowOrigin("*");
response.getHeaders().setCacheControl("no-cache");
String responseBody = JSONUtil.toJsonStr(Result.failed(resultCode));
DataBuffer buffer = response.bufferFactory().wrap(responseBody.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer))
.doOnError(error -> {
DataBufferUtils.release(buffer);
log.error("Error writing response: {}", error.getMessage());
});
}
private static HttpStatus determineHttpStatus(ResultCode resultCode) {
return switch (resultCode) {
case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED;
case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN;
default -> HttpStatus.BAD_REQUEST;
};
}
}
......@@ -2,7 +2,7 @@ package com.youlai.system.model.bo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.youlai.common.enums.MenuTypeEnum;
import com.youlai.system.enums.MenuTypeEnum;
import lombok.Data;
import java.util.List;
......
......@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.youlai.common.base.BaseEntity;
import com.youlai.common.enums.MenuTypeEnum;
import com.youlai.system.enums.MenuTypeEnum;
import lombok.Data;
/**
......
package com.youlai.system.model.form;
import com.youlai.common.enums.MenuTypeEnum;
import com.youlai.system.enums.MenuTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
......
package com.youlai.system.model.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.youlai.common.enums.MenuTypeEnum;
import com.youlai.system.enums.MenuTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
......
......@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.constant.SystemConstants;
import com.youlai.common.enums.MenuTypeEnum;
import com.youlai.system.enums.MenuTypeEnum;
import com.youlai.common.enums.StatusEnum;
import com.youlai.system.converter.MenuConverter;
import com.youlai.system.mapper.SysMenuMapper;
......
......@@ -42,7 +42,7 @@
LEFT JOIN sys_role_menu t2 ON t1.id = t2.menu_id
LEFT JOIN sys_role t3 ON t2.role_id = t3.id
WHERE
t1.type != '${@com.youlai.common.enums.MenuTypeEnum@BUTTON.getValue()}'
t1.type != '${@com.youlai.system.enums.MenuTypeEnum@BUTTON.getValue()}'
ORDER BY t1.sort asc
</select>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册