mirror of
https://gitee.com/youlaitech/youlai-mall.git
synced 2024-12-22 12:48:59 +08:00
refactor: 项目升级重构
This commit is contained in:
parent
f76909ef35
commit
a4fd813962
21
README.md
21
README.md
@ -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接口
|
||||
|
Binary file not shown.
@ -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()) {
|
||||
|
8
pom.xml
8
pom.xml
@ -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>
|
||||
|
@ -1,115 +0,0 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
|
||||
|
||||
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 {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)) // 自定义授权页
|
||||
.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
|
||||
|
||||
authorizationServerConfigurer
|
||||
.oidc(Customizer.withDefaults())
|
||||
.tokenEndpoint(tokenEndpoint ->
|
||||
tokenEndpoint
|
||||
.accessTokenRequestConverters(
|
||||
authenticationConverters ->// <1>
|
||||
// 自定义授权模式转换器(Converter)
|
||||
authenticationConverters.addAll(
|
||||
List.of(
|
||||
new PasswordAuthenticationConverter(),
|
||||
new CaptchaAuthenticationConverter(),
|
||||
new WxMiniAppAuthenticationConverter(),
|
||||
new SmsCodeAuthenticationConverter()
|
||||
)
|
||||
// 自定义授权模式转换器(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)
|
||||
|
@ -1,9 +1,9 @@
|
||||
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;
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.common.captcha.config;
|
||||
package com.youlai.auth.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
@ -1,38 +0,0 @@
|
||||
package com.youlai.auth.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 默认安全配置
|
||||
*/
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class DefaultSecurityConfig {
|
||||
|
||||
/**
|
||||
* Spring Security 安全过滤器链配置
|
||||
*
|
||||
* @param http 安全配置
|
||||
* @return 安全过滤器链
|
||||
*/
|
||||
@Bean
|
||||
@Order(0)
|
||||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(authorize ->
|
||||
authorize.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.formLogin(Customizer.withDefaults());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
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;
|
||||
import org.springframework.security.config.Customizer;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 默认安全配置
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "security")
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
* 白名单路径列表
|
||||
*/
|
||||
@Setter
|
||||
private List<String> whitelistPaths;
|
||||
|
||||
/**
|
||||
* Spring Security 安全过滤器链配置
|
||||
*
|
||||
* @param http 安全配置
|
||||
* @return 安全过滤器链
|
||||
*/
|
||||
@Bean
|
||||
@Order(0)
|
||||
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());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
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;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.common.captcha.enums;
|
||||
package com.youlai.auth.enums;
|
||||
|
||||
/**
|
||||
* EasyCaptcha 验证码类型枚举
|
@ -0,0 +1,26 @@
|
||||
package com.youlai.auth.model;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 验证码响应对象
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
public class CaptchaResult {
|
||||
|
||||
/**
|
||||
* 验证码唯一标识(用于从Redis获取验证码Code)
|
||||
*/
|
||||
private String captchaId;
|
||||
|
||||
/**
|
||||
* 验证码图片Base64字符串
|
||||
*/
|
||||
private String captchaBase64;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.youlai.auth.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author haoxr
|
||||
* @since 2024/1/30
|
||||
*/
|
||||
@Data
|
||||
public class LoginUserInfo {
|
||||
|
||||
private Long id;
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
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;
|
@ -1,4 +1,4 @@
|
||||
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;
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.authentication.miniapp;
|
||||
package com.youlai.auth.oauth2.extension.miniapp;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
@ -1,6 +1,7 @@
|
||||
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()
|
@ -1,9 +1,15 @@
|
||||
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)
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.authentication.password;
|
||||
package com.youlai.auth.oauth2.extension.password;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
@ -1,4 +1,4 @@
|
||||
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
|
@ -1,10 +1,10 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
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() {
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.handler;
|
||||
package com.youlai.auth.oauth2.handler;
|
||||
|
||||
import com.youlai.common.result.Result;
|
||||
import jakarta.servlet.ServletException;
|
@ -1,6 +1,5 @@
|
||||
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 {
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.jackson;
|
||||
package com.youlai.auth.oauth2.jackson;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.jackson;
|
||||
package com.youlai.auth.oauth2.jackson;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
@ -1,15 +1,11 @@
|
||||
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 自定义字段
|
@ -0,0 +1,84 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* 生成图形验证码
|
||||
*/
|
@ -1,6 +1,7 @@
|
||||
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 {
|
||||
|
||||
|
@ -0,0 +1,36 @@
|
||||
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"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
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;
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.authentication;
|
||||
package com.youlai.auth.oauth2;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.authentication;
|
||||
package com.youlai.auth.oauth2;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.authentication;
|
||||
package com.youlai.auth.oauth2;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
@ -1,43 +0,0 @@
|
||||
<?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>
|
@ -1,4 +0,0 @@
|
||||
com.youlai.common.captcha.config.CaptchaConfig
|
||||
com.youlai.common.captcha.config.CaptchaProperties
|
||||
com.youlai.common.captcha.component.CaptchaGenerator
|
||||
|
@ -9,33 +9,20 @@
|
||||
</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>
|
||||
</dependency>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Mybatis 分页参数 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-redis</artifactId>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
@ -1,8 +1,6 @@
|
||||
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;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
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", "访问权限异常"),
|
@ -1,4 +1,4 @@
|
||||
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) {
|
@ -1,7 +1,7 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
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;
|
||||
|
@ -0,0 +1,74 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
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)));
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package com.youlai.gateway.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
|
||||
*/
|
||||
@Schema(description ="验证码响应对象")
|
||||
@Builder
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class CaptchaResult {
|
||||
|
||||
@Schema(description = "验证码唯一标识(用于从Redis获取验证码Code)")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "验证码图片Base64字符串")
|
||||
private String captchaBase64;
|
||||
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
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;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user