工具类准备:
Repository;
Connection;
ConnectionFactory(ServiceProvider、ApiAdapter);
ServiceProvider(OAuth2Operations、Api);
Api:
/**
* QQ接口
*
* @author zhaohaibin
*/
public interface QQ {
/**
* 获取用户信息
*
* @return
*/
QQUserInfo getUserInfo();
}
/**
* QQ API 实现
*
* @author zhaohaibin
*/
@Slf4j
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
/**
* 获取openId
*/
private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
/**
* 获取用户信息
* <p>
* access_token=YOUR_ACCESS_TOKEN&由父类传递
*/
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);
log.info(result);
this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
}
@Override
public QQUserInfo getUserInfo() {
String url = String.format(URL_GET_USERINFO, appId, openId);
String result = getRestTemplate().getForObject(url, String.class);
log.info(result);
QQUserInfo userInfo = null;
try {
userInfo = objectMapper.readValue(result, QQUserInfo.class);
userInfo.setOpenId(openId);
return objectMapper.readValue(result, QQUserInfo.class);
} catch (IOException e) {
throw new RuntimeException("获取用户信息失败", e);
}
}
}
/**
* QQ 用户信息
*
* @author zhaohaibin
*/
@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;
}
ServiceProvider:
/**
* ServiceProvider
*
* @author zhaohaibin
*/
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
/**
* QQ获取授权码的url
*/
private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
/**
* QQ获取accessToken的url
*/
private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
private String appId;
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);
}
}
/**
* 自定义返回接收处理
* QQ 认证返回数据非JSON格式,自定义接收处理逻辑
*
* @author zhaohaibin
*/
@Slf4j
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
// 默认true来携带client_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("获取accessToken的响应" + 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;
}
}
ApiAdapter:
/**
* ApiAdapter
* 第三方数据和框架数据适配
*
* @author zhaohaibin
*/
public class QQAdapter implements ApiAdapter<QQ> {
/**
* QQ服务默认一直可用
*
* @param qq
* @return
*/
@Override
public boolean test(QQ qq) {
return true;
}
@Override
public void setConnectionValues(QQ qq, ConnectionValues connectionValues) {
QQUserInfo userInfo = qq.getUserInfo();
connectionValues.setDisplayName(userInfo.getNickname());
connectionValues.setImageUrl(userInfo.getFigureurl_qq_1());
// QQ无个人主页
connectionValues.setProfileUrl(null);
connectionValues.setProviderUserId(userInfo.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQ qq) {
return null;
}
@Override
public void updateStatus(QQ qq, String s) {
// QQ无个人主页,不做任何处理
}
}
ConnectionFactory:
/**
* ConnectionFactory
*
* @author zhaohaibin
*/
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
}
}
Repository:
/**
* 社交配置适配基础类
*
* @author zhaohaibin
*/
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private SecurityProperties securityProperties;
/**
* 自动注册处理逻辑(不一定实现),非必要加载
*/
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
/**
* connectionFactoryLocator:QQ、微信等connectionFactory
*
* @param connectionFactoryLocator
* @return
*/
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 调用自动注册逻辑
if (null != connectionSignUp) {
repository.setConnectionSignUp(connectionSignUp);
}
// Encryptors.noOpText() 不需要加解密
return repository;
}
/**
* 解决启动报错Error creating bean with name 'userIdSource' defined in class path resource
*
* @return
*/
@Override
public UserIdSource getUserIdSource() {
// TODO Auto-generated method stub
return new AuthenticationNameUserIdSource();
}
/**
* 自定义拦截配置
* 注册服务后修改配置文件端口,地址,请求等与申请一致即可
*
* @return
*/
@Bean
public SpringSocialConfigurer demoSocialSecurityConfig() {
String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
DemoSpringSocialConfigurer configurer = new DemoSpringSocialConfigurer(filterProcessesUrl);
// 找不到用户时跳转到自定义注册页
configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
return configurer;
}
/**
* 注册过程中拿到SpringSocial信息,注册完成把userId给SpringSocial
*
* @param connectionFactoryLocator
* @return
*/
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator));
}
}
/**
* 社交配置适配:QQ配置默认实现
*
* ConditionalOnProperty 只有配置了相关属性("app-id")才生效
*
* @author zhaohaibin
*/
@Configuration
@ConditionalOnProperty(prefix = "demo.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
// /**
// * extends SocialAutoConfigurerAdapter 而重写的方法
// * 但SocialAutoConfigurerAdapter因版本升级而删除,重新手写实现registeredAuthenticationProviderIds获取仍为空
// * 直接extends SocialAutoConfigurerAdapter的父类SocialConfigurerAdapter,重写addConnectionFactories方法
// * @return
// */
// @Autowired
// protected ConnectionFactory<?> createConnectionFactory() {
//
// QQProperties qqConfig = securityProperties.getSocial().getQq();
//
// // 将QQAutoConfig配置传到QQConnectionFactory供后续调用
// return new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret());
// }
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
QQProperties qqConfig = securityProperties.getSocial().getQq();
connectionFactoryConfigurer.addConnectionFactory(new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret()));
}
}
/**
* 自定义拦截规则
*
* @author zhaohaibin
*/
public class DemoSpringSocialConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
public DemoSpringSocialConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
配置:
/**
* @author zhaohaibin
*/
@Data
public class QQProperties /*extends SocialProperties*/ {
/**
* 服务提供者标识——QQ
*/
private String providerId = "qq";
/**
* 问题:遇到SocialAutoConfigurerAdapter,SocialProperties和SocialWebAutoConfigurerAdapter类不存在
* <p>
* 解决import org.springframework.boot.autoconfigure.social.SocialProperties;
* 因Springboot 版本升级(1.x-2.x)删除问题(自己手动重写)
* <p>
*/
private String appId;
private String appSecret;
}
/**
* Social 相关配置基础类
*
* @author zhaohaibin
*/
@Data
public class SocialProperties {
/**
* 第三方认证默认拦截url
*/
private String filterProcessesUrl = "/auth";
/**
* QQ认证配置
*/
private QQProperties qq = new QQProperties();
}
更新:
SecurityProperties增加social属性配置:
/**
* 第三方验证配置
*/
private SocialProperties social = new SocialProperties();
BrowserProperties增加signUpUrl属性配置:
/**
* 默认注册页
*/
private String signUpUrl= DEFAULT_PROJECT_NAME_URL + "signUp.html";
WebSecurityConfig部分更新代码如下:
/**
* 拦截路径在类SocialAuthenticationFilter中
*/
@Autowired
private SpringSocialConfigurer demoSocialSecurityConfig;
...
.and()
// 第三方登录拦截配置
.apply(demoSocialSecurityConfig)
.and()
// 记住我相关配置
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(authenticationBeanConfig.userDetailsService())
.and()
// 对任何请求授权
.authorizeRequests()
// 匹配页面授权所有权限
.antMatchers(
// API
"/swagger-ui.html",
// 默认登录页
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
// 自定义登录页(demoLogin)
securityProperties.getBrowser().getLoginPage(),
// 验证码
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*",
securityProperties.getBrowser().getSignUpUrl(),
"/user/regist")
.permitAll()
# security 默认登录页面配置
demo:
security:
browser:
loginPage: "/demoLogin.html"
# loginType: "REDIRECT"
# code:
# image:
# # 图形验证码长度
# length: 6
# # 图形验证码图形宽
# width: 100
# url: "/demo/user/1,/demo/user/3"
signUpUrl: "/demoSignUp.html"
social:
qq:
app-id:
app-secret:
# 注册服务时的请求,如callback.do
providerId: "callback.do"
# 注册服务时的过滤地址,如/qqLogin
filterProcessesUrl: "/qqLogin"
<h2>社交登录</h2>
<h3>QQ登录</h3>
<a href="qqLogin/callback.do">QQ登录</a>
首次登录会跳转注册页,对应代码更新如下:
BrowserSecurityController:
/**
* 获取第三方注册的用户信息
*
* @param request
* @return
*/
@GetMapping("/social/user")
public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
SocialUserInfo socialUserInfo = new SocialUserInfo();
Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
socialUserInfo.setProviderId(connection.getKey().getProviderId());
socialUserInfo.setProviderUserId(connection.getKey().getProviderUserId());
socialUserInfo.setNickname(connection.getDisplayName());
socialUserInfo.setHeadimg(connection.getImageUrl());
return socialUserInfo;
}
demoSignUp.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SocialDemo注册页</title>
</head>
<body>
<h2>Demo注册页面</h2>
<form action="user/regist" method="post">
<div><label>用户名</label><input type="text" name="username" placeholder="请输入用户名"/></div>
<div><label>密 码</label><input type="password" name="password" placeholder="请输入密码"/></div>
<div><input type="submit" value="regist"/></div>
<div><input type="submit" value="binding"/></div>
</form>
</body>
</html>
Controller用户注册逻辑:
@Autowired
private ProviderSignInUtils providerSignInUtils;
@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {
// 不管注册用户还是绑定用户,都会拿到一个用户唯一标识
String userId = user.getUsername();
providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
}
跳过首次登录手动注册,默认注册需实现之前的配置ConnectionSignUp:
/**
* 自定义第三方注册逻辑
*
* @author zhaohaibin
*/
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
@Override
public String execute(Connection<?> connection) {
// 根据社交用户信息默认创建用户并返回用户唯一标识
return connection.getDisplayName();
}
}
问题排查:
SpringBoot2.x:
1.QQProperties /*extends SocialProperties*/中SocialProperties引入Maven依赖仍找不到该类,是因为资源包升级后(1.x-2.x)被删掉了,所以需要降低版本(1.x);
2.继续启动后报错java.lang.IllegalStateException,改为手动copy,而实际就为了appId和appSecret两个属性,所以取消extends直接加入两个属性;
3.继续启动,跳转/auth/qq总是跳过拦截进入之前开发的登录页引导提示,根据之前经验怀疑是代码出现了异常重定向导致,debug后最终发现是QQAutoConfig继承SocialAutoConfigurerAdapter时重写的createConnectionFactory未执行,导致服务未加载,后续匹配失败后重定向所致,
所以直接继承其父类SocialConfigurerAdapter,重写addConnectionFactories方法;