// 设置带上 client_id、client_secret
setUseParametersForClientAuthentication(true);
}
/**
- 解析 QQ 返回的令牌
*/
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
// 返回格式:access_token=FE04CCE2&expires_in=7776000&refresh_token=88E4***BE14
String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
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);
}
/**
- QQ 响应 ContentType=text/html;因此需要加入 text/html; 的处理器
*/
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charsets.UTF_8));
return restTemplate;
}
}
-
QQ响应的数据是一个“ContentType=text/html;”的字符串,所以我们要配置StringHttpMessageConverter进行数据接收
-
重写postForAccessGrant方法,解析QQ响应的字符串,从中解析出accessToken 、expiresIn过期时间和refreshToken
-
如果需要在请求URL上带上client_id(APP ID)、client_secret(APP KEY),需要设置setUseParametersForClientAuthentication(true)。默认不带这两个参数。
该用户信息即“SpringSocial社交媒体登录总图”中的User。即:社交媒体平台的用户信息。
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class QQUser {
private String openId;
//返回码:0表示获取成功
private String ret;
//返回错误信息,如果返回成功,错误信息为空串
private String msg;
//用户昵称
private String nickname;
//用户的头像30x30
private String figureurl;
//性别
private String gender;
}
以上信息是从响应数据中挑选了一些重要的信息进行封装,完整的响应数据结构参考:get_user_info接口定义。因为我们定义的信息不完整,为了避免映射字段找不到的异常,加上@JsonIgnoreProperties(ignoreUnknown = true)注解。该注解如果无法理解,可以自行学习很简单。
首先我们定义一个获取QQ用户信息的接口,接口只有一个方法如下:
public interface QQApi {
QQUser getUserInfo();
}
然后我们来定义QQAPI接口实现类,同时继承AbstractOAuth2ApiBinding。我们在源码解析章节已经说到了,AbstractOAuth2ApiBinding封装了accessToken以及RestTemplate,帮助我们实现HTTP请求的参数携带,以及请求结果到对象的反序列化工作等。
@Slf4j
public class QQApiImpl extends AbstractOAuth2ApiBinding implements QQApi {
private static final String URL_GET_OPENID = “https://graph.qq.com/oauth2.0/me?access_token=%s”;
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 QQApiImpl(String accessToken, String appId) {
//默认是使用header传递accessToken,而QQ比较特殊是用parameter传递token
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.appId = appId;
this.openId = getOpenId(accessToken);
log.info(“QQ互联平台openId:{}”,this.openId);
}
//通过接口获取openId
private String getOpenId(String accessToken) {
String url = String.format(URL_GET_OPENID, accessToken);
String result = getRestTemplate().getForObject(url, String.class);
return StringUtils.substringBetween(result, ““openid”:”“, “”}”);
}
//通过接口获取用户信息
@Override
public QQUser getUserInfo() {
try {
String url = String.format(URL_GET_USERINFO, appId, openId);
String result = getRestTemplate().getForObject(url, String.class);
QQUser userInfo = objectMapper.readValue(result, QQUser.class);
userInfo.setOpenId(openId);
return userInfo;
} catch (Exception e) {
throw new RuntimeException(“获取用户信息失败”, e);
}
}
}
-
get_user_info接口的定义仍参考QQ:get_user_info接口
-
获取openId的接口参考:QQ:获取OpenId的接口。openId是用户在社交媒体平台上的唯一标识,准确的说是用于对外提供的用户唯一标识,open的开放的,他们自己内部一定会有一个内部使用的用户唯一标识。
-
AbstractOAuth2ApiBinding 在进行接口请求的时候,默认是使用header传递accessToken,而QQ是要求使用URL参数的方式传递AccessToken。所以我们需要更改一下参数的传递方式,如上文代码中的注释。
-
ObjectMapper 是jackson的类,此处用于将JSON字符串转换为QQUser对象。
-
RestTemplate用于帮助我们实现HTTP请求与响应的处理操作。
我们自己开发的应用通过OAuth2协议与服务提供商进行交互,主要有两部分
-
一是认证流程,获取授权码、获取AccessToken,这部分是标准的OAuth2认证流程,这个过程大家基本都一样,有差别也很小。由QQOAuth2Template(OAuth2Operations)帮我们完成。
-
二是请求接口,获取用户数据,获取openId。这部分每个平台都不一样,需要我们自定义完成,如QQApiImpl(QQAPI)
我们需要将这两部分内容的封装结果告知ServiceProvider,从而可以被正确调用。代码如下:
public class QQServiceProvider extends AbstractOAuth2ServiceProvider {
//OAuth2获取授权码的请求地址
private static final String URL_AUTHORIZE = “https://graph.qq.com/oauth2.0/authorize”;
//OAuth2获取AccessToken的请求地址
private static final String URL_GET_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_GET_ACCESS_TOKEN));
this.appId = appId;
}
@Override
public QQApi getApi(String accessToken) {
return new QQApiImpl(accessToken, appId);
}
}
不同的社交媒体平台(QQ、微信、GitHub)用户数据结构各式各样,但是Spring Social只认识Connection这一种用户信息结构。所以需要将QQUser与Connection进行适配。代码如下
public class QQApiAdapter implements ApiAdapter {
//测试Api连接是否可用
@Override
public boolean test(QQApi api) {
return true;
}
//QQApi 与 Connection 做适配(核心)
@Override
public void setConnectionValues(QQApi api, ConnectionValues values) {
QQUser user = api.getUserInfo();
values.setDisplayName(user.getNickname());
values.setImageUrl(user.getFigureurl());
values.setProviderUserId(user.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQApi api) {
return null;
}
@Override
public void updateStatus(QQApi api, String message) {
}
}
自定义OAuth2ConnectionFactory,通过QQServiceProvider发送请求,通过QQApiAdapter将请求结果转换为Connection。
public class QQConnectionFactory extends OAuth2ConnectionFactory {
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQApiAdapter());
}
}
QQConnectionFactory构造方法的第一个参数是providerId可以随便定义,但是最好要具有服务提供商的唯一性和可读性。比如:qq、wechat。第二个参数和第三个参数是在服务提供商创建应用申请的APP ID和APP KEY。
@Configuration
@EnableSocial
public class QQAutoConfiguration extends SocialConfigurerAdapter {
@Resource
private DataSource dataSource;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository usersConnectionRepository =
new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 设置表前缀
usersConnectionRepository.setTablePrefix(“sys_”);
return usersConnectionRepository;
}
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer,
Environment environment) {
connectionFactoryConfigurer.addConnectionFactory(
new QQConnectionFactory(“qq”, //这里配置什么取决于你的回调地址
“你申请的APP ID”,“你申请的APP KEY”)); //这里可以优化为application配置
}
@Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
}
-
UsersConnectionRepository是用于操作数据库UserConnection表的持久层封装。我们可以通过setTablePrefix为UserConnection增加一个表前缀。
-
向Spring Socail添加一个ConnectionFactory,即:QQConnectionFactory
-
UserIdSource这段代码照着写就行,是Spring Social升级2.0之后做的兼容性不好,UserIdSource需要我们自己创建。
-
上面的代码可以优化,将一些常量配置抽取到application全局配置文件里面,使用@Value或@ConfigurationProperties注解读取。
@Configuration
public class QQFilterConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
public QQFilterConfigurer() { }
public QQFilterConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
@SuppressWarnings(“unchecked”)
protected T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
- filterProcessesUrl是用于拦截用户QQ登录请求和认证服务器回调请求的路径,如果不做配置默认是“/auth”。在QQAutoConfiguration加入如下配置
@Bean
public SpringSocialConfigurer qqFilterConfig() {
QQFilterConfigurer configurer = new QQFilterConfigurer(“/login”);
configurer.signupUrl(“/bind.html”);
configurer.postLoginUrl(“/index”);
return configurer;
}
- 除了配置filterProcessesUrl,还可以配置诸如:用户绑定界面signupUrl、登录成功跳转页面postLoginUrl、登录失败跳转路径等等。
@Resource
private SpringSocialConfigurer qqFilterConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.apply(qqFilterConfig).and()
…
}
- 在应用中注入入qqFilterConfig,并在Spring Security配置中将该配置生效,用于使Spring Social过滤器拦截。
-
这个登录地址分为两段,login是上文中配置的filterProcessesUrl,qq是上文中配置的providerId。
-
QQ登录路径的配置一定要与filterProcessesUrl和providerId对应上,否则登录请求无法正确拦截
-
在QQ互联的回调域的配置也必须是http://域名:端口/{filterProcessesUrl}/{providerId},否则用户认证回调无法正确拦截
======================================================================
如果报下面类似的表找不到的错误,需要先去建表。这张表是用于保存本平台用户与社交媒体平台用户关系的数据库表。
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException:
Table ‘testdb.sys_userconnection’ doesn’t exist
在spring-social-core.jar里面找到org.springframework.social.connect.jdbc.JdbcUsersConnectionsRepository.sql。使用该文件里面的建表语句创建:[tablePrefix]UserConnection,其中tablePrefix替换为我们上一节的自定义配置。比如:“sys_”
create table sys_UserConnection (
userId varchar(255) not null,
providerId varchar(255) not null,
providerUserId varchar(255),
rank int not null,
displayName varchar(255),
profileUrl varchar(512),
imageUrl varchar(512),
accessToken varchar(512) not null,
secret varchar(512),
refreshToken varchar(512),
expireTime bigint,
primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on sys_UserConnection(userId, providerId, rank);
Spring Social依赖于session,所以不要设置无状态模式,否则无法正确跳转。
//.sessionManagement()
//.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
通过之前的文章讲解。我们知道ApiAdapter将QQ平台的用户标准数据结构QQUser转换为Spring Social用户标准的数据结构Connection。这两种用户信息仍然代表的是服务提供商的用户信息,那我们就面临着一个问题:如何通过服务提供商的用户信息Connection,得到我们自己开发的系统的用户信息?
首先,明确数据库里面有一张表UserConnection,这张表有三个核心字段userId、providerId、providerUserId。
-
userId是我们自己开发的应用的用户唯一标识
-
providerId是服务提供商的唯一标识
-
providerUserId是服务提供商用户的唯一标识(对于qq来说就是用户的openId)
通过这张表我们可以确定我们自己开发的应用的用户与服务提供商用户之间的关系,这张表里面的数据是用户注册或者绑定的时候插入的(后文会讲到如何实现)。
其次,现在已知Connection包含providerId和providerUserId,那么如何获取userId?答案就是使用UserConnectionRepository接口。Spring Social通过该接口查询UserConnection表,通过providerId和providerUserId获取userId。
至此,我们就拿到了本地系统的userId。Spring Social提供给我们两个接口,一个是SocialUserDetails,一个是SocialUserDetailsService,大家看到这两个接口是不是有点眼熟?对了,就是和我们使用用户名密码登录情景下的UserDetails和UserDetailsService是一样的,只不过一个是通过username加载UserDetails,一个是通过userId加载UserDetails。SocialUserDetails继承自UserDetails,所以我们的用户信息实体实现SocialUserDetails接口即可。
@Data
@AllArgsConstructor
public class MyUserDetails implements SocialUserDetails {
String password; //密码
String username; //用户名
boolean accountNonExpired; //是否没过期
boolean accountNonLocked; //是否没被锁定
boolean credentialsNonExpired; //是否没过期
boolean enabled; //账号是否可用
Collection<? extends GrantedAuthority> authorities; //用户的权限集合
@Override
public String getUserId() {
return username;
}
}
通常我们需要为用户生成一个userId并保存在数据库字段,我们这里就不做的那么麻烦了,直接使用username作为userId。
然后在原有的UserDetailsService实现上,新增SocialUserDetailsService的实现,即实现loadUserByUserId。
@Component
public class MyUserDetailsService implements UserDetailsService ,SocialUserDetailsService {
@Resource
private MyUserDetailsServiceMapper myUserDetailsServiceMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUserDetails myUserDetails = getMyUserDetails(username);
return myUserDetails;
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
MyUserDetails myUserDetails = getMyUserDetails(userId);
return myUserDetails;
}
private MyUserDetails getMyUserDetails(String username) {
//加载基础用户信息
MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(username);
//加载用户角色列表
List roleCodes = myUserDetailsServiceMapper.findRoleByUserName(username);
//通过用户角色列表加载用户的资源权限列表
List authorties = myUserDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
//角色是一个特殊的权限,ROLE_前缀
roleCodes = roleCodes.stream()
.map(rc -> “ROLE_” +rc)
.collect(Collectors.toList());
authorties.addAll(roleCodes);
myUserDetails.setAuthorities(
AuthorityUtils.commaSeparatedStringToAuthorityList(
String.join(“,”,authorties)
)
);
return myUserDetails;
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
最后
小编精心为大家准备了一手资料
以上Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术
【附】架构书籍
- BAT面试的20道高频数据库问题解析
- Java面试宝典
- Netty实战
- 算法
BATJ面试要点及Java架构师进阶资料
花板技术停滞不前!**
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-eVHCi2Kz-1711656620135)]
[外链图片转存中…(img-1mL2RZdf-1711656620136)]
[外链图片转存中…(img-PcNnlQmF-1711656620137)]
[外链图片转存中…(img-ELKNji74-1711656620137)]
[外链图片转存中…(img-suvYHVAm-1711656620137)]
[外链图片转存中…(img-5ursx7mE-1711656620138)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-IPXSKcuw-1711656620138)]
最后
小编精心为大家准备了一手资料
[外链图片转存中…(img-MY3nv3dd-1711656620139)]
[外链图片转存中…(img-qC7fJwA3-1711656620139)]
以上Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术
【附】架构书籍
- BAT面试的20道高频数据库问题解析
- Java面试宝典
- Netty实战
- 算法
[外链图片转存中…(img-ErEVPMld-1711656620139)]
BATJ面试要点及Java架构师进阶资料
[外链图片转存中…(img-nHOlnHv5-1711656620140)]