Spring security social源码分析及第三方QQ登录实践

有了前面几篇针对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登录的总结,感兴趣的小伙伴可以根据这个思路实现微信或微博等其他第三方登录功能。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容