1 原理介绍
根据慕课网的网上教程开发的

在spring security中的执行逻辑图
通过SocialAuthenticationFilter拦截到第三方登录的请求后,根据对应的ConnectionFactory获取对应第三方的用户信息

自定义第三方登录逻辑

2 代码开发
2.1 根据以上逻辑图,先开发 OAuth2ApiBinding 类
QQUserInfo.java
/*** qq互联:http://wiki.connect.qq.com/get_user_info*/@Datapublic 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;}
QQ.java
public interface QQ {/*** 获取用户信息*/public QQUserInfo getUserInfo();}
QQImpl.java
@Slf4jpublic class QQImpl extends AbstractOAuth2ApiBinding implements QQ {/*** 获取 openId 的url地址*/private static final String QQ_URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";/*** 获取用户信息* http://wiki.connect.qq.com/get_user_info(access_token由父类提供)*/private static final String QQ_URL_GET_USER_INFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";/*** appId 配置文件读取*/private String appId;/*** openId 请求QQ_URL_GET_OPENID返回*/private String openId;/*** 构造方法获取openId** @param accessToken* @param appId*/public QQImpl(String accessToken, String appId) {//access_token作为查询参数来携带。super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);this.appId = appId;String url = String.format(QQ_URL_GET_OPENID, accessToken);String result = getRestTemplate().getForObject(url, String.class);log.info("【QQImpl】 获取openId地址:QQ_URL_GET_OPENID={} 获取openId结果:result={}", QQ_URL_GET_OPENID, result);this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");}@Overridepublic QQUserInfo getUserInfo() {String url = String.format(QQ_URL_GET_USER_INFO, appId, openId);String result = getRestTemplate().getForObject(url, String.class);//log.info("【QQImpl】 QQ_URL_GET_USER_INFO={} result={}", QQ_URL_GET_USER_INFO, result);QQUserInfo userInfo = null;try {userInfo = JSON.parseObject(result, QQUserInfo.class);userInfo.setOpenId(openId);return userInfo;} catch (Exception e) {throw new RuntimeException("获取用户信息失败", e);}}}
开发OAuth2Template
QQOAuth2Template.java
@Slf4jpublic class QQOAuth2Template extends OAuth2Template {public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {super(clientId, clientSecret, authorizeUrl, accessTokenUrl);//保证可以带着clent_id 和client_secret参数setUseParametersForClientAuthentication(true);}@Overrideprotected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);log.info("【QQOAuth2Template】获取accessToke的响应:responseStr={}", responseStr);String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");//http://wiki.connect.qq.com/使用authorization_code获取access_token//access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14String 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);}/*** 坑,日志debug模式才打印出来 处理qq返回的text/html 类型数据** @return*/@Overrideprotected RestTemplate createRestTemplate() {RestTemplate restTemplate = super.createRestTemplate();restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));return restTemplate;}}
开发OAuth2ServiceProvider
QQOAuth2ServiceProvider.java
public class QQOAuth2ServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {public String appId;/*** 获取 授权码code 地址*/private static final String QQ_URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";/*** 获取access_token 也就是令牌*/private static final String QQ_URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";public QQOAuth2ServiceProvider(String appId, String appSecret) {super(new QQOAuth2Template(appId, appSecret, QQ_URL_AUTHORIZE, QQ_URL_ACCESS_TOKEN));this.appId = appId;}@Overridepublic QQ getApi(String accessToken) {return new QQImpl(accessToken, appId);}}
开发ApiAdapter
QQAdapter.java
/*** qq返回的信息为spring social提供的适配器*/public class QQAdapter implements ApiAdapter<QQ> {@Overridepublic boolean test(QQ api) {//测试qq服务器是否正常工作return true;}@Overridepublic void setConnectionValues(QQ api, ConnectionValues values) {//将获取的qq用户信息,转为social的标准用户信息,保存到数据库中QQUserInfo userInfo = api.getUserInfo();values.setProviderUserId(userInfo.getOpenId());//openId 唯一标识values.setDisplayName(userInfo.getNickname());values.setImageUrl(userInfo.getFigureurl_qq_1());values.setProfileUrl(null);}@Overridepublic UserProfile fetchUserProfile(QQ api) {return null;}@Overridepublic void updateStatus(QQ api, String message) {//返回用户操作信息,例如微博用户,可以更新微博状态等。在QQ中没有这个概念}}
开发OAuth2ConnectionFactory
QQConnectionFactory.java
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {public QQConnectionFactory(String providerId, String appId, String appSecret) {super(providerId, new QQOAuth2ServiceProvider(appId, appSecret), new QQAdapter());}}
配置ConnectionFactor到系统中
@Configuration// 当配置了app-id的时候才启用@ConditionalOnProperty(prefix = "system.security.social.qq", name = "app-id")public class QQAutoConfig extends SocialConfigurerAdapter {@Autowiredprivate SystemSecurityProperties systemSecurityProperties;/*** 添加QQConnectionFacitonry到ConnectionFactionry中** @param cfConfig* @param env*/@Overridepublic void addConnectionFactories(ConnectionFactoryConfigurer cfConfig, Environment env) {QQProperties qq = systemSecurityProperties.getSocial().getQq();QQConnectionFactory qqConnectionFactory = new QQConnectionFactory(qq.getProviderId(), qq.getAppId(),qq.getAppKey());cfConfig.addConnectionFactory(qqConnectionFactory);}// 后补:做到处理注册逻辑的时候发现的一个bug:登录完成后,数据库没有数据,但是再次登录却不用注册了// 就怀疑是否是在内存中存储了。结果果然发现这里父类的内存ConnectionRepository覆盖了SocialConfig中配置的jdbcConnectionRepository// 这里需要返回null,否则会返回内存的 ConnectionRepository@Overridepublic UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {return null;}}
配置SocialConfig
SocialConfig.java
@Configuration@EnableSocialpublic class SocialConfig extends SocialConfigurerAdapter {@Autowiredprivate DataSource dataSource;//第三方用户登录,是否默认直接注册@Autowired(required=false)private ConnectionSignUp connectionSignUp;@Autowiredprivate SystemSecurityProperties systemSecurityProperties;/*** Default implementation of {@link #getUsersConnectionRepository(ConnectionFactoryLocator)} that creates an in-memory repository.*/public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,connectionFactoryLocator, Encryptors.noOpText());repository.setTablePrefix("sys_");//首次使用第三方用户登录时,自动创建系统用户if(connectionSignUp != null) {repository.setConnectionSignUp(connectionSignUp);}return repository;}@Overridepublic UserIdSource getUserIdSource() {return () -> {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication == null) {throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");}return authentication.getName();};}@Beanpublic SpringSocialConfigurer systemSpringSocialConfigurer() {//自定义过滤器拦截路径// String filterProcessesUrl = systemSecurityProperties.getSocial().getFilterProcessesUrl();// SystemSpringSocialConfigurer systemSpringSocialConfigurer= new SystemSpringSocialConfigurer(filterProcessesUrl);// return systemSpringSocialConfigurer;return new SpringSocialConfigurer();}}
测试
此时若是在QQ互联上定义回调地址为 /auth/qq,应该是没有问题的了。但是一般我们会自定义拦截路径,此时应该新建SpringSocialConfigurer的实现类,重写 SocialAuthenticationFilter中的拦截路径
SystemSpringSocialConfigurer.java
public class SystemSpringSocialConfigurer extends SpringSocialConfigurer {private String filterProcessesUrl;public SystemSpringSocialConfigurer(String filterProcessesUrl) {this.filterProcessesUrl = filterProcessesUrl;}/**** @param object* 放到过滤器链上的filter* @param <T>* @return*/@SuppressWarnings("unchecked")@Overrideprotected <T> T postProcess(T object) {SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);filter.setFilterProcessesUrl(filterProcessesUrl);return (T) filter;}}
实现登录后自动注册
/*** 第三方登录时,自动注册用户信息*/@Slf4j@Componentpublic class SystemConnectionSignUp implements ConnectionSignUp {@Overridepublic String execute(Connection<?> connection) {log.info("【SystemConnectionSignUp】 自动注册用户");OAuth2Connection oAuth2Connection=(OAuth2Connection)connection;ConnectionKey key = oAuth2Connection.getKey();log.info("key={}",key);//若是返回为null,则不会自动注册用户return oAuth2Connection.getDisplayName();}}
