SpringSecurity使用Spring Social开发第三方登录
in JavaDevelop with 0 comment

SpringSecurity使用Spring Social开发第三方登录

in JavaDevelop with 0 comment

使用Spring Social开发第三方登录

从本章开始进入了oauth协议;这里的第三方登录也是oauth协议;

OAuth协议简介

OAuth要解决的问题

考虑这样一个场景:假如开发一个APP-慕课微信图片美化,需要用户存储在微信上的数据;

传统的做法是:拿到用户的用户名密码
问题:

OAuth要使用Token令牌来解决以上的问题

OAuth协议中的各个角色

OAuth运行流程

同意授权是关键;只有同意了才会有接下来的流程

OAuth提供了4中授权方式

授权码模式流程

该模式与其他三种模式不同,最重要的区别就是:同意授权这个动作,是在认证服务器上完成的,而其他的三种都是在第三方应用上完成的。

该模式是4中模式中最严格最完整的一种协议

简化模式

在授权后直接带回令牌

SpringSocial简介

spring security 与 oath social的关系

还是上次那个图片示例,如果只是获取微信的昵称头像信息,

在spring security中被认定为认证成功的标志是:根据用户信息构建Authentication放入SecurityContext中

所以只要引导用户走完oath的所有流程。最后根据用户信息构建Authentication放入SecurityContext中

social的原理:
基于我们之前学习过的过滤链原理,在过滤器链上增加了一个 SocialAuthenticationFilter,

拦截到有需要第三方登录的请求则开始引导完成所有的流程,就完成了第三方登录

social基本概念和原理

之前1-5步都是协议化流程步骤

这里只是介绍的是与我们要写代码相关的流程;实现这些节点就可以运行了。

问题:

官网 : https://projects.spring.io/spring-social/

官网中页面信息提供了

该课程会接收QQ和微信登录

QQ登录上

根据上面接收的原理,开发思路是:

最终我们是要获取到Connection,

那么就需要使用ConnectionFactory创建

而ConnectionFactory又需要ServiceProvider和ApiAdapter;

倒退流程,走到最后就是先实现Api。

http://wiki.connect.qq.com/api列表

开发Api对象

package cn.mrcode.imooc.springsecurity.securitycore.qq.api;

import java.io.IOException;

public interface QQ {
    QQUserInfo getUserInfo() throws IOException;
}
import org.springframework.social.oauth2.TokenStrategy;

import java.io.IOException;

/**
 * 构建与qq交互的api实例;完成的是第6步
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 0:46
 */
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
    // http://wiki.connect.qq.com/%E8%8E%B7%E5%8F%96%E7%94%A8%E6%88%B7openid_oauth2-0
    public final static String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
    // 父类会自动携带accessToken
    public final static String URL_GET_USER_INFO = "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) {
        // 该语句代码查看继承类的源码得知
        // 默认是把accessToken放入请求头中
        // qqapi的文档确是放在参数中传递的
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
        this.appId = appId;
        String url = String.format(URL_GET_OPENID, accessToken);
        //callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
        String result = getRestTemplate().getForObject(url, String.class);
        System.out.println(result);
        this.openid = StringUtils.substringBetween("\"openid\":", "}");
    }

    @Override
    public QQUserInfo getUserInfo() throws IOException {
        String url = String.format(URL_GET_USER_INFO, appId, openid);
        String result = getRestTemplate().getForObject(url, String.class);
        System.out.println(result);
        QQUserInfo qqUserInfo = objectMapper.readValue(result, QQUserInfo.class);
        return qqUserInfo;
    }
}

package cn.mrcode.imooc.springsecurity.securitycore.qq.api;

/**
 * qq用户信息:
 * http://wiki.connect.qq.com/get_user_info
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 0:45
 */
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;
    /**
     * 大小为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;

开发服务提供商

package cn.mrcode.imooc.springsecurity.securitycore.qq.connet;

import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQ;
import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Template;

/**
 * 服务提供商:
 * 官网地址可以获取 authorizeUrl 和 accessTokenUrl
 * http://wiki.connect.qq.com/%E5%BC%80%E5%8F%91%E6%94%BB%E7%95%A5_server-side
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 1:20
 */
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
    public static final String authorizeUrl = "https://graph.qq.com/oauth2.0/authorize";
    public static final String accessTokenUrl = "https://graph.qq.com/oauth2.0/token";
    private String appId;

    /**
     * Create a new {@link OAuth2ServiceProvider}.
     */
    public QQServiceProvider(String appId, String secret) {
        // OAuth2Operations 有一个默认实现类,可以使用这个默认实现类
        // oauth2的一个流程服务
        super(new OAuth2Template(appId, secret, authorizeUrl, accessTokenUrl));
    }

    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken, appId);
    }
}

对于social基本概念和原理中的图上右侧的服务提供商的对象已经开发完成

QQ登录

上一章节完成了 ServiceProvider的功能,这一节完成应用内部的需要做的一些功能

注意看这个官网文档: https://docs.spring.io/spring-social/docs/1.1.x/
由于在spring-boot-autoconfigure-2.0.4.RELEASE.jar没有对 social的自动配置了
所以我搞这节课的连通流程花费了5个小时,最后认证查看官网文档的说明才跑起来

实现 ConnectionFactory

package cn.mrcode.imooc.springsecurity.securitycore.qq.connet;

import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQ;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth2.GenericOAuth2ConnectionFactory;

/**
 * qq
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 9:02
 * @see GenericOAuth2ConnectionFactory 模仿这个来写
 */
public class QQOAuth2ConnectionFactory extends OAuth2ConnectionFactory<QQ> {
    /**
     * 唯一的构造函数,需要
     * Create a {@link OAuth2ConnectionFactory}.
     * @param providerId 服务商id;自定义字符串;也是后面添加social的过滤,过滤器帮我们拦截的url其中的某一段地址
     *                   on} interface.
     */
    public QQOAuth2ConnectionFactory(String providerId, String appid, String secret) {
      // 传递进来是因为使用该服务的地方才知道  这些参数是什么
        /**
         * serviceProvider 用于执行授权流和获取本机服务API实例的ServiceProvider模型
         * apiAdapter      适配器,用于将不同服务提供商的个性化用户信息映射到 {@link Connection}
         */
        super(providerId, new QQServiceProvider(appid, secret), new QQApiAdapter());
    }
}

这里需要提供一个 ApiAdapter

QQApiAdapter

package cn.mrcode.imooc.springsecurity.securitycore.qq.connet;

import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQ;
import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;

/**
 * 适配器,用于将不同服务提供商的个性化用户信息映射到 {@link Connection}
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 9:10
 */
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.getUserInfo();
        values.setDisplayName(userInfo.getNickname());
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        values.setProfileUrl(null); // 主页地址,像微博一般有主页地址
        // 服务提供商返回的该user的openid
        // 一般来说这个openid是和你的开发账户也就是appid绑定的
        values.setProviderUserId(userInfo.getOpenId());
    }

    @Override
    public UserProfile fetchUserProfile(QQ api) {
        // 暂时不知道有什么用处
        return UserProfile.EMPTY;
    }

    @Override
    public void updateStatus(QQ api, String message) {
        // 应该是退出的状态操作。
    }
}

开启并配置串联之前写的功能组件

/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.qq;

import cn.mrcode.imooc.springsecurity.securitycore.qq.config.QQAutoConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.UserIdSource;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ConnectController;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import org.springframework.social.security.SpringSocialConfigurer;

import javax.sql.DataSource;

/**
 * @author zhailiang
 */
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        // 指定表前缀,后缀是固定的,在JdbcUsersConnectionRepository所在位置
        repository.setTablePrefix("imooc_");
        return repository;
    }

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

    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        // 默认配置类,进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
        return springSocialConfigurer;
    }

    //https://docs.spring.io/spring-social/docs/1.1.x-SNAPSHOT/reference/htmlsingle/#creating-connections-with-connectcontroller
    // 这个在目前阶段不是必须的,
    // 之前不知道为什么就是没有响应
    // 可以暂时忽略该配置
    @Bean
    public ConnectController connectController(
            ConnectionFactoryLocator connectionFactoryLocator,
            ConnectionRepository connectionRepository) {
        return new ConnectController(connectionFactoryLocator, connectionRepository);
    }
}

表创建的sql在 JdbcUsersConnectionRepository类所在位置

-- This SQL contains a "create table" that can be used to create a table that JdbcUsersConnectionRepository can persist
-- connection in. It is, however, not to be assumed to be production-ready, all-purpose SQL. It is merely representative
-- of the kind of table that JdbcUsersConnectionRepository works with. The table and column names, as well as the general
-- column types, are what is important. Specific column types and sizes that work may vary across database vendors and
-- the required sizes may vary across API providers.

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

在 SpringSocialConfigurer 中需要注入 SocialUserDetailsService,之前我们有写好的,改造一下

package cn.mrcode.imooc.springsecurity.securitybrowser;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.security.SocialUser;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/3 9:16
 * @date 2018/8/3 9:16
 * @since 1.0
 */
// 自定义数据源来获取数据
// 这里只要是存在一个自定义的 UserDetailsService ,那么security将会使用该实例进行配置
@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 可以从任何地方获取数据
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名查找用户信息
        logger.info("登录用户名:{}", username);
        // 写死一个密码,赋予一个admin权限
//        User admin = new User(username, "{noop}123456",
//                              AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        return getUserDetails(username);
    }


    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        logger.info("登录用户名:{}", userId);
        return getUserDetails(userId);
    }

    private SocialUser getUserDetails(String username) {
        String password = passwordEncoder.encode("123456");
        logger.info("数据库密码{}", password);
        SocialUser admin = new SocialUser(username,
//                              "{noop}123456",
                password,
                true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        return admin;
    }
}

提供qq服务的配置

这个单独拿出来来。方便自动配置和切换

/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.properties;

/**
 * 没有默认值;由使用方注入
 * @author zhailiang
 */
public class QQProperties {
    /**
     * Application id.
     */
    private String appId;

    /**
     * Application secret.
     */
    private String appSecret;
    private String providerId = "qq";
/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.properties;

/**
 * @author zhailiang
 *
 */
public class SocialProperties {

	private QQProperties qq = new QQProperties();
package cn.mrcode.imooc.springsecurity.securitycore.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/3 15:28
 * @date 2018/8/3 15:28
 * @since 1.0
 */
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
    /** imooc.security.browser 路径下的配置会被映射到该配置类中 */
    private BrowserProperties browser = new BrowserProperties();
    private ValidateCodeProperties code = new ValidateCodeProperties();
    private SocialProperties social = new SocialProperties();

上面的代码是为了提供配置功能,和之前这些配置一样的思路

下面的配置是为qq登录提供服务商

package cn.mrcode.imooc.springsecurity.securitycore.qq.config;

import cn.mrcode.imooc.springsecurity.securitycore.properties.QQProperties;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import cn.mrcode.imooc.springsecurity.securitycore.qq.connet.QQOAuth2ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactory;

/**
 * autoconfigure2.04中已经不存在social的自动配置类了
 * org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 9:20
 */
@Configuration
// 当配置了app-id的时候才启用
@ConditionalOnProperty(prefix = "imooc.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer configurer,
                                       Environment environment) {
        configurer.addConnectionFactory(createConnectionFactory());
    }

    public ConnectionFactory<?> createConnectionFactory() {
        QQProperties qq = securityProperties.getSocial().getQq();
        return new QQOAuth2ConnectionFactory(qq.getProviderId(), qq.getAppId(), qq.getAppSecret());
    }

    // 后补:做到处理注册逻辑的时候发现的一个bug:登录完成后,数据库没有数据,但是再次登录却不用注册了
    // 就怀疑是否是在内存中存储了。结果果然发现这里父类的内存ConnectionRepository覆盖了SocialConfig中配置的jdbcConnectionRepository
    // 这里需要返回null,否则会返回内存的 ConnectionRepository
  @Override
  public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
      return null;
  }
}

浏览器项目中的配置,需要使用apply把开启social的配置文件加入

cn.mrcode.imooc.springsecurity.securitybrowser.BrowserSecurityConfig

// 这里目前注入的其实就是 之前写的开启social的配置类SocialConfig
@Autowired
  private SpringSocialConfigurer imoocSocialSecurityConfig;

  .apply(imoocSocialSecurityConfig)

最后注意把 “/auth/*” 路径放行;

页面提供qq登录地址

<h3>社交登录</h3>
<!--不支持get请求-->
<form action="/auth/qq" method="post">
    <button type="submit">QQ登录</button>
</form>

实现 ConnectionFactory

上一章节完成了 ServiceProvider的功能,这一节完成应用内部的需要做的一些功能

注意看这个官网文档: https://docs.spring.io/spring-social/docs/1.1.x/
由于在spring-boot-autoconfigure-2.0.4.RELEASE.jar没有对 social的自动配置了

package cn.mrcode.imooc.springsecurity.securitycore.qq.connet;

import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQ;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth2.GenericOAuth2ConnectionFactory;

/**
 * qq
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 9:02
 * @see GenericOAuth2ConnectionFactory 模仿这个来写
 */
public class QQOAuth2ConnectionFactory extends OAuth2ConnectionFactory<QQ> {
    /**
     * 唯一的构造函数,需要
     * Create a {@link OAuth2ConnectionFactory}.
     * @param providerId 服务商id;自定义字符串;也是后面添加social的过滤,过滤器帮我们拦截的url其中的某一段地址
     *                   on} interface.
     */
    public QQOAuth2ConnectionFactory(String providerId, String appid, String secret) {
      // 传递进来是因为使用该服务的地方才知道  这些参数是什么
        /**
         * serviceProvider 用于执行授权流和获取本机服务API实例的ServiceProvider模型
         * apiAdapter      适配器,用于将不同服务提供商的个性化用户信息映射到 {@link Connection}
         */
        super(providerId, new QQServiceProvider(appid, secret), new QQApiAdapter());
    }
}

这里需要提供一个 ApiAdapter

QQApiAdapter

package cn.mrcode.imooc.springsecurity.securitycore.qq.connet;

import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQ;
import cn.mrcode.imooc.springsecurity.securitycore.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;

/**
 * 适配器,用于将不同服务提供商的个性化用户信息映射到 {@link Connection}
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 9:10
 */
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.getUserInfo();
        values.setDisplayName(userInfo.getNickname());
        values.setImageUrl(userInfo.getFigureurl_qq_1());
        values.setProfileUrl(null); // 主页地址,像微博一般有主页地址
        // 服务提供商返回的该user的openid
        // 一般来说这个openid是和你的开发账户也就是appid绑定的
        values.setProviderUserId(userInfo.getOpenId());
    }

    @Override
    public UserProfile fetchUserProfile(QQ api) {
        // 暂时不知道有什么用处
        return UserProfile.EMPTY;
    }

    @Override
    public void updateStatus(QQ api, String message) {
        // 应该是退出的状态操作。
    }
}

开启并配置串联之前写的功能组件

/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.qq;

import cn.mrcode.imooc.springsecurity.securitycore.qq.config.QQAutoConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.UserIdSource;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ConnectController;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import org.springframework.social.security.SpringSocialConfigurer;

import javax.sql.DataSource;

/**
 * @author zhailiang
 */
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        // 指定表前缀,后缀是固定的,在JdbcUsersConnectionRepository所在位置
        repository.setTablePrefix("imooc_");
        return repository;
    }

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

    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        // 默认配置类,进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
        return springSocialConfigurer;
    }

    //https://docs.spring.io/spring-social/docs/1.1.x-SNAPSHOT/reference/htmlsingle/#creating-connections-with-connectcontroller
    // 这个在目前阶段不是必须的,
    // 之前不知道为什么就是没有响应
    // 可以暂时忽略该配置
    @Bean
    public ConnectController connectController(
            ConnectionFactoryLocator connectionFactoryLocator,
            ConnectionRepository connectionRepository) {
        return new ConnectController(connectionFactoryLocator, connectionRepository);
    }
}

表创建的sql在 JdbcUsersConnectionRepository类所在位置

-- This SQL contains a "create table" that can be used to create a table that JdbcUsersConnectionRepository can persist
-- connection in. It is, however, not to be assumed to be production-ready, all-purpose SQL. It is merely representative
-- of the kind of table that JdbcUsersConnectionRepository works with. The table and column names, as well as the general
-- column types, are what is important. Specific column types and sizes that work may vary across database vendors and
-- the required sizes may vary across API providers.

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

在 SpringSocialConfigurer 中需要注入 SocialUserDetailsService,之前我们有写好的,改造一下

package cn.mrcode.imooc.springsecurity.securitybrowser;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.security.SocialUser;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/3 9:16
 * @date 2018/8/3 9:16
 * @since 1.0
 */
// 自定义数据源来获取数据
// 这里只要是存在一个自定义的 UserDetailsService ,那么security将会使用该实例进行配置
@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 可以从任何地方获取数据
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名查找用户信息
        logger.info("登录用户名:{}", username);
        // 写死一个密码,赋予一个admin权限
//        User admin = new User(username, "{noop}123456",
//                              AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        return getUserDetails(username);
    }


    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        logger.info("登录用户名:{}", userId);
        return getUserDetails(userId);
    }

    private SocialUser getUserDetails(String username) {
        String password = passwordEncoder.encode("123456");
        logger.info("数据库密码{}", password);
        SocialUser admin = new SocialUser(username,
//                              "{noop}123456",
                password,
                true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        return admin;
    }
}

提供qq服务的配置

这个单独拿出来来。方便自动配置和切换

/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.properties;

/**
 * 没有默认值;由使用方注入
 * @author zhailiang
 */
public class QQProperties {
    /**
     * Application id.
     */
    private String appId;

    /**
     * Application secret.
     */
    private String appSecret;
    private String providerId = "qq";
/**
 *
 */
package cn.mrcode.imooc.springsecurity.securitycore.properties;

/**
 * @author zhailiang
 *
 */
public class SocialProperties {

	private QQProperties qq = new QQProperties();
package cn.mrcode.imooc.springsecurity.securitycore.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * ${desc}
 * @author zhuqiang
 * @version 1.0.1 2018/8/3 15:28
 * @date 2018/8/3 15:28
 * @since 1.0
 */
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
    /** imooc.security.browser 路径下的配置会被映射到该配置类中 */
    private BrowserProperties browser = new BrowserProperties();
    private ValidateCodeProperties code = new ValidateCodeProperties();
    private SocialProperties social = new SocialProperties();

上面的代码是为了提供配置功能,和之前这些配置一样的思路

下面的配置是为qq登录提供服务商

package cn.mrcode.imooc.springsecurity.securitycore.qq.config;

import cn.mrcode.imooc.springsecurity.securitycore.properties.QQProperties;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import cn.mrcode.imooc.springsecurity.securitycore.qq.connet.QQOAuth2ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactory;

/**
 * autoconfigure2.04中已经不存在social的自动配置类了
 * org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 9:20
 */
@Configuration
// 当配置了app-id的时候才启用
@ConditionalOnProperty(prefix = "imooc.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer configurer,
                                       Environment environment) {
        configurer.addConnectionFactory(createConnectionFactory());
    }

    public ConnectionFactory<?> createConnectionFactory() {
        QQProperties qq = securityProperties.getSocial().getQq();
        return new QQOAuth2ConnectionFactory(qq.getProviderId(), qq.getAppId(), qq.getAppSecret());
    }

    // 后补:做到处理注册逻辑的时候发现的一个bug:登录完成后,数据库没有数据,但是再次登录却不用注册了
    // 就怀疑是否是在内存中存储了。结果果然发现这里父类的内存ConnectionRepository覆盖了SocialConfig中配置的jdbcConnectionRepository
    // 这里需要返回null,否则会返回内存的 ConnectionRepository
  @Override
  public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
      return null;
  }
}

浏览器项目中的配置,需要使用apply把开启social的配置文件加入

cn.mrcode.imooc.springsecurity.securitybrowser.BrowserSecurityConfig

// 这里目前注入的其实就是 之前写的开启social的配置类SocialConfig
@Autowired
  private SpringSocialConfigurer imoocSocialSecurityConfig;

  .apply(imoocSocialSecurityConfig)

最后注意把 “/auth/*” 路径放行;

页面提供qq登录地址

<h3>社交登录</h3>
<!--不支持get请求-->
<form action="/auth/qq" method="post">
    <button type="submit">QQ登录</button>
</form>

本地调试qq互联应用

前面把所有的代码组件都弄好了。现在可以开启调试了

在这之前你需要有一个qq互联的应用;也就是为了拿到appid和appSecret;自己去qq互联创建一个应用即可

这里讲下本地怎么调试应用

申请好应用。并配置好

imooc:
  security:
    browser:
#      loginPage: /demo-signIn.html
#      loginType: REDIRECT
      loginType: JSON
    code:
      image:
        width: 100
        height: 50
        url: /order,/user/*
    social:
      qq:
        app-id: xxx
        app-secret: xxx

启动项目,qq登录,发现跳转到了qq登录的界面,登录完成后默认会跳转回 /signin地址

如果提示你回调地址验证失败;那么请验证,在qq互联的应用里面网站和回调地址是否设置好;

这是官网的教程:http://wiki.connect.qq.com/回调地址常见问题及修改方法

我的网站地址是:http://mrcode.cn/
回调地址就要写:http://mrcode.cn/
之前可以写根语名,但是现在不行了,必须写一个具体的回调地址;
所以回调地址需要修改成:http://mrcode.cn/outh/qq

/outh/qq 对应了social过滤器中的登录拦截地址,也就是我们前面在登录页面添加的提交地址;

那么这里就有一个小技巧了,怎么访问这个域名映射到我本地的项目呢?
有一个最简单的方法就是修改hosts文件

windows下hosts文件地址:C:\Windows\System32\drivers\etc

127.0.0.1 mrcode.cn
127.0.0.1 localhost

添加以上两个映射,保存文件

由于回调域名只支持80端口,本地项目端口也要修改成80端口;

现在就可以访问这个页面:http://mrcode.cn/imocc-signIn.html 会跳转到我们本地项目了

修改hosts原理:因为是在浏览器中进行跳转,改变的是浏览器的地址栏中的地址,
而mrcode.cn域名又被我们修改hosts文件映射到了本地,所以这样就可以调试qq登录了

如果你的回调url不是/outh/qq

那么需要覆盖配置; 并在之前配置new SpringSocialConfigurer 的地方修改成这里的覆盖之后的对象。即可

package cn.mrcode.imooc.springsecurity.securitycore.social;

import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;

/**
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 12:12
 */
public class MySpringSocialConfigurer extends SpringSocialConfigurer {
    @Override
    protected <T> T postProcess(T object) {
        // org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess()
        // 在SocialAuthenticationFilter中配置死的过滤器拦截地址
        // 这样的方法可以更改拦截的前缀
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl("/myouth");
        return (T) filter;
    }
}

基本流程源码解析

spring security的核心流程不变,social在这基础上增加了组件,和我们之前编写的流程类似;
上图蓝色部分是social除了security的东西外都是social实现好的。我们要做的只是 橘色部分;

当登录完成后,会调回 /outh/qq 并携带code;

org.springframework.social.security.SocialAuthenticationFilter#attemptAuthService

org.springframework.social.security.provider.OAuth2AuthenticationService#getAuthToken

该方法处理了 是否携带code参数的 /outh/qq 的请求,如果没有则跳转到第三方授权页面。

这里在调用会拿code去换取accesstoken,调用api的时候解析出现了异常(根源码)
Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]
抛出异常之后,会重定向到失败的地址,也就是/signin;

使用了的 org.springframework.web.client.RestTemplate 发送请求,
也就是之前我们在 cn.mrcode.imooc.springsecurity.securitycore.qq.connet.QQServiceProvider#QQServiceProvider 中提供的  OAuth2Template 类。里面就是使用的 RestTemplate

拿到响应结果之后会调用方法展开结果,
org.springframework.web.client.HttpMessageConverterExtractor#extractData;
在循环中没有找到对应的转换器,故而报错

MediaType contentType = getContentType(responseWrapper);

try {
  for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
    if (messageConverter instanceof GenericHttpMessageConverter) {

稳定定位到了,那么就来处理掉,添加一个HttpMessageConverter进去应该就ok了;

那要怎么添加呢?最好就是查看下OAuth2Template是否有提供,没有的话就继承然后使用自己的

package cn.mrcode.imooc.springsecurity.securitycore.qq.connet;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 14:58
 */
public class QQAuth2Template extends OAuth2Template {
    private Logger logger = LoggerFactory.getLogger(getClass());

    public QQAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
    }

    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        // 添加一个处理 [text/plan] 格式的转换器
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("utf-8")));
        return restTemplate;
    }

    // http://wiki.connect.qq.com/%E4%BD%BF%E7%94%A8authorization_code%E8%8E%B7%E5%8F%96access_token
    // 文档中说明:响应的是 access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
    // 不是一个json串
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
        String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
        // 会发现返回的信息是 callback( {"error":100002,"error_description":"param client_secret is wrong or lost "} )
        // 通过debug可以发现,传递过来的参数少了2个,对比文档中的;
        // 调用本方法之前传递过来的参数,也就是 exchangeForAccess() 方法
        // 其中有一个 useParametersForClientAuthentication 属性需要为true才会携带另外另个参数
        logger.info("获取accessToken响应:{}", responseStr);
        String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
        String accessToken = StringUtils.substringAfterLast(items[0], "=");
        String expiresIn = StringUtils.substringAfterLast(items[1], "=");
        String refreshToken = StringUtils.substringAfterLast(items[2], "=");
        AccessGrant accessGrant = new AccessGrant(accessToken, null, refreshToken, new Long(expiresIn));
        return accessGrant;
    }
}

这其中出现很多问题,比如添加了转换器;一调试还是报错,只有一步一步的跟,
才会发现默认的OAuth2Template中是需要一个json串,并转成map,
然后又覆盖了这个获取解析的地方。

其他小地方也出现一些代码问题。那就是不要慌,保持本心,一步一步调试即可

回顾下流程

  1. 访问/auth/qq,未携带code参数
  2. 会重定向认证服务器,用户授权完成后,再调回原地址/auth/qq
  3. social检测到携带了code参数,会去调用qqimpl交换accessToken
  4. 条用api获取accessToken信息
  5. 拿到令牌,包装成AccessGrant
  6. 获取用户信息

但是这里获取完用户信息,就跳转到了 http://mrcode.cn/signup ; 注册页面

为什么会跳转到注册页面呢?下一节继续

处理注册逻辑

获取完用户信息,就跳转到了 http://mrcode.cn/signup ; 注册页面

为什么会跳转到注册页面呢?学习了这么长时间,核心基本原理也了解了,解决问题的方法也了解了,
那么先靠自己来尝试解决下,一步一步的跟着代码,发现报了一个错误

org.springframework.social.security.SocialAuthenticationProvider#authenticate

String userId = toUserId(connection);
  if (userId == null) {
    throw new BadCredentialsException("Unknown access token");
  }

  protected String toUserId(Connection<?> connection) {
		List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
		// only if a single userId is connected to this providerUserId
		return (userIds.size() == 1) ? userIds.iterator().next() : null;
	}
看源码,这里使用了查询数据库,没有获取到userId,标识该用户还没有在我们的业务系统中绑定

设置默认跳转到注册页面

就是把默认的/signUp路径修改成我们自己的路径;

提供一个配置,支持使用方自定义注册页面

@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
    // 默认配置类,进行组件的组装
    // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
    SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
    springSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
    return springSocialConfigurer;
}

怎么让注册动作与social互动?

  1. 怎么拿到用户授权后获取的用户信息?
  2. 怎么让注册这个动作与social互动(也就是要把关联信息插入到数据库中)

关键工具:org.springframework.social.connect.web.ProviderSignInUtils
目前不知道这个工具从哪里来的;

原理:原理就是把存储在session中的用户信息获取到;

org.springframework.social.security.SocialAuthenticationFilter#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) {
    // connection unknown, register new user?
    if (signupUrl != null) {
      // 跳转到注册页面前把连接信息存入session
      //
      sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
      throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
    }
    throw e;
  }
}

实现:

  1. 提供获取用户信息的接口
  2. 提供注册方法,然后使用工具类交互插入数据库,再次登录的时候就不会再次跳往注册页面了

获取用户的接口:

cn.mrcode.imooc.springsecurity.securitybrowser.BrowserSecurityController
/**
 * see {@link SocialConfig#providerSignInUtils(org.springframework.social.connect.ConnectionFactoryLocator, org.springframework.social.connect.UsersConnectionRepository)}
 */
@Autowired
private ProviderSignInUtils providerSignInUtils;
@GetMapping("/social/user")
public SocialUserInfo getSocialUserInfo(javax.servlet.http.HttpServletRequest request) {
    SocialUserInfo userInfo = new SocialUserInfo();
    Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
    userInfo.setProviderId(connection.getKey().getProviderId());
    userInfo.setProviderUserId(connection.getKey().getProviderUserId());
    userInfo.setNickname(connection.getDisplayName());
    userInfo.setHeadimg(connection.getImageUrl());
    return userInfo;
}

提供注册方法:在demo里面,因为注册的逻辑是使用方才知道

com.example.demo.web.controller.UserController

@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {

    //不管是注册用户还是绑定用户,都会拿到一个用户唯一标识。
    String userId = user.getUsername();
    // 在这里就可以执行绑定或则注册用户的逻辑了
    // 然后使用 doPostSignUp 进行插入数据库
    providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
}

怎么让没有查询到userId的用户不跳转到注册页面,默认注册一个账户?

在这样的场景下;有一部分网站是使用qq登录就默认为你注册一个账户。然后直接完成登录;

原理是:

之前的源码中没有获取到 userId的地方 :
org.springframework.social.security.SocialAuthenticationProvider#toUserId
protected String toUserId(Connection<?> connection) {
  List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
  // only if a single userId is connected to this providerUserId
  return (userIds.size() == 1) ? userIds.iterator().next() : null;
}

org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository#findUserIdsWithConnection

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());		
		if (localUserIds.size() == 0 && connectionSignUp != null) {
      // 注意这里,connectionSignUp 可以返回一个新的userid
      // 在这里就可以插入我们自己的逻辑,比如默认注册用户
			String newUserId = connectionSignUp.execute(connection);
			if (newUserId != null)
			{
				createConnectionRepository(newUserId).addConnection(connection);
				return Arrays.asList(newUserId);
			}
		}
		return localUserIds;
	}

实现:由于这样个性化的也属性使用方控制

package com.example.demo.security;

import cn.mrcode.imooc.springsecurity.securitycore.social.SocialConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;

/**
 * 第三方登录,默认注册用户
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/6 20:04
 * @see SocialConfig#connectionSignUp  该对象存在则会在该地方被使用
 */
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public String execute(Connection<?> connection) {
        logger.info("根据社交用户信息默认创建用户并返回用户唯一标识");
        return connection.getDisplayName();
    }
}

构建UsersConnectionRepository的时候把ConnectionSignUp实现设置进去

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;
    @Autowired
    private DataSource dataSource;

    /**
     * 不存在则不使用默认注册用户,而是跳转到注册页完成注册或则绑定
     */
    @Autowired(required = false)
    private ConnectionSignUp connectionSignUp;

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

运行查看效果:直接默认插入了一条数据到数据库中完成了默认的注册;直接认证登录成功

INSERT INTO `imooc-demo`.`imooc_userconnection` (`userId`, `providerId`, `providerUserId`, `rank`, `displayName`, `profileUrl`, `imageUrl`, `accessToken`, `secret`, `refreshToken`, `expireTime`) VALUES ('猪', 'qq', '81F03E50B76D6D829F5A4875941567A6', '1', '猪', NULL, 'http://thirdqq.qlogo.cn/qqapp/101316278/81F03E50B76D6D829F5A4875941567A6/40', '2FCF43C2BA45ECD4CA4508FC8DC2CED8', NULL, 'D5C83B6F2E95DA9B4B97849113F15972', '1541333450678');

开发微信登录

微信大体的逻辑和 qq的实现类似,但是有api相关和不太一样,要仔细分析这些不同的地方,进行适配