Spring Boot Security OAuth2 Client + JustAuth 实现第三方登录
Spring Boot Security OAuth2 Client + JustAuth 实现第三方登录
1. 引言
在现代Web应用中,第三方登录已成为提升用户体验的重要功能。用户无需记忆额外的账号密码,只需通过已有的社交账号(如微信、GitHub、Google等)即可快速登录,大大降低了注册门槛,提高了用户转化率。
Spring Security OAuth2 Client是Spring官方提供的OAuth2客户端实现,支持标准的OAuth2和OpenID Connect协议。而JustAuth是国内开源的第三方登录库,开箱即用,支持国内外数十家主流平台的第三方登录。
本文将详细介绍如何在Spring Boot 3.x项目中,结合Spring Security OAuth2 Client和JustAuth,实现微信等第三方登录功能。
2. 技术栈
- Spring Boot 3.x
- Spring Security 6.x
- Spring Security OAuth2 Client
- JustAuth 1.16.x
3. OAuth2与第三方登录原理
3.1 OAuth2授权流程
OAuth2是一种授权框架,允许第三方应用在用户授权下,获取有限的资源访问权限。标准OAuth2授权码模式的流程如下:
┌─────────┐ ┌──────────────┐
│ │ 1. 用户点击第三方登录 │ │
│ 用户 │ ──────────────────────────────>│ 前端应用 │
│ │ │ │
└─────────┘ └──────────────┘
│
2. 重定向到授权服务器 │
▼
┌──────────────┐
│ │
│ 授权服务器 │
│ (微信/GitHub)│
│ │
└──────────────┘
│
3. 用户授权后重定向回应用 │
▼
┌─────────┐ ┌──────────────┐
│ │ 4. 获取用户信息并登录 │ │
│ 用户 │ <──────────────────────────────│ 后端服务 │
│ │ │ │
└─────────┘ └──────────────┘
3.2 Spring Security OAuth2 Client && JustAuth
JustAuth是一个OAuth2的一个客户端库,它封装了OAuth2的请求,内置支持国内外数十家主流平台的第三方登录。相比于Spring Security OAuth2 Client,JustAuth的优势在于:
- 开箱即用,无需手动配置各平台的回调URL等参数。
- 支持国内外数十家主流平台的第三方登录,包括微信、GitHub、Google、Facebook等。
- 提供了简单的API接口,方便集成到Spring Boot项目中。
Spring-boot-starter-oauth2-client是一个完整的OAuth2客户端实现,支持标准的OAuth2和OpenID Connect协议。它提供了丰富的配置选项,方便自定义授权流程、令牌管理等。
由于Spring Security OAuth2 Client的提供了完备性OAuth2-client流程,我们可以将JustAuth集成到Spring-boot-starter-oauth2-client中,即保留了Oauth2-client的完整性,同时又利用了JustAuth的便利性和兼容性,实现第三方登录功能。
4. 具体实现
4.1 创建Spring Boot项目(可选)
使用Spring Boot Initializr创建项目,添加以下依赖:
- Spring Security
- Spring Web
- OAuth2 Client
- Lombok
4.2 添加相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.8</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>oauth2-login-demo</artifactId>
<version>1.0.0</version>
<name>oauth2-login-demo</name>
<properties>
<java.version>21</java.version>
<justauth.version>1.16.6</justauth.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5. Spring Security OAuth2 Client集成JustAuth
5.1 自定义OAuth2ClientAuthExecutor授权执行器
创建JustAuth的授权执行器JustAuthExecutor.java:
public class JustAuthExecutor implements OAuth2ClientAuthExecutor {
private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");
private final Set<Class<?>> authRequestTypes = ClassUtil.scanPackage(ClassUtil.getPackage(AuthRequest.class));
private AuthRequest getAuthRequest(ClientRegistration clientRegistration) {
Map<String, Object> configurationMetadata = clientRegistration.getProviderDetails().getConfigurationMetadata();
String productCode = (String) configurationMetadata.get(OAuth2ClientConstants.PRODUCT_CODE_NAME);
if(StrUtil.isBlank(productCode)) {
throw new OAuth2AuthenticationException("Missing product code in configuration metadata.");
}
Class<?> authRequestType = null;
String typeName = "Auth" + productCode + "Request";
for(Class<?> clz : authRequestTypes) {
if (clz.getSimpleName().equalsIgnoreCase(typeName)) {
authRequestType = clz;
break;
}
}
if(authRequestType == null) {
throw new OAuth2AuthenticationException("[" + productCode + "] request type not found.");
}
String clientId = clientRegistration.getClientId();
String clientSecret = clientRegistration.getClientSecret();
String redirectUri = clientRegistration.getRedirectUri();
AuthConfig.AuthConfigBuilder authConfigBuilder = AuthConfig.builder();
authConfigBuilder.clientId(clientId)
.clientSecret(clientSecret)
.redirectUri(redirectUri);
Map<String, Object> additionalParameters = (Map<String, Object>) configurationMetadata.get(OAuth2ClientConstants.PARAMETER_NAME_ADDITIONAL_PARAMETERS);
if(MapUtil.isNotEmpty(additionalParameters)) {
Set<String> methodNames = ClassUtil.getDeclaredMethodNames(authConfigBuilder.getClass());
for(Map.Entry<String, Object> entry : additionalParameters.entrySet()) {
String attributeName = entry.getKey();
Object value = entry.getValue();
if(methodNames.contains(attributeName) && value != null) {
ReflectUtil.invoke(authConfigBuilder, attributeName, value);
}
}
}
HttpConfig httpConfig = HttpConfig.builder().timeout(10000).build();// 延长超时时间
AuthConfig authConfig = authConfigBuilder.httpConfig(httpConfig).build();
return (AuthRequest) (AuthDefaultRequest) ReflectUtil.newInstance(authRequestType, authConfig);
}
public void authorizationRequest(ClientRegistration clientRegistration, OAuth2AuthorizationRequest.Builder builder) {
OAuth2AuthorizationRequest authorizationRequest = builder.build();
String state = authorizationRequest.getState();
AuthRequest authRequest = getAuthRequest(clientRegistration);
String authorizeUrl = authRequest.authorize(state);
String query = URLUtil.toURI(authorizeUrl).getRawQuery();
builder.parameters(parameters -> {
// 删除旧数据
parameters.remove(OAuth2ParameterNames.CLIENT_ID);
parameters.remove(OAuth2ParameterNames.CLIENT_SECRET);
Matcher matcher = QUERY_PARAM_PATTERN.matcher(query);
while (matcher.find()) {
String name = matcher.group(1);
String eq = matcher.group(2);
String value = matcher.group(3);
if(name.equals(OAuth2ParameterNames.REDIRECT_URI) && value != null) {
value = URLUtil.decode(value);
}
parameters.put(name, (value != null ? value : (StringUtils.hasLength(eq) ? "" : null)));
}
});
}
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
String code = authorizationCodeGrantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode();
HttpServletRequest request = WebUtil.getRequest();
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, Object> parameters = MapUtil.newHashMap();
for(Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String[] value = entry.getValue();
String key = entry.getKey();
if(ArrayUtil.isNotEmpty(value)) {
if(value.length == 1) {
parameters.put(key, value[0]);
}else {
parameters.put(key, value);
}
}
}
AuthCallback authCallback = BeanUtil.toBeanIgnoreError(parameters, AuthCallback.class);
AuthToken authToken = getAuthRequest(clientRegistration).getAccessToken(authCallback);
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(authToken.getAccessToken());
builder.tokenType(OAuth2AccessToken.TokenType.BEARER);
builder.refreshToken(authToken.getRefreshToken());
builder.expiresIn(authToken.getExpireIn());
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.CODE, code);
additionalParameters.put(OAuth2ClientConstants.PARAMETER_NAME_AUTH_TOKEN, authToken);
builder.additionalParameters(additionalParameters);
return builder.build();
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
ClientRegistration clientRegistration = userRequest.getClientRegistration();
AuthToken authToken = (AuthToken) userRequest.getAdditionalParameters().get(OAuth2ClientConstants.PARAMETER_NAME_AUTH_TOKEN);
AuthUser authUser = getAuthRequest(clientRegistration).getUserInfo(authToken);
List<GrantedAuthority> grantedAuthorities = CollectionUtil.newArrayList();
for (String scope : userRequest.getClientRegistration().getScopes()) {
grantedAuthorities.add(new SimpleGrantedAuthority(scope));
}
Map<String, Object> attributes = BeanUtil.beanToMap(authUser);
return new DefaultOAuth2User(grantedAuthorities, attributes, LambdaUtil.getFieldName(AuthUser::getUuid));
}
}
5.2 自定义OAuth2AuthorizationRequestResolver,集成OAuth2ClientAuthExecutor进行自定义授权请求
创OAuth2AuthorizationRequestCustomizer.java:
@RequiredArgsConstructor
public class OAuth2AuthorizationRequestCustomizer implements Consumer<Builder> {
private final ClientRegistrationRepository clientRegistrationRepository;
@Override
public void accept(Builder builder) {
StringBuilder registrationId = new StringBuilder();
builder.attributes(attributes -> {
registrationId.append(attributes.get(OAuth2ParameterNames.REGISTRATION_ID));
});
ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId.toString());
OAuth2ClientAuthExecutor authExecutor = OAuth2ClientAuthExecutor.getAuthExecutor(clientRegistration);
if(ObjUtil.isNull(authExecutor)) {
return;
}
authExecutor.authorizationRequest(clientRegistration, builder);
}
}
5.3 自定义OAuth2AccessTokenResponseClient客户端,集成OAuth2ClientAuthExecutor进行自定义访问令牌响应
创建OAuth2ClientAuthorizationCodeTokenResponseClient.java:
public class OAuth2ClientAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private final RestClientAuthorizationCodeTokenResponseClient delegating = new RestClientAuthorizationCodeTokenResponseClient();
public OAuth2ClientAuthorizationCodeTokenResponseClient() {
OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
accessTokenResponseHttpMessageConverter.setSupportedMediaTypes(List.of(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
RestClient restClient = RestClient.builder()
.messageConverters((messageConverters) -> {
messageConverters.clear();
messageConverters.add(new FormHttpMessageConverter());
messageConverters.add(accessTokenResponseHttpMessageConverter);
})
.defaultStatusHandler(new OAuth2ErrorResponseErrorHandler())
.build();
delegating.setRestClient(restClient);
}
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
OAuth2ClientAuthExecutor authExecutor = OAuth2ClientAuthExecutor.getAuthExecutor(clientRegistration);
if(ObjUtil.isNull(authExecutor)) {
return delegating.getTokenResponse(authorizationCodeGrantRequest);
}
return authExecutor.getTokenResponse(authorizationCodeGrantRequest);
}
}
5.4 自定义OAuth2用户服务,集成OAuth2ClientAuthExecutor进行自定义用户加载
创建Oauth2ClientUserService.java:
public class Oauth2ClientUserService extends DefaultOAuth2UserService {
public Oauth2ClientUserService() {
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setSupportedMediaTypes(List.of(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
RestTemplate restTemplate = new RestTemplate(List.of(messageConverter));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
super.setRestOperations(restTemplate);
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
ClientRegistration clientRegistration = userRequest.getClientRegistration();
OAuth2ClientAuthExecutor oAuth2ClientAuthExecutor = OAuth2ClientAuthExecutor.getAuthExecutor(clientRegistration);
if(ObjUtil.isNull(oAuth2ClientAuthExecutor)) {
return super.loadUser(userRequest);
}
return oAuth2ClientAuthExecutor.loadUser(userRequest);
}
}
6. JustAuth实现微信登录
微信登录采用定制化的OAuth2协议,与标准协议存在差异。使用JustAuth可以大大简化接入流程。
6.1 微信开放平台配置
- 注册微信开放平台账号:https://open.weixin.qq.com
- 创建网站应用,获取AppID和AppSecret
- 配置授权回调域名
6.2 配置执行器名称和产品代码
在configuration_metadata中配置authExecutorName和productCode,例如:
{"@class":"java.util.Collections$UnmodifiableMap","authExecutorName":"JustAuthExecutor","productCode": "WeChatOpen"}
7. 参考资源
完整代码请参考项目仓库:mwc-security-authentication-server
演示地址:mwcs
更多推荐




所有评论(0)