工具类准备:
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\":\"", "\"}");}@Overridepublic 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;}@Overridepublic 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_secretsetUseParametersForClientAuthentication(true);}@Overrideprotected 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);}@Overrideprotected 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*/@Overridepublic boolean test(QQ qq) {return true;}@Overridepublic 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());}@Overridepublic UserProfile fetchUserProfile(QQ qq) {return null;}@Overridepublic 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 {@Autowiredprivate DataSource dataSource;@Autowiredprivate SecurityProperties securityProperties;/*** 自动注册处理逻辑(不一定实现),非必要加载*/@Autowired(required = false)private ConnectionSignUp connectionSignUp;/*** connectionFactoryLocator:QQ、微信等connectionFactory** @param connectionFactoryLocator* @return*/@Overridepublic 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*/@Overridepublic UserIdSource getUserIdSource() {// TODO Auto-generated method stubreturn new AuthenticationNameUserIdSource();}/*** 自定义拦截配置* 注册服务后修改配置文件端口,地址,请求等与申请一致即可** @return*/@Beanpublic SpringSocialConfigurer demoSocialSecurityConfig() {String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();DemoSpringSocialConfigurer configurer = new DemoSpringSocialConfigurer(filterProcessesUrl);// 找不到用户时跳转到自定义注册页configurer.signupUrl(securityProperties.getBrowser().getSignUpPage());return configurer;}/*** 注册过程中拿到SpringSocial信息,注册完成把userId给SpringSocial** @param connectionFactoryLocator* @return*/@Beanpublic 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 {@Autowiredprivate 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());
// }@Overridepublic 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;}@Overrideprotected <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增加signUpPage属性配置:
/*** 默认注册页*/
private String signUpPage= 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().getSignUpPage(),"/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"signUpPage: "/demoSignUp.html"social:qq:app-id:app-secret:# 注册服务时的请求,如callback.doproviderId: "callback.do"# 注册服务时的过滤地址,如/qqLoginfilterProcessesUrl: "/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用户注册逻辑:
@Autowiredprivate 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 {@Overridepublic 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方法;