Spring Boot整合Security系列步骤及问题排查(十一)—— 集成QQ登录

news/2024/11/20 4:20:14/

工具类准备:
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>&nbsp;&nbsp;&nbsp;&nbsp;</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方法;

http://www.ppmy.cn/news/528648.html

相关文章

支付宝不用网络,也能正常支付,太神奇了吧!

文末留言送书5本 现在生活已经离不开微信/支付宝电子支付&#xff0c;平常出去吃饭、购物只要带个手机&#xff0c;就可以解决一切&#xff0c;以致于现在已经好久没摸过真????了。 有一次出去吃饭&#xff0c;排着队付钱&#xff0c;等着过程非常无聊&#xff0c;准备拔出…

做wish用多大的虚拟服务器,Wish卖家看过来,这几个平台“潜规则”你必须知道!...

相比与其他平台的条条框框&#xff0c;Wish的低门槛可谓十分亲民&#xff0c;吸引了不少其他平台的卖家入驻&#xff0c;但不同于亚马逊、eBay的是&#xff0c;Wish平台有一套专属自己的玩法和规则&#xff0c;如果用其他平台的思维去运营Wish&#xff0c;是万万不行的&#xf…

从qq for android登录,GitHub - BarefootBKK/QQLoginForAndroid: 安卓第三方登录

安卓第三方QQ登录-项目说明 本项目集成封装了QQ第三方登录&#xff0c;大大减少了开发者编写【调用QQ登录】功能的代码量 引入本项目 Step 1. 添加依赖 build.gradle (project) allprojects { repositories { // ... maven { url https://jitpack.io } } } build.gradle (app) …

做wish用多大的虚拟服务器,虚拟海外仓适应于wish,ebay,速卖通,自营平台

Wish六月政策更新总结来了&#xff0c;跟紧平台步伐很重要&#xff01; 1. 瑞典路向订单之可接受的物流服务商更新 非Wish Express订单必须使用以下配送商&#xff1a;Wishpost(WishPost-Chinapost, WishPost-EUB)** BPostInternational OneWorldExpress** SFInternational** …

springboot网站应用使用第三方qq登录

使用第三方qq登录需要在QQ互联官网https://connect.qq.com/中申请成功才可以使用。 1.登录成功后进入个人设置中心设置个人信息 2.选择公司或个人接入&#xff0c;这里我选择个人接入 3.注册审核通过以后&#xff0c;点击应用管理&#xff0c;右侧会显示审核已通过&#xff0c…

qq互联android sdk,QQ互联API列表 - YangJunwei

分组名称 适用平台 功能说明 备注 访问用户资料 网站、移动 获取登录用户的昵称、头像、性别 访问用户QQ会员信息 网站、移动 获取QQ会员的基本信息 需要申请 访问我的空间相册 网站、移动 获取用户QQ空间相册列表 需要申请 上传一张照片到QQ空间相册 在用户的空间相册里&#…

尚硅谷书城项目第二阶段——用户注册和登陆(2)

1、编写UserDao并测试 1.1、编写UserDao接口 public interface UserDao {/*** 根据用户名查询用户信息* param username 用户名* return 如果返回null,说明没有这个用户。反之亦然*/public User queryUserByUsername(String username);/*** 根据 用户名和密码查询用户信息* p…

javascript属性

一window对象 alert方法 &#xff1a;弹出一个只有确定按钮的提示框 confirm方法 &#xff1a;弹出一个具有确定和取消按钮的提示框 //window.confirm("确定是个好人吗&#xff1f;")确定返回true&#xff0c;取消返回false prompt方法 &#xff1a;弹出用户可以输…