Spring Security-15-开发QQ登录(中)

上文中实现了 ServiceProvider 的部分, 然后剩下就是 ConnectionFactory 创建 Connection 的部分还有与数据量交互的部分

ConnectionFactory 构建

适配器实现

要构建 ConnectionFactory 就需要 ServiceProvider 和一个 ApiAdapter ,ServiceProvider 已经有了,那下面先把 ApiAdapter 实现一下

ApiAdapter 的作用,就是把我们获取回来的第三方的数据,和 social 的标准的数据做一个适配, 新建类 QQAdapter:

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
27
28
29
30
31
32
33
34
35
36
public class QQAdapter implements ApiAdapter<QQ> {

/**
* 测试当前的API是否可以访问
* @param api
* @return
*/
@Override
public boolean test(QQ api) {
return true;
}

/**
* 适配,把 ConnectionValues 需要的数据set进去
* @param api
* @param values
*/
@Override
public void setConnectionValues(QQ api, ConnectionValues values) {
QQUserInfo qqUserInfo = api.getUserInfo();
values.setDisplayName(qqUserInfo.getNickname());
values.setImageUrl(qqUserInfo.getFigureurl_qq_1());
// 用户在服务商的唯一id
values.setProviderUserId(qqUserInfo.getOpenId());
// 用户主页, QQ没有
values.setProfileUrl(null);
}

@Override
public UserProfile fetchUserProfile(QQ api) {
return null;
}

@Override
public void updateStatus(QQ api, String message) {
}

首先这个类实现了一个 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
14
public 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,这个是后面我们自定义去配置的, 然后把 ServiceProviderApiAdapter 都传过去就好了

数据库层的实现

ConnectionFactory 已经实现了,然后 Connection 就交给 ConnectionFactory 去创建, 最后还剩下的就是数据库层的实现, 这个直接做个配置就好了

首先,数据库需要一个表,sql如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE 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登录的流程就算完了, 接下来还有一些配置

之前我们写了一个自定义的 UserDetailServiceMyUserDetailsService, 这里面只有一个通过用户名去查找的, 但是第三方登录之后,我们只能通过 userconnection 表 通过 providerId 然后还有 第三方的 openId 去拿到我们业务数据库的 userid, 所以这里还需要一个通过 userId 去查询用户的方法

social提供了他自己的 UserDetailServiceSocialUserDetailsService,在之前的 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 只有两个属性 appIdappSecret ,然后自定义一个供应商id ,这里给个默认值是 qq

然后再写一个 SocialProperties

1
2
3
4
5
6
7
8
@Data
public class SocialProperties {

/**
* QQ的配置
*/
private QQProperties qq = new QQProperties();
}

最后放到总的配置中 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
6
core:
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 的用法

本文代码传送门