提交 ec404738 编写于 作者: 郝先瑞

feat: 新增SAS密码、验证码、短信验证码和微信小程序授权模式

上级 37a90229
......@@ -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
\ No newline at end of file
......@@ -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
......@@ -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>
......
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
);
}
}
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);
}
}
package com.youlai.auth.authentication.captcha;
import jakarta.annotation.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 验证码模式身份验证令牌(包含用户名、密码、验证码)
*
* @author haoxr
* @see org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken
* @since 3.0.0
*/
public class CaptchaAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
/**
* 令牌申请访问范围
*/
private final Set<String> scopes;
/**
* 授权类型(验证码: captcha)
*/
public static final AuthorizationGrantType CAPTCHA = new AuthorizationGrantType("captcha");
/**
* 验证码模式身份验证令牌
*
* @param clientPrincipal 客户端信息
* @param scopes 令牌申请访问范围
* @param additionalParameters 自定义额外参数(用户名、密码、验证码)
*/
public CaptchaAuthenticationToken(
Authentication clientPrincipal,
Set<String> scopes,
@Nullable Map<String, Object> additionalParameters
) {
super(CAPTCHA, clientPrincipal, additionalParameters);
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}
/**
* 用户凭证(密码)
*/
@Override
public Object getCredentials() {
return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD);
}
public Set<String> getScopes() {
return scopes;
}
}
package com.youlai.auth.authentication.captcha;
/**
* 验证码模式请求参数名称常量
*
* @author haoxr
* @since 3.0.0
*/
public final class CaptchaParameterNames {
/**
* 验证码
*/
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
......
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,9 +58,9 @@ public class ResourceOwnerPasswordAuthenticationProvider implements Authenticati
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator
public PasswordAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator
) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
......@@ -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");
......
/*
* 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,25 +94,25 @@ public class AuthorizationServerConfig {
authorizationServerConfigurer
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverters( // <1>
authenticationConverters ->
authenticationConverters.addAll(
List.of(
new ResourceOwnerPasswordAuthenticationConverter(),
new WxMiniAppAuthenticationConverter(),
new SmsCodeAuthenticationConverter()
)
.accessTokenRequestConverters(authenticationConverters ->// <1>
authenticationConverters.addAll(
List.of(
new PasswordAuthenticationConverter(),
new CaptchaAuthenticationConverter(),
new WxMiniAppAuthenticationConverter(),
new SmsCodeAuthenticationConverter()
)
)
)
.authenticationProviders( // <2>
authenticationProviders ->
authenticationProviders.addAll(
.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));
......@@ -194,13 +214,13 @@ public class AuthorizationServerConfig {
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (OAuth2TokenType.ACCESS_TOKEN .equals(context.getTokenType()) && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) {
// Customize headers/claims for access_token
Optional.ofNullable(context.getPrincipal().getPrincipal()).ifPresent(principal -> {
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;
......
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;
}
}
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:
# 注册中心
......@@ -17,5 +20,4 @@ spring:
data-id: youlai-common.yaml
refresh: true
username: nacos
password: nacos
password: nacos
\ No newline at end of file
......@@ -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
\ No newline at end of file
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
}
}
\ No newline at end of file
package com.youlai.auth.security.authentication.password;
package com.youlai.auth.authentication;
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;
......@@ -29,59 +18,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@SpringBootTest
@AutoConfigureMockMvc
@Slf4j
public class ResourceOwnerPasswordAuthenticationTests {
private final String clientId = "mall-app";
private final String clientSecret = "secret";
@Autowired
private RegisteredClientRepository registeredClientRepository;
public class PasswordAuthenticationTests {
@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 {
void testPasswordAuthentication() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth("mall-app", "secret");
headers.setBasicAuth("mall-admin", "123456");
// @formatter:off
this.mvc.perform(post("/oauth2/token")
......
package com.youlai.auth.security.authentication.password;
package com.youlai.auth.authentication;
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;
......@@ -33,58 +21,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
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 {
void testSmsCodeAuthentication() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth("mall-app", "secret");
headers.setBasicAuth("mall-app", "123456");
// @formatter:off
this.mvc.perform(post("/oauth2/token")
.param(OAuth2ParameterNames.GRANT_TYPE, "sms_code")
.param("mobile", "18866668888")
......@@ -93,7 +37,6 @@ public class SmsCodeAuthenticationTests {
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty());
// @formatter:on
}
......
package com.youlai.auth.security.authentication.password;
package com.youlai.auth.authentication;
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;
......@@ -31,56 +20,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@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 {
void testWechatMiniAppPasswordAuthentication() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth("mall-app", "secret");
headers.setBasicAuth("mall-app", "123456");
// @formatter:off
this.mvc.perform(post("/oauth2/token")
......
......@@ -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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册