使用SpringSocial 开发第三方QQ登录


OAuth 协议
OAuth协议要解决的问题
解决传统模式的授权(授权协议),认证资源访问问题
OAuth协议中的各种角色
Privider 服务提供商(提供令牌) (如微信)
Authorization Server 认证服务器
ResourceServer 资源服务器
Resource Owner 资源所有者 (用户)
Client 第三方应用
OAuth协议运行流程
0访问Client
1将用户导向认证服务器
2用户同意授权
3返回Client并携带授权码
4申请令牌
5发放令牌 (1-5是OAuth标准流程)
6获取用户信息 (每个服务商提供返回的字段不一样)
7根据用户信息构建Authentictioin并放入SecurityContext
OAuth协议授权模式
授权码模式 (authorization code)
认证授权在服务器提供商认证服务器完成,更安全
密码模式(Resource owner password credentials)
客户端模式(client credentials)
简化模式(implicit)
SpringSocial 基本原理 (就是封装了OAuth2的基本流程 SocialAuthenticationFilter加入到过滤器链中)
ServiceProvider (AbstractOAuth2ServiceProvider服务提供商的抽象实现)
OAuth2Operations (OAuth2Template 封装1-5 会帮你去完成OAuth的认证流程)
Api(AbstractOAuth2ApiBinding获取用户信息抽象实现 6)
第7步是在第三方应用上操作
Connection(OAuth2Connection服务提供商的信息) 是由 ConnectionFactory(OAuth2ConnectionFactory) 创建的
ConnectionFactory 中是包含(ServiceProvider 的)
ApiAdapter (在Aip 和 Connection之间做一个适配,因为Connection字段是固定的,服务商是个性化的)
用户和第三方用户有个关联表,UserConnection
UserConnection 表是由 JdbcUsersConnectionRepository(UsersConnectionRepository) 这个类进行操作CRUD
第一步: 根据图2 先 构建ServiceProvider
(6获取用户信息): 根据OAuth2Operations (OAuth2Template),Api(AbstractOAuth2ApiBinding) 构建 ServiceProvider
创建API 获取用户信息接口
package com.imooc.security.core.social.qq.api;
/**
* @Title: QQ
* @ProjectName spring-security-main
* @date 2020/12/710:21
*/
public interface QQ {
QQUserInfo getUserInfo();
}
实现
package com.imooc.security.core.social.qq.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
/**
* @Title: QQImpl
* @ProjectName spring-security-main
* @date 2020/12/710:22
*/
@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";
// 获取用户信息 accessToken 父类会处理,这里不用拼接参数了
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); // 默认父类是放在header中,这里要放在参数里
this.appId = appId;
// 获取openId
String url = String.format(URL_GET_OPENID, accessToken);
String result = getRestTemplate().getForObject(url, String.class);
log.info(result);
// 截取一下openId
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 userInfo;
} catch (Exception e) {
throw new RuntimeException("获取用户信息失败", e);
}
}
}
QQUserInfo 实体类
package com.imooc.security.core.social.qq.api;
import lombok.Data;
@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;
private String gender_type;
private String constellation;
/**
* 标识用户是否为黄钻用户(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;
private String figureurl_qq;
private String figureurl_type;
}
创建QQOAuth2Template
package com.imooc.security.core.social.qq.connet;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
@Slf4j
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true); // 设置true client_secret client_id才会带上这两个参数
}
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
// 因为qq返回的是 用&拼接的字符串 需要自己处理一下
log.info("获取accessToke的响应:"+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();
// qq返回的是text/html 默认的template没有添加处理这样的类型,自己添加一个
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
}
根据 QQOAuth2Template ,QQImpl 创建 QQServiceProvider
package com.imooc.security.core.social.qq.connet;
import com.imooc.security.core.social.qq.api.QQ;
import com.imooc.security.core.social.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Template;
/**
* @Title: QQServiceProvider
* @ProjectName spring-security-main
* @date 2020/12/711:01
*/
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
private String appId;
// 1.导向认证服务器rul
private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
// 4.用户同意授权,返回授权码去申请令牌的url
private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
public QQServiceProvider(String appId, String appSecret) {
// super(oauth2Operations);
// OAuth2Template 用系统默认的
// super(new OAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
// 默认Template不能满足使用
super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
this.appId = appId;
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken, appId);
}
}
图2右边的代码已经完成
图2 左边开发
创建QQAdapter
package com.imooc.security.core.social.qq.connet;
import com.imooc.security.core.social.qq.api.QQ;
import com.imooc.security.core.social.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
/**
* 服务商和第三方应用之间做适配的,适配的类型就是QQ
*/
public class QQAdapter implements ApiAdapter<QQ> {
/**
* 测试方法
* @param api
* @return
*/
@Override
public boolean test(QQ api) {
return true;
}
/**
* 主要方法, qq api的信息设置到connectionValues
* @param api
* @param values
*/
@Override
public void setConnectionValues(QQ api, ConnectionValues values) {
QQUserInfo userInfo = api.getUserInfo();
values.setDisplayName(userInfo.getNickname());
// 头像
values.setImageUrl(userInfo.getFigureurl_qq_1());
// 个人主页
values.setProfileUrl(null);
values.setProviderUserId(userInfo.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQ api) {
return null;
}
@Override
public void updateStatus(QQ api, String message) {
}
}
根据 QQAdapter 和 QQServiceProvider 可以构建QQConnectionFactory
package com.imooc.security.core.social.qq.connet;
import com.imooc.security.core.social.qq.api.QQ;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
/**
* 根据 QQAdapter 和 QQServiceProvider 可以构建QQConnectionFactory
* @param providerId 服务商Id
* @param appId
* @param appSecret
*/
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
}
}
建表 查找 JdbcUsersConnectionRepository.sql
创建 SocialConfig 配置类,启用Social
package com.imooc.security.core.social;
import com.imooc.security.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.social.security.SpringSocialConfigurer;
import javax.sql.DataSource;
/**
* @Title: SocialConfig
* @ProjectName spring-security-main
* @date 2020/12/713:57
*/
@Configuration
@EnableSocial
@Slf4j
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
// 数据源
@Autowired
private DataSource dataSource;
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
/**
* 默认使用InMemoryUsersConnectionRepository类 , Primary 候选bean中优先使用,Bean 添加这两个注解
* @param connectionFactoryLocator 作用就是查找ConnectionFactory
* @return
*/
@Primary
@Bean
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
connectionFactoryLocator, Encryptors.noOpText());
// 配置数据库前缀
// repository.setTablePrefix("imooc_");
if(connectionSignUp != null) {
repository.setConnectionSignUp(connectionSignUp);
}
return repository;
}
/**
* 配置自定义路径 默认是auth
*需要 把SpringSocialConfigure 加入到过滤器链上
* @return
*/
@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
log.info(filterProcessesUrl);
ImoocSpringSocialConfigurer imoocSpringSocialConfigurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
// 登录成功 回调的url
imoocSpringSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
return imoocSpringSocialConfigurer;
// return new SpringSocialConfigurer();
}
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
return new ProviderSignInUtils(connectionFactoryLocator,
getUsersConnectionRepository(connectionFactoryLocator)) {
};
}
}
创建 ImoocSpringSocialConfigurer
package com.imooc.security.core.social;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
public class ImoocSpringSocialConfigurer extends SpringSocialConfigurer {
/**
* 修改默认的路由auth 可配置,默认是auth,自定义回调地址, 要和备案的一致
*/
private String filterProcessesUrl;
public ImoocSpringSocialConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
创建 QQAutoConfig
package com.imooc.security.core.social.qq.config;
import com.imooc.security.core.properties.QQProperties;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.social.qq.connet.QQConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.connect.ConnectionFactory;
@Configuration
@ConditionalOnProperty(prefix = "imooc.security.social.qq", name = "app-id") // 当配置文件中,有配置属性才会生效
public class QQAutoConfig extends SocialAutoConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Override
protected ConnectionFactory<?> createConnectionFactory() {
QQProperties qqConfig = securityProperties.getSocial().getQq();
return new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret());
}
}
把SpringSocialConfigurer 加入到过滤器链上
@Autowired private SpringSocialConfigurer imoocSocialSecurityConfig; .apply(imoocSocialSecurityConfig)
imooc-signIn.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <h2>标准登录页面</h2> <h3>表单登录</h3> <form action="/authentication/form" method="post"> <table> <tr> <td>用户名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password"></td> </tr> <tr> <td>图形验证码:</td> <td> <input type="text" name="imageCode"> <img src="/code/image?width=200"> </td> </tr> <tr> <td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td> </tr> <tr> <td colspan="2"><button type="submit">登录</button></td> </tr> </table> </form> <h3>短信登录</h3> <form action="/authentication/mobile" method="post"> <table> <tr> <td>手机号:</td> <td><input type="text" name="mobile" value="13012345678"></td> </tr> <tr> <td>短信验证码:</td> <td> <input type="text" name="smsCode"> <a href="/code/sms?mobile=13012345678">发送验证码</a> </td> </tr> <tr> <td colspan="2"><button type="submit">登录</button></td> </tr> </table> </form> <br> <h3>社交登录</h3> <a href="/qqLogin/callback.do">QQ登录2</a> <a href="/auth/qq">QQ登录1</a> <a href="/qqLogin/weixin">微信登录</a> </body> </html>

注册问题 SocialConfig
首次登录自动注册
DemoConnectionSignUp
package com.imooc.security;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
@Override
public String execute(Connection<?> connection) {
//根据社交用户信息默认创建用户并返回用户唯一标识
return connection.getDisplayName();
}
}
点击按钮进行手动注册
@Autowired
private ProviderSignInUtils providerSignInUtils;
@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {
//不管是注册用户还是绑定用户,都会拿到一个用户唯一标识。
String userId = user.getUsername();
providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
}
获取当前用户信息
/**
* 前端可以显示 当前登录的用户信息
* @param request
* @return
*/
@GetMapping("/social/user")
public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
SocialUserInfo userInfo = new SocialUserInfo();
// 在session中取出用户信息
Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
userInfo.setProviderId(connection.getKey().getProviderId());
userInfo.setProviderUserId(connection.getKey().getProviderUserId());
userInfo.setNickname(connection.getDisplayName());
userInfo.setHeadimg(connection.getImageUrl());
return userInfo;
}

浙公网安备 33010602011771号