有了前面几篇针对spring security原理分析
的博客,现在基于spring security social实现第三方登录就容易多了。
1.依赖
基于JDK11,spring boot版本2.1.6.RELEASE
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.security.oauth:spring-security-oauth2:2.3.6.RELEASE'
compile 'org.springframework.social:spring-social-core:1.1.6.RELEASE'
compile 'org.springframework.social:spring-social-config:1.1.6.RELEASE'
compile 'org.springframework.social:spring-social-security:1.1.6.RELEASE'
compile 'org.springframework.social:spring-social-web:1.1.6.RELEASE'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
compile 'org.apache.commons:commons-lang3:3.9'
compile 'commons-collections:commons-collections:3.2.2'
compile 'commons-beanutils:commons-beanutils:1.9.3'
compile 'javax.xml.bind:jaxb-api:2.3.0'
compile 'com.sun.xml.bind:jaxb-impl:2.3.0'
compile 'com.sun.xml.bind:jaxb-core:2.3.0'
2.源码分析
因为spring security social是基于SocialAuthenticationFilter实现的,所以咱们从SocialAuthenticationFilter入手开始分析:
1.一说到Filter,必定有一个对应的配置类
SocialAuthenticationFilter也不例外,它对应的配置类就是SpringSocialConfigurer,为了让项目运行起来,咱们先把配置准备好:
@EnableSocial
@Configuration
public class SocialConfig extends SocialConfigurerAdapter {
public static final String UTF_8 = "UTF-8";
// 注意要配置数据源
@Autowired private DataSource dataSource;
@Autowired private ConnectionFactoryLocator connectionFactoryLocator;
/**
* 创建SpringSocialFilter过滤器配置,用于第三方登录
*
* @return
*/
@Bean
public SpringSocialConfigurer springSocialConfigurer() {
return new SpringSocialConfigurer();
}
// 添加关于ProviderId=qq的处理器。注意clientId和clientSecret需要自己去qq互联申请
@Override
public void addConnectionFactories(
ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
connectionFactoryConfigurer.addConnectionFactory(
new QQConnectionFactory("qq", "clientId", "clientSecret"));
}
/**
* 必须要创建一个UserIdSource,否则会报错
*
* @return
*/
@Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
/**
* 指定UsersConnectionRepository,用于操作数据库中第三方用户信息
*
* @return
*/
@Override
public UsersConnectionRepository getUsersConnectionRepository(
ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository connectionRepository =
new JdbcUsersConnectionRepository(
dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 设置表单前缀
// connectionRepository.setTablePrefix("");
// 注意配置ConnectionSignUp后,将不会跳注册页面,会自动完成注册
//connectionRepository.setConnectionSignUp();
return connectionRepository;
}
/**
* 用于跳注册页面后从session中获取第三方用户信息
*
* @return
*/
@Bean
public ProviderSignInUtils providerSignInUtils() {
return new ProviderSignInUtils(
connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator));
}
}
注意要在数据库中创建以下数据表:
create table UserConnection (userId varchar(255) not null,
providerId varchar(255) not null,
providerUserId varchar(255),
rank int not null,
displayName varchar(255),
profileUrl varchar(512),
imageUrl varchar(512),
accessToken varchar(512) not null,
secret varchar(512),
refreshToken varchar(512),
expireTime bigint,
primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
@Configuration
@EnableWebSecurity
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SpringSocialConfigurer springSocialConfigurer;
@Override
public void configure(HttpSecurity http) throws Exception {
/**
* 将springSocialConfigurer配置加入spring security filter配置中
*/
http.apply(springSocialConfigurer)
.and()
.formLogin()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
}
}
以上两个配置,就完全可以让程序运行起来了。
2.访问/auth/qq,进入SocialAuthenticationFilter中
private static final String DEFAULT_FILTER_PROCESSES_URL = "/auth";
private String filterProcessesUrl = DEFAULT_FILTER_PROCESSES_URL;
// 拦截符合要求的请求
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
//假如请求的url是/auth/qq,那么ProviderId提取出来就是qq
String providerId = getRequestedProviderId(request);
if (providerId != null){
Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
// 检查是否存在处理ProviderId=qq的SocialAuthenticationService
return authProviders.contains(providerId);
}
return false;
}
//分析请求的url,从url中提取ProviderId。
//假如请求的url是/auth/qq,那么ProviderId提取出来就是qq
private String getRequestedProviderId(HttpServletRequest request) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
// uri must start with context path
uri = uri.substring(request.getContextPath().length());
// remaining uri must start with filterProcessesUrl
if (!uri.startsWith(filterProcessesUrl)) {
return null;
}
uri = uri.substring(filterProcessesUrl.length());
// expect /filterprocessesurl/provider, not /filterprocessesurlproviderr
if (uri.startsWith("/")) {
return uri.substring(1);
} else {
return null;
}
}
从上面代码中,我们发现需要配置一个ProviderId=qq的SocialAuthenticationService,我们找到SocialAuthenticationServiceLocator的实现类SocialAuthenticationServiceRegistry,发现里面有一个专门用于添加SocialAuthenticationService的方法
public void addAuthenticationService(SocialAuthenticationService<?> authenticationService) {
addConnectionFactory(authenticationService.getConnectionFactory());
authenticationServices.put(authenticationService.getConnectionFactory().getProviderId(), authenticationService);
}
通过debug分析,发现在SocialConfiguration类中的下面代码会创建SocialAuthenticationServiceRegistry并且调用addAuthenticationService
@Bean
public ConnectionFactoryLocator connectionFactoryLocator() {
if (securityEnabled) {
SecurityEnabledConnectionFactoryConfigurer cfConfig = new SecurityEnabledConnectionFactoryConfigurer();
for (SocialConfigurer socialConfigurer : socialConfigurers) {
socialConfigurer.addConnectionFactories(cfConfig, environment);
}
return cfConfig.getConnectionFactoryLocator();
} else {
DefaultConnectionFactoryConfigurer cfConfig = new DefaultConnectionFactoryConfigurer();
for (SocialConfigurer socialConfigurer : socialConfigurers) {
socialConfigurer.addConnectionFactories(cfConfig, environment);
}
return cfConfig.getConnectionFactoryLocator();
}
}
最终,通过对上面代码的分析,我们可以重写SocialConfigurerAdapter的addConnectionFactories方法,也就是咱们上面的SocialConfig类:
// 添加关于ProviderId=qq的处理器。注意clientId和clientSecret需要自己去qq互联申请
@Override
public void addConnectionFactories(
ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
connectionFactoryConfigurer.addConnectionFactory(
new QQConnectionFactory("qq", "clientId", "clientSecret"));
}
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
/** @author zouwei */
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
public QQConnectionFactory(String providerId, String clientId, String clientSecret) {
super(providerId, new QQOAuth2ServiceProvider(clientId, clientSecret), new QQApiAdapter());
}
}
继续跟进/auth/qq这个请求,当我们添加了一个针对qq的ConnectionFactory后,请求将向后执行至:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (detectRejection(request)) {
if (logger.isDebugEnabled()) {
logger.debug("A rejection was detected. Failing authentication.");
}
throw new SocialAuthenticationException("Authentication failed because user rejected authorization.");
}
Authentication auth = null;
Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
//获取/auth/qq中的ProviderId=qq
String authProviderId = getRequestedProviderId(request);
if (!authProviders.isEmpty() && authProviderId != null && authProviders.contains(authProviderId)) {
// 根据ProviderId=qq获取SocialAuthenticationService
SocialAuthenticationService<?> authService = authServiceLocator.getAuthenticationService(authProviderId);
// 通过SocialAuthenticationService获取Authentication
auth = attemptAuthService(authService, request, response);
if (auth == null) {
throw new AuthenticationServiceException("authentication failed");
}
}
return auth;
}
private Authentication attemptAuthService(final SocialAuthenticationService<?> authService, final HttpServletRequest request, HttpServletResponse response)
throws SocialAuthenticationRedirectException, AuthenticationException {
// 通过SocialAuthenticationService获取token
final SocialAuthenticationToken token = authService.getAuthToken(request, response);
if (token == null) return null;
Assert.notNull(token.getConnection());
// 获取SecurityContext中的Authentication,未登录的话就是null
Authentication auth = getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
// 将获取到的第三方用户信息做一个更新,并返回Authentication
return doAuthentication(authService, request, token);
} else {
// 如果不是null,会检查数据库中的第三方用户信息表单,不存在就会添加进表单
addConnection(authService, request, token, auth);
return null;
}
}
咱们跟着代码进final SocialAuthenticationToken token = authService.getAuthToken(request, response);也就是OAuth2AuthenticationService
public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
// 获取请求参数code
String code = request.getParameter("code");
// 如果没有拿到code,说明不是回调请求,根据oauth2.0协议,就要创建一个请求进入第三方登录页面
if (!StringUtils.hasText(code)) {
//准备请求参数
OAuth2Parameters params = new OAuth2Parameters();
params.setRedirectUri(buildReturnToUrl(request));
setScope(request, params);
params.add("state", generateState(connectionFactory, request));
addCustomParameters(params);
// 抛出异常,准备跳转至第三方登录页面
throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
} else if (StringUtils.hasText(code)) {
// 如果code不为空,那么意味着就是第三方回调请求,那么就需要截取code值去请求accessToken
try {
// 回调url,要保持和之前的回调url一致
String returnToUrl = buildReturnToUrl(request);
// 重点在于这里,这里是获取accessToken
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
// 通过ApiAdapter设置ConnectionValues
Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
return new SocialAuthenticationToken(connection, null);
} catch (RestClientException e) {
logger.debug("failed to exchange for access", e);
return null;
}
} else {
return null;
}
}
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
分析上面这段代码,getConnectionFactory().getOAuthOperations()执行逻辑是获取QQConnectionFactory中QQOAuth2ServiceProvider里面的OAuth2Operations,一般情况下就是OAuth2Template,但是因为qq服务器返回的响应数据OAuth2Template处理不了,所以我们需要自定义OAuth2Template
import com.example.oauth2.social.SocialConfig;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
/** @author zouwei */
public class QQTemplate extends OAuth2Template {
public QQTemplate(
String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
// 设置成true才会将clientId和clientSecret放到请求参数中
setUseParametersForClientAuthentication(true);
}
/**
* 自定义发送请求并解析
* https://wiki.connect.qq.com/%E4%BD%BF%E7%94%A8authorization_code%E8%8E%B7%E5%8F%96access_token
*
* @param accessTokenUrl
* @param parameters
* @return
*/
@Override
protected AccessGrant postForAccessGrant(
String accessTokenUrl, MultiValueMap<String, String> parameters) {
/** 虽然qq互联上文档中获取accessToken要求发送GET请求, 但是结果并不如意,最终通过postForObject获得期望的结果 */
String result = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
return createAccessGrant(result);
}
/**
* 根据返回的结构创建AccessGrant 成功返回,即可在返回包中获取到Access Token。 如:
* access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
*
* @param responseResult
* @return
*/
private AccessGrant createAccessGrant(String responseResult) {
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseResult, "&");
String access_token = StringUtils.substringAfterLast(items[0], "=");
String expires_in = StringUtils.substringAfterLast(items[1], "=");
String refresh_token = StringUtils.substringAfterLast(items[2], "=");
return new AccessGrant(
access_token, StringUtils.EMPTY, refresh_token, Long.valueOf(expires_in));
}
/**
* 因为返回access_token的响应类型是text/html,所以需要添加额外的HttpMessageConverter
*
* @return
*/
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate
.getMessageConverters()
.add(new StringHttpMessageConverter(Charset.forName(SocialConfig.UTF_8)));
return restTemplate;
}
}
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
/** @author zouwei */
public class QQOAuth2ServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
private static final String authorizeUrl = "https://graph.qq.com/oauth2.0/authorize";
private static final String accessTokenUrl = "https://graph.qq.com/oauth2.0/token";
private final String clientId;
public QQOAuth2ServiceProvider(String clientId, String clientSecret) {
super(new QQTemplate(clientId, clientSecret, authorizeUrl, accessTokenUrl));
this.clientId = clientId;
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken, clientId);
}
}
后续通过AcessGrant通过Connection:
// 通过ApiAdapter设置ConnectionValues
Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
也就是调用下面的setConnectionValues方法
import org.apache.commons.lang3.StringUtils;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
/** @author zouwei */
public class QQApiAdapter implements ApiAdapter<QQ> {
@Override
public boolean test(QQ api) {
return true;
}
@Override
public void setConnectionValues(QQ api, ConnectionValues values) {
QQUserInfo userInfo = api.userInfo();
// 设置用户信息,用户信息需要从api里面来
// 昵称
values.setDisplayName(userInfo.getNickname());
// 头像
values.setImageUrl(userInfo.getFigureurl_qq_1());
// 个人主页
values.setProfileUrl(StringUtils.EMPTY);
// openId
values.setProviderUserId(userInfo.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQ api) {
return null;
}
@Override
public void updateStatus(QQ api, String message) {}
}
在拿到token后,咱们回到SocialAuthenticationFilter.attemptAuthService方法中的doAuthentication操作
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
try {
if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
token.setDetails(authenticationDetailsSource.buildDetails(request));
// 根据数据库中是否存在当前第三方用户信息
// 不存在就抛异常并跳转进注册页面
Authentication success = getAuthenticationManager().authenticate(token);
Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
// 假如已经存在这个用户,就更新数据
updateConnections(authService, token, success);
return success;
} catch (BadCredentialsException e) {
// 如果是需要注册的用户,就跳注册页面
if (signupUrl != null) {
// 这里会把ConnectionData存储进session,可以通过ProviderSignInUtils获取
sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
}
throw e;
}
}
注意这里:
Authentication success = getAuthenticationManager().authenticate(token);
这段代码会先跳转至ProviderManager.authenticate,再进入SocialAuthenticationProvider.authenticate
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
String providerId = authToken.getProviderId();
Connection<?> connection = authToken.getConnection();
// 查询数据库是否存在当前第三方数据,不存在就抛异常
// 通过JdbcUsersConnectionRepository查询
String userId = toUserId(connection);
if (userId == null) {
throw new BadCredentialsException("Unknown access token");
}
UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
if (userDetails == null) {
throw new UsernameNotFoundException("Unknown connected account id");
}
return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
}
JdbcUsersConnectionRepository
public List<String> findUserIdsWithConnection(Connection<?> connection) {
ConnectionKey key = connection.getKey();
List<String> localUserIds = jdbcTemplate.queryForList("select userId from " + tablePrefix + "UserConnection where providerId = ? and providerUserId = ?", String.class, key.getProviderId(), key.getProviderUserId());
// 根据代码逻辑,发现假如设置了connectionSignUp,那么就会自动创建一个newUserId,就不会抛异常,也就不会进入注册页面
if (localUserIds.size() == 0 && connectionSignUp != null) {
String newUserId = connectionSignUp.execute(connection);
if (newUserId != null)
{
createConnectionRepository(newUserId).addConnection(connection);
return Arrays.asList(newUserId);
}
}
return localUserIds;
}
到这里,根据源码分析第三方登录基本告一段落,剩下的几个相关类分别是
QQImpl
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
/** @author zouwei */
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
/** 获取openId */
private static final String URL_OPEN_ID = "https://graph.qq.com/oauth2.0/me";
/** 获取用户信息 */
private static final String URL_USER_INFO =
"https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
private String openId;
private String clientId;
private ObjectMapper objectMapper = new ObjectMapper();
public QQImpl(String accessToken, String clientId) {
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.clientId = clientId;
// 之所以需要获取openId,是因为需要通过openId获取用户信息
this.openId = requestOpenId(accessToken);
}
/**
* 获取openId
*
* <p>响应: callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
*
* @param accessToken
* @return
*/
private String requestOpenId(String accessToken) {
// String url = String.format(URL_OPEN_ID, accessToken);
String result = getRestTemplate().getForObject(URL_OPEN_ID, String.class);
String first = StringUtils.substringBeforeLast(result, "\"");
return StringUtils.substringAfterLast(first, "\"");
}
/**
* 获取用户信息
*
* <p>https://wiki.connect.qq.com/get_user_info
*
* @return
*/
@Override
public QQUserInfo userInfo() {
String url = String.format(URL_USER_INFO, clientId, openId);
String result = getRestTemplate().getForObject(url, String.class);
try {
QQUserInfo userInfo = objectMapper.readValue(result, QQUserInfo.class);
userInfo.setOpenId(openId);
return userInfo;
} catch (Exception e) {
throw new RuntimeException("获取用户信息失败", e);
}
}
}
QQUserInfo
import lombok.Getter;
import lombok.Setter;
/** qq用户信息 */
@Getter
@Setter
public class QQUserInfo {
/** 返回码 */
private String ret;
/** 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。 */
private String msg;
/** */
private String openId;
/** 不知道什么东西,文档上没写,但是实际api返回里有。 */
private String is_lost;
/** 省(直辖市) */
private String province;
/** 市(直辖市区) */
private String city;
/** 出生年月 */
private String year;
/** 用户在QQ空间的昵称。 */
private String nickname;
/** 大小为30×30像素的QQ空间头像URL。 */
private String figureurl;
/** 大小为50×50像素的QQ空间头像URL。 */
private String figureurl_1;
/** 大小为100×100像素的QQ空间头像URL。 */
private String figureurl_2;
private String figureurl_type;
private String figureurl_qq;
/** 大小为40×40像素的QQ头像URL。 */
private String figureurl_qq_1;
/** 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。 */
private String figureurl_qq_2;
/** 性别。 如果获取不到则默认返回”男” */
private String gender;
/** 标识用户是否为黄钻用户(0:不是;1:是)。 */
private String is_yellow_vip;
/** 标识用户是否为黄钻用户(0:不是;1:是) */
private String vip;
/** 黄钻等级 */
private String yellow_vip_level;
/** 黄钻等级 */
private String level;
/** 标识是否为年费黄钻用户(0:不是; 1:是) */
private String is_yellow_year_vip;
private String constellation;
}
注意
1.一定要有一个SocialUserDetailsService的实现类
2.要把一些不需要权限的接口开放,否则会一直跳登录页面
http.apply(springSocialConfigurer)
.and()
.formLogin()
.and()
.authorizeRequests()
.antMatchers("/auth/qq","/signup","/connected/userInfo")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable();
3.跳注册页面后,可通过ProviderSignInUtils访问刚刚获得的第三方用户信息
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionKey;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/** @author zouwei */
@RestController
public class SocialController {
@Autowired private ProviderSignInUtils providerSignInUtils;
@Autowired private HttpServletRequest request;
@GetMapping("/connected/userInfo")
public ConnectedUserInfo socialUserInfo() {
Connection connection =
providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
if (Objects.isNull(connection)) {
throw new IllegalStateException("没有第三方用户信息");
}
ConnectionKey connectionKey = connection.getKey();
String imageUrl = connection.getImageUrl();
String displayName = connection.getDisplayName();
String providerId = connectionKey.getProviderId();
String providerUserId = connectionKey.getProviderUserId();
return new ConnectedUserInfo(imageUrl, displayName, providerId, providerUserId);
}
@Getter
@Setter
@AllArgsConstructor
private static class ConnectedUserInfo {
private String imageUrl;
private String displayName;
private String providerId;
private String providerUserId;
}
}
以上就是通过源码分析并实现第三方qq登录的总结,感兴趣的小伙伴可以根据这个思路实现微信或微博等其他第三方登录功能。