mirror of
https://gitee.com/youlaitech/youlai-mall.git
synced 2024-12-22 12:48:59 +08:00
feat: 新增SAS密码、验证码、短信验证码和微信小程序授权模式
This commit is contained in:
parent
37a90229c5
commit
ec40473835
Binary file not shown.
@ -11,11 +11,11 @@ spring:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: http://f.youlai.tech:8848
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
file-extension: yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
shared-configs[0]:
|
||||
data-id: youlai-common.yaml
|
||||
refresh: true
|
||||
|
@ -11,11 +11,11 @@ spring:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: http://f.youlai.tech:8848
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
file-extension: yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
# 公共配置
|
||||
shared-configs[0]:
|
||||
data-id: youlai-common.yaml
|
||||
|
@ -12,12 +12,12 @@ spring:
|
||||
# 注册中心
|
||||
discovery:
|
||||
server-addr: http://f.youlai.tech:8848
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
# 配置中心
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
file-extension: yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
shared-configs[0]:
|
||||
data-id: youlai-common.yaml
|
||||
refresh: true
|
@ -11,11 +11,11 @@ spring:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: http://f.youlai.tech:8848
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
file-extension: yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
shared-configs[0]:
|
||||
data-id: youlai-common.yaml
|
||||
refresh: true
|
||||
|
20
pom.xml
20
pom.xml
@ -35,14 +35,14 @@
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
|
||||
<spring-boot.version>3.1.0</spring-boot.version>
|
||||
<spring-boot.version>3.1.1</spring-boot.version>
|
||||
|
||||
<!-- spring cloud & alibaba -->
|
||||
<spring-cloud.version>2022.0.3</spring-cloud.version>
|
||||
<spring-cloud.version>2022.0.2</spring-cloud.version>
|
||||
<spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version>
|
||||
<!-- spring authorization server -->
|
||||
<authorization-server.version>1.1.0</authorization-server.version>
|
||||
<nimbus-jose-jwt.version>9.16.1</nimbus-jose-jwt.version>
|
||||
<spring-authorization-server.version>1.1.0</spring-authorization-server.version>
|
||||
<nimbus-jose-jwt.version>9.31</nimbus-jose-jwt.version>
|
||||
|
||||
<!-- db && orm -->
|
||||
<mysql.version>8.0.28</mysql.version>
|
||||
@ -60,7 +60,7 @@
|
||||
<easyexcel.version>3.0.5</easyexcel.version>
|
||||
<easy-captcha.version>1.6.2</easy-captcha.version>
|
||||
<nimbus-jose-jwt.version>9.16.1</nimbus-jose-jwt.version>
|
||||
<thumbnailator.version>0.4.17</thumbnailator.version>
|
||||
<thumbnailator.version>0.4.19</thumbnailator.version>
|
||||
|
||||
<!-- 阿里云短信 -->
|
||||
<aliyun.java.sdk.core.version>4.5.25</aliyun.java.sdk.core.version>
|
||||
@ -69,7 +69,7 @@
|
||||
<!-- minio -->
|
||||
<minio.version>8.5.3</minio.version>
|
||||
<okhttp3.version>4.8.1</okhttp3.version>
|
||||
|
||||
<!-- aliyun oss sdk -->
|
||||
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version>
|
||||
|
||||
<!-- redisson 分布式锁 -->
|
||||
@ -302,16 +302,10 @@
|
||||
<version>${nimbus-jose-jwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
<version>${thumbnailator.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-authorization-server</artifactId>
|
||||
<version>${authorization-server.version}</version>
|
||||
<version>${spring-authorization-server.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
@ -12,6 +12,11 @@
|
||||
<artifactId>youlai-auth</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!--Spring Cloud & Alibaba -->
|
||||
<dependency>
|
||||
@ -88,11 +93,6 @@
|
||||
<artifactId>common-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -0,0 +1,115 @@
|
||||
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.VERIFY_CODE);
|
||||
if (StrUtil.isBlank(verifyCode)) {
|
||||
OAuth2EndpointUtils.throwError(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
CaptchaParameterNames.VERIFY_CODE,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
|
||||
);
|
||||
}
|
||||
|
||||
// 验证码缓存Key(必需)
|
||||
String verifyCodeKey = parameters.getFirst(CaptchaParameterNames.VERIFY_CODE_KEY);
|
||||
if (StrUtil.isBlank(verifyCodeKey)) {
|
||||
OAuth2EndpointUtils.throwError(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
CaptchaParameterNames.VERIFY_CODE_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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
package com.youlai.auth.authentication.captcha;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.auth.authentication.smscode.SmsCodeParameterNames;
|
||||
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
|
||||
import com.youlai.common.constant.SecurityConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
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.*;
|
||||
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 RedisTemplate 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,
|
||||
RedisTemplate 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.VERIFY_CODE);
|
||||
String verifyCodeKey = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE_KEY);
|
||||
|
||||
String cacheCode = (String) redisTemplate.opsForValue().get(verifyCodeKey);
|
||||
if (!StrUtil.equals(verifyCode, cacheCode)) {
|
||||
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 = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
|
||||
|
||||
// 验证申请访问范围(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())
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.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)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.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);
|
||||
}
|
||||
|
||||
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 = Collections.emptyMap();
|
||||
if (idToken != null) {
|
||||
additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
|
||||
}
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return CaptchaAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
|
||||
|
||||
package com.youlai.auth.authentication.captcha;
|
||||
|
||||
/**
|
||||
* 验证码模式请求参数名称常量
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public final class CaptchaParameterNames {
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
public static final String VERIFY_CODE = "verifyCode";
|
||||
|
||||
/**
|
||||
* 验证码缓存Key
|
||||
*/
|
||||
public static final String VERIFY_CODE_KEY = "verifyCodeKey";
|
||||
|
||||
|
||||
private CaptchaParameterNames() {
|
||||
}
|
||||
|
||||
}
|
@ -20,7 +20,7 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 密码认证参数解析器
|
||||
* 密码模式参数解析器
|
||||
* <p>
|
||||
* 解析请求参数中的用户名和密码,并构建相应的身份验证(Authentication)对象
|
||||
*
|
||||
@ -28,7 +28,7 @@ import java.util.stream.Collectors;
|
||||
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class ResourceOwnerPasswordAuthenticationConverter implements AuthenticationConverter {
|
||||
public class PasswordAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
@ -78,7 +78,7 @@ public class ResourceOwnerPasswordAuthenticationConverter implements Authenticat
|
||||
);
|
||||
}
|
||||
|
||||
// 附加参数(保存用户名/密码传递给 ResourceOwnerPasswordAuthenticationProvider 用于身份认证)
|
||||
// 附加参数(保存用户名/密码传递给 PasswordAuthenticationProvider 用于身份认证)
|
||||
Map<String, Object> additionalParameters = parameters
|
||||
.entrySet()
|
||||
.stream()
|
||||
@ -86,7 +86,7 @@ public class ResourceOwnerPasswordAuthenticationConverter implements Authenticat
|
||||
!e.getKey().equals(OAuth2ParameterNames.SCOPE)
|
||||
).collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
||||
|
||||
return new ResourceOwnerPasswordAuthenticationToken(
|
||||
return new PasswordAuthenticationToken(
|
||||
clientPrincipal,
|
||||
requestedScopes,
|
||||
additionalParameters
|
@ -1,5 +1,6 @@
|
||||
package com.youlai.auth.authentication.password;
|
||||
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -36,11 +37,10 @@ import java.util.stream.Collectors;
|
||||
* 处理基于用户名和密码的身份验证
|
||||
*
|
||||
* @author haoxr
|
||||
* @see org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class ResourceOwnerPasswordAuthenticationProvider implements AuthenticationProvider {
|
||||
public class PasswordAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
@ -58,7 +58,7 @@ public class ResourceOwnerPasswordAuthenticationProvider implements Authenticati
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager,
|
||||
public PasswordAuthenticationProvider(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator
|
||||
) {
|
||||
@ -72,7 +72,7 @@ public class ResourceOwnerPasswordAuthenticationProvider implements Authenticati
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
|
||||
ResourceOwnerPasswordAuthenticationToken resourceOwnerPasswordAuthentication = (ResourceOwnerPasswordAuthenticationToken) authentication;
|
||||
PasswordAuthenticationToken resourceOwnerPasswordAuthentication = (PasswordAuthenticationToken) authentication;
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
|
||||
.getAuthenticatedClientElseThrowInvalidClient(resourceOwnerPasswordAuthentication);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
@ -138,7 +138,7 @@ public class ResourceOwnerPasswordAuthenticationProvider implements Authenticati
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
// 生成刷新令牌(Refresh token)
|
||||
// 生成刷新令牌(Refresh Token)
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||
// Do not issue refresh token to public client
|
||||
@ -194,7 +194,7 @@ public class ResourceOwnerPasswordAuthenticationProvider implements Authenticati
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
return PasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
}
|
@ -12,10 +12,12 @@ import java.util.*;
|
||||
* 密码授权模式身份验证令牌(包含用户名和密码等)
|
||||
*
|
||||
* @author haoxr
|
||||
* @see org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class ResourceOwnerPasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
public class PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");
|
||||
|
||||
|
||||
/**
|
||||
* 令牌申请访问范围
|
||||
@ -29,12 +31,12 @@ public class ResourceOwnerPasswordAuthenticationToken extends OAuth2Authorizatio
|
||||
* @param scopes 令牌申请访问范围
|
||||
* @param additionalParameters 自定义额外参数(用户名和密码)
|
||||
*/
|
||||
public ResourceOwnerPasswordAuthenticationToken(
|
||||
public PasswordAuthenticationToken(
|
||||
Authentication clientPrincipal,
|
||||
Set<String> scopes,
|
||||
@Nullable Map<String, Object> additionalParameters
|
||||
) {
|
||||
super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
|
||||
super(PASSWORD, clientPrincipal, additionalParameters);
|
||||
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
|
||||
|
||||
}
|
@ -57,20 +57,20 @@ public class SmsCodeAuthenticationConverter implements AuthenticationConverter {
|
||||
}
|
||||
|
||||
// 手机号(必需)
|
||||
String mobile = parameters.getFirst("mobile");
|
||||
String mobile = parameters.getFirst(SmsCodeParameterNames.MOBILE);
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
OAuth2EndpointUtils.throwError(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"mobile",
|
||||
SmsCodeParameterNames.MOBILE,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
|
||||
// 验证码(必需)
|
||||
String verifyCode = parameters.getFirst("verifyCode");
|
||||
String verifyCode = parameters.getFirst(SmsCodeParameterNames.VERIFY_CODE);
|
||||
if (StrUtil.isBlank(verifyCode)) {
|
||||
OAuth2EndpointUtils.throwError(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"verifyCode",
|
||||
SmsCodeParameterNames.VERIFY_CODE,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ package com.youlai.auth.authentication.smscode;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.auth.userdetails.member.MobileUserDetailsService;
|
||||
import com.youlai.auth.userdetails.member.MemberDetailsService;
|
||||
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
|
||||
import com.youlai.common.constant.SecurityConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -41,7 +41,7 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
private final MobileUserDetailsService mobileUserDetailsService;
|
||||
private final MemberDetailsService memberDetailsService;
|
||||
|
||||
private final RedisTemplate redisTemplate;
|
||||
|
||||
@ -55,17 +55,17 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
public SmsCodeAuthenticationProvider(
|
||||
OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
|
||||
MobileUserDetailsService mobileUserDetailsService,
|
||||
MemberDetailsService memberDetailsService,
|
||||
RedisTemplate redisTemplate
|
||||
|
||||
) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
Assert.notNull(mobileUserDetailsService, "userDetailsService cannot be null");
|
||||
Assert.notNull(memberDetailsService, "userDetailsService cannot be null");
|
||||
Assert.notNull(redisTemplate, "redisTemplate cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
this.mobileUserDetailsService = mobileUserDetailsService;
|
||||
this.memberDetailsService = memberDetailsService;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@ -83,10 +83,10 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
|
||||
// 微信 code 获取 openid
|
||||
// 短信验证码校验
|
||||
Map<String, Object> additionalParameters = smsCodeAuthenticationToken.getAdditionalParameters();
|
||||
String mobile = (String) additionalParameters.get("mobile");
|
||||
String verifyCode = (String) additionalParameters.get("verifyCode");
|
||||
String mobile = (String) additionalParameters.get(SmsCodeParameterNames.MOBILE);
|
||||
String verifyCode = (String) additionalParameters.get(SmsCodeParameterNames.VERIFY_CODE);
|
||||
|
||||
if (!verifyCode.equals("666666")) { // 666666 是后门,因为短信收费,正式环境删除这个if
|
||||
String codeKey = SecurityConstants.SMS_CODE_PREFIX + mobile;
|
||||
@ -98,7 +98,7 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
}
|
||||
|
||||
// 根据手机号获取会员信息
|
||||
UserDetails userDetails = mobileUserDetailsService.loadUserByUsername(mobile);
|
||||
UserDetails userDetails = memberDetailsService.loadUserByMobile(mobile);
|
||||
|
||||
Authentication usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(userDetails, null);
|
||||
|
||||
|
@ -25,7 +25,7 @@ public class SmsCodeAuthenticationToken extends OAuth2AuthorizationGrantAuthenti
|
||||
private final Set<String> scopes;
|
||||
|
||||
/**
|
||||
* 授权类型(短信验证码:sms_code)
|
||||
* 授权类型(短信验证码: sms_code)
|
||||
*/
|
||||
public static final AuthorizationGrantType SMS_CODE = new AuthorizationGrantType("sms_code");
|
||||
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.youlai.auth.authentication.smscode;
|
||||
|
||||
/**
|
||||
* 短信验证码模式参数名称常量
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public final class SmsCodeParameterNames {
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
public static final String MOBILE = "mobile";
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
public static final String VERIFY_CODE = "verifyCode";
|
||||
|
||||
|
||||
private SmsCodeParameterNames() {
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,7 @@ package com.youlai.auth.authentication.wxminiapp;
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.youlai.auth.userdetails.member.OpenidUserDetailsService;
|
||||
import com.youlai.auth.userdetails.member.MemberDetailsService;
|
||||
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
@ -42,7 +42,7 @@ public class WxMiniAppAuthenticationProvider implements AuthenticationProvider {
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
private final OpenidUserDetailsService openidUserDetailsService;
|
||||
private final MemberDetailsService memberDetailsService;
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
|
||||
@ -57,17 +57,17 @@ public class WxMiniAppAuthenticationProvider implements AuthenticationProvider {
|
||||
public WxMiniAppAuthenticationProvider(
|
||||
OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
|
||||
OpenidUserDetailsService openidUserDetailsService,
|
||||
MemberDetailsService memberDetailsService,
|
||||
WxMaService wxMaService
|
||||
|
||||
) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
Assert.notNull(openidUserDetailsService, "userDetailsService cannot be null");
|
||||
Assert.notNull(memberDetailsService, "userDetailsService cannot be null");
|
||||
Assert.notNull(wxMaService, "wxMaService cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
this.openidUserDetailsService = openidUserDetailsService;
|
||||
this.memberDetailsService = memberDetailsService;
|
||||
this.wxMaService = wxMaService;
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ public class WxMiniAppAuthenticationProvider implements AuthenticationProvider {
|
||||
}
|
||||
String openid = sessionInfo.getOpenid();
|
||||
// 根据 openid 获取会员信息
|
||||
UserDetails userDetails = openidUserDetailsService.loadUserByUsername(openid);
|
||||
UserDetails userDetails = memberDetailsService.loadUserByOpenid(openid);
|
||||
|
||||
Authentication usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword());
|
||||
|
||||
|
@ -7,15 +7,19 @@ 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.password.ResourceOwnerPasswordAuthenticationConverter;
|
||||
import com.youlai.auth.authentication.password.ResourceOwnerPasswordAuthenticationProvider;
|
||||
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.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.authentication.wxminiapp.WxMiniAppAuthenticationConverter;
|
||||
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationProvider;
|
||||
import com.youlai.auth.userdetails.member.MemberUserDetails;
|
||||
import com.youlai.auth.userdetails.member.MobileUserDetailsService;
|
||||
import com.youlai.auth.userdetails.member.OpenidUserDetailsService;
|
||||
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationToken;
|
||||
import com.youlai.auth.userdetails.member.MemberDetails;
|
||||
import com.youlai.auth.userdetails.member.MemberDetailsService;
|
||||
import com.youlai.auth.userdetails.user.SysUserDetails;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@ -30,16 +34,21 @@ import org.springframework.security.config.annotation.authentication.configurati
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.*;
|
||||
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@ -52,14 +61,19 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 授权服务器配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class AuthorizationServerConfig {
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
private final MobileUserDetailsService mobileUserDetailsService;
|
||||
private final OpenidUserDetailsService openidUserDetailsService;
|
||||
private final RedisTemplate redisTemplate;
|
||||
private final MemberDetailsService memberDetailsService;
|
||||
|
||||
|
||||
/**
|
||||
@ -80,23 +94,23 @@ public class AuthorizationServerConfig {
|
||||
authorizationServerConfigurer
|
||||
.tokenEndpoint(tokenEndpoint ->
|
||||
tokenEndpoint
|
||||
.accessTokenRequestConverters( // <1>
|
||||
authenticationConverters ->
|
||||
.accessTokenRequestConverters(authenticationConverters ->// <1>
|
||||
authenticationConverters.addAll(
|
||||
List.of(
|
||||
new ResourceOwnerPasswordAuthenticationConverter(),
|
||||
new PasswordAuthenticationConverter(),
|
||||
new CaptchaAuthenticationConverter(),
|
||||
new WxMiniAppAuthenticationConverter(),
|
||||
new SmsCodeAuthenticationConverter()
|
||||
)
|
||||
)
|
||||
)
|
||||
.authenticationProviders( // <2>
|
||||
authenticationProviders ->
|
||||
.authenticationProviders(authenticationProviders ->// <2>
|
||||
authenticationProviders.addAll(
|
||||
List.of(
|
||||
new ResourceOwnerPasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator),
|
||||
new WxMiniAppAuthenticationProvider(authorizationService, tokenGenerator, openidUserDetailsService,wxMaService),
|
||||
new SmsCodeAuthenticationProvider(authorizationService, tokenGenerator, mobileUserDetailsService,redisTemplate)
|
||||
new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator),
|
||||
new CaptchaAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator, redisTemplate),
|
||||
new WxMiniAppAuthenticationProvider(authorizationService, tokenGenerator, memberDetailsService, wxMaService),
|
||||
new SmsCodeAuthenticationProvider(authorizationService, tokenGenerator, memberDetailsService, redisTemplate)
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -161,9 +175,16 @@ public class AuthorizationServerConfig {
|
||||
|
||||
@Bean
|
||||
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
|
||||
return new JdbcRegisteredClientRepository(jdbcTemplate);
|
||||
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
|
||||
|
||||
// 初始化 OAuth2 客户端
|
||||
initMallAppClient(registeredClientRepository);
|
||||
initMallAdminClient(registeredClientRepository);
|
||||
|
||||
return registeredClientRepository;
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
|
||||
RegisteredClientRepository registeredClientRepository) {
|
||||
@ -178,7 +199,6 @@ public class AuthorizationServerConfig {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {
|
||||
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));
|
||||
@ -200,7 +220,7 @@ public class AuthorizationServerConfig {
|
||||
JwtClaimsSet.Builder claims = context.getClaims();
|
||||
if (principal instanceof SysUserDetails userDetails) {
|
||||
claims.claim("user_id", String.valueOf(userDetails.getUserId()));
|
||||
} else if (principal instanceof MemberUserDetails userDetails) {
|
||||
} else if (principal instanceof MemberDetails userDetails) {
|
||||
claims.claim("member_id", String.valueOf(userDetails.getId()));
|
||||
}
|
||||
});
|
||||
@ -211,9 +231,84 @@ public class AuthorizationServerConfig {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
||||
return authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化创建商城管理客户端
|
||||
*
|
||||
* @param registeredClientRepository
|
||||
*/
|
||||
private void initMallAdminClient(JdbcRegisteredClientRepository registeredClientRepository) {
|
||||
|
||||
String clientId = "mall-admin";
|
||||
String clientSecret = "123456";
|
||||
String clientName = "商城管理客户端";
|
||||
|
||||
// 如果使用明文,在客户端认证的时候会自动升级加密方式(修改密码), 直接使用 bcrypt 加密避免不必要的麻烦
|
||||
// 不开玩笑,官方ISSUE: https://github.com/spring-projects/spring-authorization-server/issues/1099
|
||||
String encodeSecret = passwordEncoder().encode(clientSecret);
|
||||
|
||||
RegisteredClient registeredMallAdminClient = registeredClientRepository.findByClientId(clientId);
|
||||
String id = registeredMallAdminClient != null ? registeredMallAdminClient.getId() : UUID.randomUUID().toString();
|
||||
|
||||
RegisteredClient mallAppClient = RegisteredClient.withId(id)
|
||||
.clientId(clientId)
|
||||
.clientSecret(encodeSecret)
|
||||
.clientName(clientName)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.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)
|
||||
.scope(OidcScopes.PROFILE)
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
.build();
|
||||
registeredClientRepository.save(mallAppClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化创建商城APP客户端
|
||||
*
|
||||
* @param registeredClientRepository
|
||||
*/
|
||||
private void initMallAppClient(JdbcRegisteredClientRepository registeredClientRepository) {
|
||||
|
||||
String clientId = "mall-app";
|
||||
String clientSecret = "123456";
|
||||
String clientName = "商城APP客户端";
|
||||
|
||||
// 如果使用明文,在客户端认证的时候会自动升级加密方式,直接使用 bcrypt 加密避免不必要的麻烦
|
||||
String encodeSecret = passwordEncoder().encode(clientSecret);
|
||||
|
||||
RegisteredClient registeredMallAppClient = registeredClientRepository.findByClientId(clientId);
|
||||
String id = registeredMallAppClient != null ? registeredMallAppClient.getId() : UUID.randomUUID().toString();
|
||||
|
||||
RegisteredClient mallAppClient = RegisteredClient.withId(id)
|
||||
.clientId(clientId)
|
||||
.clientSecret(encodeSecret)
|
||||
.clientName(clientName)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.authorizationGrantType(WxMiniAppAuthenticationToken.WECHAT_MINI_APP) // 微信小程序模式
|
||||
.authorizationGrantType(SmsCodeAuthenticationToken.SMS_CODE) // 短信验证码模式
|
||||
.redirectUri("http://127.0.0.1:8080/authorized")
|
||||
.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope(OidcScopes.PROFILE)
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
.build();
|
||||
registeredClientRepository.save(mallAppClient);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ 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.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.youlai.auth.userdetails.member;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.youlai.common.constant.GlobalConstants;
|
||||
import com.youlai.mall.ums.dto.MemberAuthDTO;
|
||||
import lombok.Data;
|
||||
@ -9,7 +8,6 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
||||
|
||||
/**
|
||||
@ -19,7 +17,7 @@ import java.util.HashSet;
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
public class MemberUserDetails implements UserDetails {
|
||||
public class MemberDetails implements UserDetails {
|
||||
|
||||
/**
|
||||
* 会员ID
|
||||
@ -47,7 +45,7 @@ public class MemberUserDetails implements UserDetails {
|
||||
*
|
||||
* @param memAuthInfo 会员认证信息
|
||||
*/
|
||||
public MemberUserDetails(MemberAuthDTO memAuthInfo) {
|
||||
public MemberDetails(MemberAuthDTO memAuthInfo) {
|
||||
this.setId(memAuthInfo.getId());
|
||||
this.setUsername(memAuthInfo.getUsername());
|
||||
this.setEnabled(GlobalConstants.STATUS_YES.equals(memAuthInfo.getStatus()));
|
@ -12,29 +12,53 @@ import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 会员信息(openid为主体)加载实现类
|
||||
* 商城会员用户认证服务
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OpenidUserDetailsService implements UserDetailsService {
|
||||
public class MemberDetailsService {
|
||||
|
||||
private final MemberFeignClient memberFeignClient;
|
||||
|
||||
|
||||
/**
|
||||
* 手机号码认证方式
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @return 用户信息
|
||||
*/
|
||||
public UserDetails loadUserByMobile(String mobile) {
|
||||
Result<MemberAuthDTO> result = memberFeignClient.loadUserByMobile(mobile);
|
||||
|
||||
MemberAuthDTO memberAuthInfo;
|
||||
if (!(Result.isSuccess(result) && (memberAuthInfo = result.getData()) != null)) {
|
||||
throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
|
||||
}
|
||||
MemberDetails userDetails = new MemberDetails(memberAuthInfo);
|
||||
if (!userDetails.isEnabled()) {
|
||||
throw new DisabledException("该账户已被禁用!");
|
||||
} else if (!userDetails.isAccountNonLocked()) {
|
||||
throw new LockedException("该账号已被锁定!");
|
||||
} else if (!userDetails.isAccountNonExpired()) {
|
||||
throw new AccountExpiredException("该账号已过期!");
|
||||
}
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名获取用户信息
|
||||
*
|
||||
* @param openid 微信公众平台唯一身份标识
|
||||
* @return {@link MemberUserDetails}
|
||||
* @return {@link MemberDetails}
|
||||
*/
|
||||
public UserDetails loadUserByUsername(String openid) {
|
||||
public UserDetails loadUserByOpenid(String openid) {
|
||||
// 根据 openid 获取微信用户认证信息
|
||||
Result<MemberAuthDTO> getMemberAuthInfoResult = memberFeignClient.loadUserByOpenId(openid);
|
||||
|
||||
@ -61,7 +85,7 @@ public class OpenidUserDetailsService implements UserDetailsService {
|
||||
throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
|
||||
}
|
||||
|
||||
UserDetails userDetails = new MemberUserDetails(memberAuthInfo);
|
||||
UserDetails userDetails = new MemberDetails(memberAuthInfo);
|
||||
if (!userDetails.isEnabled()) {
|
||||
throw new DisabledException("该账户已被禁用!");
|
||||
} else if (!userDetails.isAccountNonLocked()) {
|
||||
@ -72,5 +96,4 @@ public class OpenidUserDetailsService implements UserDetailsService {
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
package com.youlai.auth.userdetails.member;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.youlai.common.enums.StatusEnum;
|
||||
import com.youlai.common.result.Result;
|
||||
import com.youlai.common.result.ResultCode;
|
||||
import com.youlai.mall.ums.api.MemberFeignClient;
|
||||
import com.youlai.mall.ums.dto.MemberAuthDTO;
|
||||
import com.youlai.mall.ums.dto.MemberRegisterDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 商城会员用户认证服务
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MobileUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final MemberFeignClient memberFeignClient;
|
||||
|
||||
|
||||
/**
|
||||
* 手机号码认证方式
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @return 用户信息
|
||||
*/
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String mobile) {
|
||||
Result<MemberAuthDTO> result = memberFeignClient.loadUserByMobile(mobile);
|
||||
|
||||
MemberAuthDTO memberAuthInfo;
|
||||
if (!(Result.isSuccess(result) && (memberAuthInfo = result.getData()) != null)) {
|
||||
throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
|
||||
}
|
||||
MemberUserDetails userDetails = new MemberUserDetails(memberAuthInfo);
|
||||
if (!userDetails.isEnabled()) {
|
||||
throw new DisabledException("该账户已被禁用!");
|
||||
} else if (!userDetails.isAccountNonLocked()) {
|
||||
throw new LockedException("该账号已被锁定!");
|
||||
} else if (!userDetails.isAccountNonExpired()) {
|
||||
throw new AccountExpiredException("该账号已过期!");
|
||||
}
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -14,9 +14,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* 系统用户信息
|
||||
* <p>
|
||||
* 包含用户名、密码和权限
|
||||
* 系统用户信息(包含用户名、密码和权限)
|
||||
* <p>
|
||||
* 用户名和密码用于认证,认证成功之后授予权限
|
||||
*
|
||||
|
@ -19,7 +19,6 @@ import org.springframework.stereotype.Service;
|
||||
* @author haoxr
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Primary // UserDetailsService 默认的实现,其他需要显式声明
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SysUserDetailsService implements UserDetailsService {
|
||||
|
@ -2,6 +2,9 @@ server:
|
||||
port: 9000
|
||||
|
||||
spring:
|
||||
mvc:
|
||||
path-match:
|
||||
matching-strategy: ant_path_matcher
|
||||
cloud:
|
||||
nacos:
|
||||
# 注册中心
|
||||
@ -18,4 +21,3 @@ spring:
|
||||
refresh: true
|
||||
username: nacos
|
||||
password: nacos
|
||||
|
||||
|
@ -10,13 +10,12 @@ spring:
|
||||
# 注册中心
|
||||
discovery:
|
||||
server-addr: http://f.youlai.tech:8848
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
# 配置中心
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
file-extension: yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
shared-configs[0]:
|
||||
data-id: youlai-common.yaml
|
||||
namespace: prod-namespace-id
|
||||
refresh: true
|
@ -0,0 +1,48 @@
|
||||
package com.youlai.auth.authentication;
|
||||
|
||||
|
||||
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;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Slf4j
|
||||
public class CaptchaAuthenticationTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
|
||||
@Test
|
||||
void testPasswordAuthentication() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth("mall-admin", "123456");
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/oauth2/token")
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, "captcha")
|
||||
.param(OAuth2ParameterNames.USERNAME, "admin")
|
||||
.param(OAuth2ParameterNames.PASSWORD, "123456")
|
||||
.param(CaptchaParameterNames.VERIFY_CODE, "123456")
|
||||
.param(CaptchaParameterNames.VERIFY_CODE_KEY, "123456")
|
||||
.headers(headers))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package com.youlai.auth.authentication;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Slf4j
|
||||
public class PasswordAuthenticationTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
|
||||
@Test
|
||||
void testPasswordAuthentication() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth("mall-admin", "123456");
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/oauth2/token")
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, "password")
|
||||
.param(OAuth2ParameterNames.USERNAME, "admin")
|
||||
.param(OAuth2ParameterNames.PASSWORD, "123456")
|
||||
.headers(headers))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.youlai.auth.authentication;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Slf4j
|
||||
public class SmsCodeAuthenticationTests {
|
||||
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void testSmsCodeAuthentication() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth("mall-app", "123456");
|
||||
|
||||
this.mvc.perform(post("/oauth2/token")
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, "sms_code")
|
||||
.param("mobile", "18866668888")
|
||||
.param("verifyCode", "666666")
|
||||
.headers(headers))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.youlai.auth.authentication;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Slf4j
|
||||
public class WechatMiniAppAuthenticationTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void testWechatMiniAppPasswordAuthentication() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth("mall-app", "123456");
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/oauth2/token")
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, "wechat_mini_app")
|
||||
.param(OAuth2ParameterNames.CODE, "codeVal")
|
||||
.headers(headers))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package com.youlai.auth.security.authentication.password;
|
||||
|
||||
|
||||
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationToken;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Slf4j
|
||||
public class ResourceOwnerPasswordAuthenticationTests {
|
||||
|
||||
|
||||
private final String clientId = "mall-app";
|
||||
private final String clientSecret = "secret";
|
||||
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 注册 mall-app 客户端
|
||||
|
||||
//
|
||||
String encodeSecret = passwordEncoder.encode(clientSecret);
|
||||
RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId(clientId)
|
||||
.clientSecret(encodeSecret)
|
||||
.clientName(clientId)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.authorizationGrantType(WxMiniAppAuthenticationToken.WECHAT_MINI_APP)
|
||||
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
|
||||
.redirectUri("http://127.0.0.1:8080/authorized")
|
||||
.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope(OidcScopes.PROFILE)
|
||||
.scope("message.read")
|
||||
.scope("message.write")
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
.build();
|
||||
|
||||
RegisteredClient registeredMessagingClient = registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredMessagingClient == null) {
|
||||
registeredClientRepository.save(messagingClient);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testLoginApiForOAuth2PasswordMode() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth("mall-app", "secret");
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/oauth2/token")
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, "password")
|
||||
.param(OAuth2ParameterNames.USERNAME, "admin")
|
||||
.param(OAuth2ParameterNames.PASSWORD, "123456")
|
||||
.headers(headers))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
package com.youlai.auth.security.authentication.password;
|
||||
|
||||
|
||||
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationToken;
|
||||
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationToken;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Slf4j
|
||||
public class SmsCodeAuthenticationTests {
|
||||
|
||||
|
||||
private final String clientId = "mall-app";
|
||||
private final String clientSecret = "secret";
|
||||
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 注册 mall-app 客户端
|
||||
|
||||
String encodeSecret = passwordEncoder.encode(clientSecret);
|
||||
RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId(clientId)
|
||||
.clientSecret(encodeSecret)
|
||||
.clientName(clientId)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.authorizationGrantType(WxMiniAppAuthenticationToken.WECHAT_MINI_APP)
|
||||
.authorizationGrantType(SmsCodeAuthenticationToken.SMS_CODE)
|
||||
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
|
||||
.redirectUri("http://127.0.0.1:8080/authorized")
|
||||
.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope(OidcScopes.PROFILE)
|
||||
.scope("message.read")
|
||||
.scope("message.write")
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
.build();
|
||||
|
||||
RegisteredClient registeredMessagingClient = registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredMessagingClient == null) {
|
||||
registeredClientRepository.save(messagingClient);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSmsCodeMode() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth("mall-app", "secret");
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/oauth2/token")
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, "sms_code")
|
||||
.param("mobile", "18866668888")
|
||||
.param("verifyCode", "666666")
|
||||
.headers(headers))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package com.youlai.auth.security.authentication.password;
|
||||
|
||||
|
||||
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationToken;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Slf4j
|
||||
public class WechatMiniAppAuthenticationTests {
|
||||
|
||||
|
||||
private final String clientId = "mall-app";
|
||||
private final String clientSecret = "secret";
|
||||
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 注册 mall-app 客户端
|
||||
|
||||
String encodeSecret = passwordEncoder.encode(clientSecret);
|
||||
RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId(clientId)
|
||||
.clientSecret(encodeSecret)
|
||||
.clientName(clientId)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.authorizationGrantType(WxMiniAppAuthenticationToken.WECHAT_MINI_APP)
|
||||
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
|
||||
.redirectUri("http://127.0.0.1:8080/authorized")
|
||||
.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope(OidcScopes.PROFILE)
|
||||
.scope("message.read")
|
||||
.scope("message.write")
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
.build();
|
||||
|
||||
RegisteredClient registeredMessagingClient = registeredClientRepository.findByClientId(clientId);
|
||||
if (registeredMessagingClient == null) {
|
||||
registeredClientRepository.save(messagingClient);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testWechatMiniAppPasswordMode() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth("mall-app", "secret");
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/oauth2/token")
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, "wechat_mini_app")
|
||||
.param(OAuth2ParameterNames.CODE, "codeVal")
|
||||
.headers(headers))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -65,7 +65,7 @@ public class CaptchaHandler implements HandlerFunction<ServerResponse> {
|
||||
String captchaBase64 = captcha.toBase64();
|
||||
Map<String, String> result = new HashMap<>(2);
|
||||
result.put("verifyCodeKey", uuid);
|
||||
result.put("verifyCodeImg", captchaBase64);
|
||||
result.put("verifyCodeBase64", captchaBase64);
|
||||
|
||||
return ServerResponse.ok().body(BodyInserters.fromValue(Result.success(result)));
|
||||
}
|
||||
|
@ -10,12 +10,12 @@ spring:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: http://f.youlai.tech:8848
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
file-extension: yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
shared-configs[0]:
|
||||
data-id: youlai-common.yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
refresh: true
|
||||
|
@ -162,7 +162,8 @@ public class SysUserController {
|
||||
@PostMapping("/_import")
|
||||
public Result importUsers(@Parameter(description = "部门ID") Long deptId, MultipartFile file) throws IOException {
|
||||
UserImportListener listener = new UserImportListener(deptId);
|
||||
String msg = importExcel(file.getInputStream(), UserImportVO.class, listener);
|
||||
EasyExcel.read(file.getInputStream(), UserImportVO.class, listener).sheet().doRead();
|
||||
String msg = listener.getMsg();
|
||||
return Result.success(msg);
|
||||
}
|
||||
|
||||
@ -177,10 +178,4 @@ public class SysUserController {
|
||||
EasyExcel.write(response.getOutputStream(), UserExportVO.class).sheet("用户列表")
|
||||
.doWrite(exportUserList);
|
||||
}
|
||||
|
||||
public static <T> String importExcel(InputStream is, Class clazz, MyAnalysisEventListener<T> listener) {
|
||||
EasyExcel.read(is, clazz, listener).sheet().doRead();
|
||||
return listener.getMsg();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import com.youlai.system.model.query.UserPageQuery;
|
||||
import com.youlai.system.model.vo.UserExportVO;
|
||||
import com.youlai.system.model.vo.UserInfoVO;
|
||||
import com.youlai.system.model.vo.UserPageVO;
|
||||
import com.youlai.system.service.SysMenuService;
|
||||
import com.youlai.system.service.SysRoleService;
|
||||
import com.youlai.system.service.SysUserRoleService;
|
||||
import com.youlai.system.service.SysUserService;
|
||||
@ -50,8 +49,6 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
|
||||
private final SysUserRoleService userRoleService;
|
||||
|
||||
private final SysMenuService menuService;
|
||||
|
||||
private final SysRoleService roleService;
|
||||
|
||||
private final UserConverter userConverter;
|
||||
|
@ -11,13 +11,13 @@ spring:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: http://f.youlai.tech:8848
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
file-extension: yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
shared-configs[0]:
|
||||
data-id: youlai-common.yaml
|
||||
namespace: prod-namespace-id
|
||||
namespace: prod
|
||||
refresh: true
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user