SpringSocial开发QQ授权登录实战篇

  本篇文章我们讲解基于SpringBoot2.0,SpringSocial开发QQ微信登录。 请大家阅读了OAuth协议SpringSocial基本原理后再来看本篇文章
  我们开发QQ微信的授权登录主要就是为了完成对以下组件的实现。获取用户信息封装成Connection,如何构建Connection?必须要ConnectionFactory,ConnectionFactory需要ServiceProvider和ApiAdapter,ServiceProvider又需要我们去实现API和OAuth2Operations。

1.Api开发

1.1声明获取用户信息接口

  Api主要是为了获取用户信息,因此我们第一步声明一个获取用户信息的接口。

public interface QQ {
    QQUserInfo getUserInfo();
}

1.2开发接口实现类

接口实现类继承了SpringSocial中的AbstractOAuth2ApiBinding并实现了QQ接口。

public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
    
    private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
    
    private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
    
    private String appId;
    
    private String openId;
    
    private ObjectMapper objectMapper = new ObjectMapper();
    
    public QQImpl(String accessToken, String appId) {
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
        
        this.appId = appId;
    
        String url = String.format(URL_GET_OPENID, accessToken);
        String result = getRestTemplate().getForObject(url, String.class);
        
        this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
    }
    
    /* (non-Javadoc)
     * @see com.imooc.security.core.social.qq.api.QQ#getUserInfo()
     */
    @Override
    public QQUserInfo getUserInfo() {
        
        String url = String.format(URL_GET_USERINFO, appId, openId);
        String result = getRestTemplate().getForObject(url, String.class);
        
        System.out.println(result);
        
        QQUserInfo userInfo = null;
        try {
            userInfo = objectMapper.readValue(result, QQUserInfo.class);
            userInfo.setOpenId(openId);
            return userInfo;
        } catch (Exception e) {
            throw new RuntimeException("获取用户信息失败", e);
        }
    }

}

1.2.1 AbstractOAuth2ApiBinding

再讲解这段代码之前,我们先看一下SpringSocial中AbstractOAuth2ApiBinding中的部分源码:

public abstract class AbstractOAuth2ApiBinding implements ApiBinding, InitializingBean {

    private final String accessToken;

    private RestTemplate restTemplate;

    /**
     * Constructs the API template without user authorization. This is useful for accessing operations on a provider's API that do not require user authorization.
     */
    protected AbstractOAuth2ApiBinding() {
        accessToken = null;
        restTemplate = createRestTemplateWithCulledMessageConverters();
        configureRestTemplate(restTemplate);
    }
        //省略几万字~~~

  这个抽象类里面声明了一个accessToken和一个restTemplate,授权登录中,获取每一个QQ用户的openId和登录信息都需要一个token,而且每个人的token不可能都一样,因此我们的QQImpl不能够是单例的。restTemplate主要用于发起rest请求。

1.2.2 QQImpl成员变量及构造函数

  回到我们的代码,我们要获取用户信息要走两个步骤,第一步首先根据token获取用户的openId,第二步,根据openId获取到用户的具体信息。因此我们声明了URL_GET_OPENID,这个是QQ提供的获取用户OPENID的URL,URL_GET_USERINFO也是QQ提供的获取用户具体信息的URL。appId是我们接入QQ时分配的ID,openId存储了获取到的用户openId。构造函数中,我们调用了父类的构造方法,最主要的是第二个参数TokenStrategy.ACCESS_TOKEN_PARAMETER,表示我们去获取openID的时候,token是放在请求参数中的,父类默认是放在请求头里面的,所以我们这里必须这么写一下。然后还使用了父类的restTemplate去获取用户的openId。完成了第一步初始化。

1.2.3 QQImpl的gerUserInfo

  这个方法使用openId和appId,发起rest请求从QQ那里获取到用户的具体信息,然后封装成QQUserInfo对象。

1.3QQUserInfo

这个就是把从QQ接收到的用户信息字段映射到的一个类中,具体的get和set方法就不写了。

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;
    private String figureurl_type;

    /**
     * 大小为50×50像素的QQ空间头像URL。
     */
    private String figureurl_1;
    /**
     * 大小为100×100像素的QQ空间头像URL。
     */
    private String figureurl_2;

    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;
}

OK,Api模块我们就开发完成了

2 OAuthOperations开发

2.1 QQOAuth2Template

为什么我们不直接使用OAuth2Template呢,而要自己去实现一个QQOAuth2Template?这是因为QQ返回回来的信息中是text/html格式的,OAuth2Template没有支持处理这种类型的请求,所以我们必须要自己手动定义一个

public class QQOAuth2Template extends OAuth2Template {
    
    private Logger logger = LoggerFactory.getLogger(getClass());

    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
    }
    
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
        
        logger.info("获取accessToke的响应:"+responseStr);
        
        String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
        
        String accessToken = StringUtils.substringAfterLast(items[0], "=");
        Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
        String refreshToken = StringUtils.substringAfterLast(items[2], "=");
        
        return new AccessGrant(accessToken, null, refreshToken, expiresIn);
    }
    
    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }

}

  createRestTemplate方法添加了一个StringHttpMessageConverter,这样我们就可以成功的将服务提供商返回的信息转换成对应的对象了。
  那为什么我们还要重写postForAccessGrant方法呢?我们会在后面讲解。
  OAuthOperations开发就算开发完成了,那么我们相当于凑齐了ServiceProvider的两大组件了,我们可以做ServiceProvider的实现了。

3 ServiceProvider开发

  ServiceProvider抛开API组件,主要是完成了OAuth中的步骤1-5,步骤1-5不清楚的同学可以看我之前写的文章。

3.1 QQServiceProvider

public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {

    private String appId;
    
    private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
    
    private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
    
    public QQServiceProvider(String appId, String appSecret) {
        super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
        this.appId = appId;
    }
    
    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken, appId);
    }

}

  QQServiceProvider继承了SpringSocial提供的AbstractOAuth2ServiceProvider。这是一个泛型的抽象类,泛型中需要我们提供获取用户信息的接口,那这个就是我们第一步中声明的QQ。

3.1.1 构造函数

  调用了父类的构造方法,并且参数是我们自己声明的QQOAuth2Template,其中appId和appSecret是QQ分配给我们的,URL_AUTHORIZE表示我们引导用户跳转的授权页面,对应步骤1。URL_ACCESS_TOKEN对应步骤4,表示去获取token。getApi是ServiceProvider用来获取用户信息需要的方法。我们刚才说了每个用户的token是不一样的,而且token是有期限的。所以我们不能够直接把QQImpl声明为单例的,这里必须要new一个出来。
  我们的ServiceProvider就开发完成了。

4 ApiAdapter

这个主要是用于适配作用的,试想,我们从QQ、微信、微博中获取到的UserInfo信息是五花八门的,但是我们的Connection想要的信息就那么多,如何适配呢?很简单直接继承SpringSocial提供的ApiAdapter

public class QQAdapter implements ApiAdapter<QQ> {

    @Override
    public boolean test(QQ api) {
        return true;
    }

    @Override
    public void setConnectionValues(QQ api, ConnectionValues values) {
        QQUserInfo userInfo = api.getUserInfo();
        
        values.setDisplayName(userInfo.getNickname());
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        values.setProfileUrl(null);
        values.setProviderUserId(userInfo.getOpenId());
    }

    @Override
    public UserProfile fetchUserProfile(QQ api) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void updateStatus(QQ api, String message) {
        //do noting
    }

}

4.1 test方法

  表示是否和QQ还能连接通畅,我们这里直接返回true

4.2 setConnectionValues方法

  这里就是真正做适配的地方,我们把QQUserInfo,转换成了ConnectionValues。
  当我们完成了适配和Serviceprovider后,我们就可以开始构造我们的ConnectionFactory了。

5 OAuth2ConnectionFactory

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
    public QQConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
    }
}

这里的providerId表示服务提供商的唯一标识。OAuth2ConnectionFactory的构造非常简单,然后SpringSocial就可以利用它创建Connection了,创建好后,就会使用UsersConnectionRepository来将Connection存储到DBUserConnection中去。UsersConnectionRepository已经由SpringSocail提供了一个JdbcUsersConnectionRepository了,所以我们只需要做一个配置即可。

6 配置SocialConfig

@Configuration
@EnableSocial
@ConditionalOnProperty(prefix = "imooc.security.social.qq", name = "app-id")
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private SecurityProperties securityProperties;
    
    @Autowired(required = false)
    private ConnectionSignUp connectionSignUp;

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
                connectionFactoryLocator, Encryptors.noOpText());
        repository.setTablePrefix("imooc_");
        if(connectionSignUp != null) {
            repository.setConnectionSignUp(connectionSignUp);
        }
        return repository;
    }

    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
        ImoocSpringSocialConfigurer configurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
        configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
        return configurer;
    }

    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
        return new ProviderSignInUtils(connectionFactoryLocator,
                getUsersConnectionRepository(connectionFactoryLocator)) {
        };
    }
    
    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer,
            Environment environment) {
        super.addConnectionFactories(connectionFactoryConfigurer, environment);
        QQProperties qqConfig = securityProperties.getSocial().getQq();
        WeixinProperties weixinConfig = securityProperties.getSocial().getWeixin();
        connectionFactoryConfigurer.addConnectionFactory(
                new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret()));
        connectionFactoryConfigurer.addConnectionFactory(new WeixinConnectionFactory(weixinConfig.getProviderId(),
                weixinConfig.getAppId(), weixinConfig.getAppSecret()));

    }
    
    @Override
    public UserIdSource getUserIdSource() {
        return new AuthenticationNameUserIdSource();
    }
}

6.1 成员变量解析

  DataSource表示数据源,repository会使用到
  connectionSignUp注册配置,详情请见下面的第9部分拓展部分
  SecurityProperties,存放QQ和微信的配置,其中的成员变量包含了appId和appSecret

6.2 getUsersConnectionRepository

  这里我们使用了SpringSocial默认提供的JdbcUsersConnectionRepository,第一个参数是dataSource,第二个参数connectionFactoryLocator可以帮我们选择对应的ConnectionFactory,我们这里只写了一个QQ登录,只有一个QQ的ConnectionFactory,但是下一篇文章中,我们写微信登录的时候,那么就会有多的ConnectionFactory了。Encryptors.noOpText()表示我们对数据不加密,这里我们只是为了演示使用,实际上我们的数据入库的时候,对于access_token这类敏感信息还是需要加密的。

6.3 addConnectionFactories

  把我们的QQFactory和WeixinFactory加进去对于微信的实现我们下一篇文章讲,这里先卖个关子

6.4 getUserIdSource

  这个也是SpringBoot2里面最恶心的地方了,在1.5的版本中是没有这个东西了,这个方法主要是被Filter调用的,我们就返回SpringSocial默认提供的实现就可以了。

6.5 建立表

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);

  这段SQL语句其实是Spring-social-core包提供的,直接使用就可以了。userId就是我们的业务系统userId,providerId表示服务提供商Id,providerUserId就是openId。在我们进行QQ登录的时候,我们会根据providerId和providerUserId拿到userId,然后通过userId拿到用户的具体业务信息。其实SpringSocial也提供了一个接口SocialUserDetailsService,帮助我们获取user的信息,所以我们也让MyUserDetailsService实现了SocialUserDetailsService接口:

@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());
    
    @Autowired
    private PasswordEncoder passwordEncoder;

    /*
     * (non-Javadoc)
     * 
     * @see org.springframework.security.core.userdetails.UserDetailsService#
     * loadUserByUsername(java.lang.String)
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("表单登录用户名:" + username);
        return buildUser(username);
    }

    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        logger.info("设计登录用户Id:" + userId);
        return buildUser(userId);
    }

    private SocialUserDetails buildUser(String userId) {
        // 根据用户名查找用户信息
        //根据查找到的用户信息判断用户是否被冻结
        String password = passwordEncoder.encode("123456");
        logger.info("数据库密码是:"+password);
        return new SocialUser(userId, password,
                true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }

}

6.6 SecurityProperties

@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
    
    private BrowserProperties browser = new BrowserProperties();
    
    private ValidateCodeProperties code = new ValidateCodeProperties();
    
    private SocialProperties social = new SocialProperties();

    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }

    public ValidateCodeProperties getCode() {
        return code;
    }

    public void setCode(ValidateCodeProperties code) {
        this.code = code;
    }

    public SocialProperties getSocial() {
        return social;
    }

    public void setSocial(SocialProperties social) {
        this.social = social;
    }
    
}

6.7 SocialProperties

public class SocialProperties {
    
    private String filterProcessesUrl = "/auth";

    private QQProperties qq = new QQProperties();
    
    private WeixinProperties weixin = new WeixinProperties();

    public QQProperties getQq() {
        return qq;
    }

    public void setQq(QQProperties qq) {
        this.qq = qq;
    }

    public String getFilterProcessesUrl() {
        return filterProcessesUrl;
    }

    public void setFilterProcessesUrl(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    public WeixinProperties getWeixin() {
        return weixin;
    }

    public void setWeixin(WeixinProperties weixin) {
        this.weixin = weixin;
    }
}

7 过滤器配置

我们把所有的组件都已经完成好了,接下来就是配置我们的过滤器了

7.1配置SpringSocialConfigurer

我们在配置6中有这么一段代码

 @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
        ImoocSpringSocialConfigurer configurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
        configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
        return configurer;
    }

这里的ImoocSpringSocialConfigurer继承了SpringSocialConfigurer

public class ImoocSpringSocialConfigurer extends SpringSocialConfigurer {
    
    private String filterProcessesUrl;
    
    public ImoocSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }
    
    @SuppressWarnings("unchecked")
    @Override
    protected <T> T postProcess(T object) {
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(filterProcessesUrl);
        return (T) filter;
    }

}

  继承的SpringSocialConfigurer其实生成了一个SocialAuthenticationFilter,而这个Filter默认只会拦截/auth开头的请求路径,为了满足我们的需求,所以我们这里使用了filter.setFilterProcessesUrl(filterProcessesUrl);来改变默认的配置。为什么是在setFilterProcessesUrl配置呢?因为SpringSocialConfigurer在把Filter放到过滤器链之前会调用一个postProcess方法,所以我们在调用父类的postProcess之后,我们就在这里设置processingUrl。除此之外,我们的请求跳转还涉及到providerId,这个providerId也是我们实现跳转和授权完成之后回调的关键。我们自定义Filter拦截/qqLogin/callback.do的请求会跳转到QQ登录,因此ConnectionProvider的providerId也必须配置成callback.do,filterProcessesUrl为qqLogin。
 configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());是因为如果在DB中没有获取到userId则会跳转到默认的signUp页面,这里我们需要自定义一个跳转的注册路径。

7.2 加入配置

@Configuration
public class BrowserSecurityConfig extends AbstractChannelSecurityConfig {

    @Autowired
    private SecurityProperties securityProperties;
    
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    
    @Autowired
    private ValidateCodeSecurityConfig validateCodeSecurityConfig;
    
    @Autowired
    private SpringSocialConfigurer imoocSocialSecurityConfig;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        applyPasswordAuthenticationConfig(http);
        
        http.apply(validateCodeSecurityConfig)
                .and()
            .apply(smsCodeAuthenticationSecurityConfig)
                .and()
            .apply(imoocSocialSecurityConfig)
                .and()
            .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                .userDetailsService(userDetailsService)
                .and()
            .authorizeRequests()
                .antMatchers(
                    SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                    SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
                    securityProperties.getBrowser().getLoginPage(),
                    SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*",
                    securityProperties.getBrowser().getSignUpUrl(),
                    "/user/regist")
                    .permitAll()
                .anyRequest()
                .authenticated()
                .and()
            .csrf().disable();
        
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

最主要的就是apply(imoocSocialSecurityConfig)配置

7.3 OAuth2AuthenticationService

其实这个类不是编写的代码,它是SocialAuthenticationFilter的关键service,在这里我们主要是简单讲讲它的原理。这个service中有一个getAuthToken方法:

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
        String code = request.getParameter("code");
        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)) {
            try {
                String returnToUrl = buildReturnToUrl(request);
                AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
                // TODO avoid API call if possible (auth using token would be fine)
                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;
        }
    }

  因为第三步中QQ会返回授权码code,如果发现没有这个code,SpringSocial会认为是第一步,也就是要去授权认证,因此会作重定向。但是如果我们发现我们得到了Code,这个时候我们就会利用code去拿token信息。在这里拿token的时候,会使用到我们第二步中自己实现的template来转换对应的信息。
  细心的读者会发现,我在讲解第二步的时候说postForAccessGrant被重写了,为什么会被重写呢?这是因为默认的OAuth2Template是这样解析QQ返回回来的参数的:

private AccessGrant extractAccessGrant(Map<String, Object> result) {
        return createAccessGrant((String) result.get("access_token"), (String) result.get("scope"), (String) result.get("refresh_token"), getIntegerValue(result, "expires_in"), result);
    }

但是QQ返回回来的并不是一个JSON串,而且字段也不是这样的,所以我们需要自己实现一个Template来专门解析这个返回的token信息。
为什么我们还要在QQOAuth2Template设置一个setUseParametersForClientAuthentication(true)呢?我们还是在默认的OAuth2Template中看答案:

public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> additionalParameters) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
        if (useParametersForClientAuthentication) {
            params.set("client_id", clientId);
            params.set("client_secret", clientSecret);
        }
        params.set("code", authorizationCode);
        params.set("redirect_uri", redirectUri);
        params.set("grant_type", "authorization_code");
        if (additionalParameters != null) {
            params.putAll(additionalParameters);
        }
        return postForAccessGrant(accessTokenUrl, params);
    }

默认的useParametersForClientAuthentication为false,这样我们去获取token的时候,就不会带上appId和appSecret,所以我们这里必须要设置好!
  在我们拿到Token之后,Filter会拿着这个Token选择一个Provider做验证,Provider会使用Repository到数据库中获取userId,如果获取不到,就会跳转到signUp页面,提示用户去注册。而且在跳转之前我们可以看到,session中已经存放了connection的信息,具体步骤如下:


第1步:根据得到的token去验证

第2步:provider中通repostitory去拿对应的userId

第3步:跳转到注册页面

基于此我们还需要设置一个注册页面,请见7.4章节

7.4 注册页面

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
    <h2>Demo注册页</h2>
    
    <form action="/user/regist" method="post">
        <table>
            <tr>
                <td>用户名:</td> 
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码:</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td colspan="2">
                    <button type="submit" name="type" value="regist">注册</button>
                    <button type="submit" name="type" value="binding">绑定</button>
                </td>
            </tr>
        </table>
    </form>
</body>
</html>

7.5 注册逻辑

@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private ProviderSignInUtils providerSignInUtils;
    
    @PostMapping("/regist")
    public void regist(User user, HttpServletRequest request) {
        
        //不管是注册用户还是绑定用户,都会拿到一个用户唯一标识。
        String userId = user.getUsername();
        providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
    }
}

ProviderSignInUtils是SpringSocial提供的一个工具类,帮助我们把用户信息绑定到对应的账户中去。

    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
        return new ProviderSignInUtils(connectionFactoryLocator,
                getUsersConnectionRepository(connectionFactoryLocator)) {
        };
    }

8 前端页面

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
    <h2>标准登录页面</h2>
    <h3>社交登录</h3>
    <a href="/qqLogin/callback.do">QQ登录</a>
    &nbsp;&nbsp;&nbsp;&nbsp;
    <a href="/qqLogin/weixin">微信登录</a>
</body>
</html>

9 扩展

  有时候我们发现在用第三方授权登录后,不需要用户在注册信息,那这个是怎么实现的呢?我们先看看SocialAuthenticationProvider源码



在toUserId中我们刚才找不到用户的时候直接跳转到了注册页面,原因是Repostiry在寻找userId的时候的逻辑是这样的:



如果找不到用户且connectionSignUp为空了,就会返回一个空的List,但是如果我们实现一个connectionSignUp,这里就可以根据我们的根据我们的逻辑把这个userId放到数据库中去,所以我们这里要自己实现一个connectionSignUp。

9.1 DemoConnectionSignUp

@Component
public class DemoConnectionSignUp implements ConnectionSignUp {

    /* (non-Javadoc)
     * @see org.springframework.social.connect.ConnectionSignUp#execute(org.springframework.social.connect.Connection)
     */
    @Override
    public String execute(Connection<?> connection) {
        //根据社交用户信息默认创建用户并返回用户唯一标识
        return connection.getDisplayName();
    }

}

这样配置后,我们即使在数据库中没有该用户也能够拿到用户信息

10 实战



QQ授权登录我们就开发到这里,下一章节我们将大致讲解一下微信的登录配置流程。

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

推荐阅读更多精彩内容