述
上文中实现了 ServiceProvider
的部分, 然后剩下就是 ConnectionFactory
创建 Connection
的部分还有与数据量交互的部分
ConnectionFactory 构建
适配器实现
要构建 ConnectionFactory
就需要 ServiceProvider
和一个 ApiAdapter
,ServiceProvider
已经有了,那下面先把 ApiAdapter
实现一下
ApiAdapter
的作用,就是把我们获取回来的第三方的数据,和 social 的标准的数据做一个适配, 新建类 QQAdapter
:
1 | public class QQAdapter implements ApiAdapter<QQ> { |
首先这个类实现了一个 ApiAdapter
接口,泛型是QQ ,这里的泛型就是当前适配器适配的 api 的类型,我们这里要是适配的就是QQ
第一个方法 test
是要去服务提供商是否可以使用,这里就直接返回true了
然后是 setConnectionValues()
方法, 这个就是最主要的方法,作用就是把我们api返回来的数据,设置到 ConnectionValues
里面去fetchUserProfile()
这个方法后面再说
updateStatus()
这个在QQ登录也不需要不用管
ConnectionFactory 实现
到这里,我们的 ServiceProvider
和一个 ApiAdapter
就都有了,这里就可以去构建 ConnectionFactory
了, 新建一个 QQConnectionFactory
,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
/**
* 连接工厂构造
* @param providerId 服务商的唯一id
* @param appId
* @param appSecret
*/
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
}
}
这里先继承 OAuth2ConnectionFactory
然后写了一个构造, providerId
是服务提供商的id,这个是后面我们自定义去配置的, 然后把 ServiceProvider
和 ApiAdapter
都传过去就好了
数据库层的实现
ConnectionFactory
已经实现了,然后 Connection
就交给 ConnectionFactory
去创建, 最后还剩下的就是数据库层的实现, 这个直接做个配置就好了
首先,数据库需要一个表,sql如下:1
2
3
4
5
6
7
8
9
10
11
12
13CREATE TABLE 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 UserConnection(userId, providerId, rank);
这里表名是固定的 UserConnection
这个不能改,但是可以加一个自定义的前缀, 比如test_UserConnection
表建好之后,就是代码里面的配置,新建类 SocialConfig
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
/**
* 配置 JdbcUsersConnectionRepository
* @param connectionFactoryLocator
* @return
*/
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 设置表的前缀
// jdbcUsersConnectionRepository.setTablePrefix("");
return jdbcUsersConnectionRepository;
}
}
这里继承 SocialConfigurerAdapter
然后重写 getUsersConnectionRepository()
方法
这里的参数是 ConnectionFactoryLocator
是用来查询对应的 ConnectionFactory
的,因为我们集成的服务提供商可能有多个, 所以需要找到对应的 ConnectionFactory
然后再看一下构造,前两个就不说了,最后一个 Encryptors
是配置一个加密的方式,是 noOpText()
表示不做加密处理
然后 setTablePrefix()
方法是设置表的前缀, 如果建表的时候有前缀,那这里就写自己的前缀就好
UserDetailService 改造
上面完成之后, QQ登录的流程就算完了, 接下来还有一些配置
之前我们写了一个自定义的 UserDetailService
是 MyUserDetailsService
, 这里面只有一个通过用户名去查找的, 但是第三方登录之后,我们只能通过 userconnection 表 通过 providerId
然后还有 第三方的 openId
去拿到我们业务数据库的 userid, 所以这里还需要一个通过 userId 去查询用户的方法
social提供了他自己的 UserDetailService
是 SocialUserDetailsService
,在之前的 MyUserDetailsService
上实现一下,修改后如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26@Component
@Slf4j
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// ... 省略部分代码
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
// 根据id去查询用户信息
User user = userService.getByUserId(Long.valueOf(userId));
return new SocialUser(user.getName(),
user.getPassword(),
user.getEnable(),
true,
true,
!user.getLocked(),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
配置
把用到的一些变量,写到配置类里面, 首先新建 QQProperties
然后继承 SocialProperties
1
2
3
4
5
6@Data
public class QQProperties extends SocialProperties {
private String providerId = "qq";
}
SocialProperties
只有两个属性 appId
和 appSecret
,然后自定义一个供应商id ,这里给个默认值是 qq
然后再写一个 SocialProperties
1 | @Data |
最后放到总的配置中 SecurityProperties
1
2
3
4
5
6
7
8
9
10
11@Data
@ConfigurationProperties(prefix = "core.security")
public class SecurityProperties {
// ... 省略其他配置
/**
* social 的配置
*/
private SocialProperties social = new SocialProperties();
}
之后,在 application.yml
中,配置好这些:1
2
3
4
5
6core:
security:
social:
qq:
app-id: xxxxx
app-secret: xxxxx
配置 ConnectionFactory
配置类写好之后, 把这些配置给到之前写好的 QQConnectionFactory
, 新建一个 QQAutoConfig
,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13@Configuration
@ConditionalOnProperty(prefix = "core.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialAutoConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Override
protected ConnectionFactory<?> createConnectionFactory() {
QQProperties qq = securityProperties.getSocial().getQq();
return new QQConnectionFactory(qq.getProviderId(), qq.getAppId(), qq.getAppSecret());
}
}
这里要注意 @ConditionalOnProperty(prefix = "core.security.social.qq", name = "app-id")
这个注解的作用是
只有找到前缀是 core.security.social.qq
然后名称是 name
的配置有值之后才会生效
浏览器配置
web环境如果想使用 social 的话,还需要把 social
的过滤器加到 security 的过滤器链中去,也就是要把 social
的配置引用过去,首先在 SocialConfig
中,加入以下配置:1
2
3
4@Bean
public SpringSocialConfigurer securitySocialConfigurer(){
return new SpringSocialConfigurer();
}
然后在浏览器的配置中 BrowserSecurityConfig
,就可以注入 用 apply()
方法引入了, 如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21@Configuration
public class BrowserSecurityConfig extends AbstractChannelSecurityConfig {
@Autowired
private SpringSocialConfigurer securitySocialConfigurer;
@Override
protected void configure(HttpSecurity http) throws Exception {
// web网页登录的配置
applyPasswordAuthenticationConfig(http);
http
.apply(validateCodeSecurityConfig)
.and()
.apply(smsAuthenticationSecurityConfig)
.and()
.apply(securitySocialConfigurer)
// ... 省略其他代码
}
}
页面配置
最后页面中需要一个三方登录的按钮1
<a href = 'auth/qq'>QQ登录</a>
auth/qq
这个路径,首先 auth
是social的拦截器要拦截前缀, 然后 qq
是我们设置的 providerId
总结
从 ServiceProvider
开始配置然后 ConnectionFactory
的配置, 然后是数据库方面的配置, 按照之前的图,一步一步搞好就ok
掌握 @ConditionalOnProperty
的用法