1 原理介绍
根据慕课网的网上教程开发的
在spring security中的执行逻辑图
通过SocialAuthenticationFilter拦截到第三方登录的请求后,根据对应的ConnectionFactory获取对应第三方的用户信息
自定义第三方登录逻辑
2 代码开发
2.1 根据以上逻辑图,先开发 OAuth2ApiBinding 类
QQUserInfo.java
/**
* qq互联:http://wiki.connect.qq.com/get_user_info
*/
@Data
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;
}
QQ.java
public interface QQ {
/**
* 获取用户信息
*/
public QQUserInfo getUserInfo();
}
QQImpl.java
@Slf4j
public 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\":\"", "\"}");
}
@Override
public 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
@Slf4j
public 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);
}
@Override
protected 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************************BE14
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);
}
/**
* 坑,日志debug模式才打印出来 处理qq返回的text/html 类型数据
*
* @return
*/
@Override
protected 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;
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken, appId);
}
}
开发ApiAdapter
QQAdapter.java
/**
* qq返回的信息为spring social提供的适配器
*/
public class QQAdapter implements ApiAdapter<QQ> {
@Override
public boolean test(QQ api) {
//测试qq服务器是否正常工作
return true;
}
@Override
public 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);
}
@Override
public UserProfile fetchUserProfile(QQ api) {
return null;
}
@Override
public 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 {
@Autowired
private SystemSecurityProperties systemSecurityProperties;
/**
* 添加QQConnectionFacitonry到ConnectionFactionry中
*
* @param cfConfig
* @param env
*/
@Override
public 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
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return null;
}
}
配置SocialConfig
SocialConfig.java
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
//第三方用户登录,是否默认直接注册
@Autowired(required=false)
private ConnectionSignUp connectionSignUp;
@Autowired
private 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;
}
@Override
public 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();
};
}
@Bean
public 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")
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
实现登录后自动注册
/**
* 第三方登录时,自动注册用户信息
*/
@Slf4j
@Component
public class SystemConnectionSignUp implements ConnectionSignUp {
@Override
public String execute(Connection<?> connection) {
log.info("【SystemConnectionSignUp】 自动注册用户");
OAuth2Connection oAuth2Connection=(OAuth2Connection)connection;
ConnectionKey key = oAuth2Connection.getKey();
log.info("key={}",key);
//若是返回为null,则不会自动注册用户
return oAuth2Connection.getDisplayName();
}
}