述
上文中,最后QQ授权成功,获取到用户信息之后,是跳转到了一个 /sign/up
的请求上去, 下面首先看一下为什么会跳转到注册页面上去
首先,按照 security 的登录流程, 上文中获取到了 QQ 信息,然后组成一个未认证的 SocialAuthenticationToken
对象, 然后之后就是通过 AuthenticationManager
找到对应的 Provider
来做具体的验证, 在 social 中, Provider
的具体实现是 SocialAuthenticationProvider
, 部分源码如下:
这里就是 SocialAuthenticationProvider
中的具体的认证流程, 首先会拿到 Connection
对象,也就是我们获取回来的第三方用户信息, 然后调用了一个 toUserId()
方法,这个方法会从数据库的 userconnection 表中,查询这个第三方用户关联的我们业务系统中的id,但是我们现在这个表中并没有数据,所以会返回空, 然后抛出 BadCredentialsException
异常,标识该用户还没有在我们的业务系统中绑定
然后再上层的 SocialAuthenticationFilter
代码如下:
这里会捕获到认证时候抛出来的 BadCredentialsException
异常,然后判断有没有配置登录页,有的话,就会抛出异常,跳转到注册页
问题已经找到了,下面看一下如何去解决,以及做一些配置
设置注册页面的路径
注册页默认的跳转路径是 /signUp
, 需要把这个做成一个可配置项,这个配置在之前的 SocialConfig
中1
2
3
4
5
6
7@Bean
public SpringSocialConfigurer securitySocialConfigurer(){
String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
MySpringSocialConfigurer mySpringSocialConfigurer = new MySpringSocialConfigurer(filterProcessesUrl);
mySpringSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
return mySpringSocialConfigurer;
}
在我们自定义的 MySpringSocialConfigurer
中设置, 是从配置中读取过来的
所以在 BrowserProperties
中,需要加上注册的跳转路径:1
2
3
4/**
* 跳转注册的路径
*/
private String signUpUrl = "/signUp";
默认的是 /signUp, 我们这里可以提供一个默认的处理方式, web 项目中,写一个 SignUpController
,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22@RestController
@RequestMapping("/signUp")
public class SignUpController {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
@RequestMapping(produces = "text/html")
public void signUpHtml(HttpServletRequest request, HttpServletResponse response) throws IOException {
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getSignUpPage());
}
@RequestMapping
@ResponseStatus(HttpStatus.PRECONDITION_REQUIRED)
public Map<String, Object> signUpJson() {
Map<String, Object> resultMap = new HashMap<>(1);
resultMap.put("message", "用户未注册,请引导用户到注册页面....");
return resultMap;
}
}
这里和之前的 /authentication/require
一样,判断是哪里来的请求,做出不同的跳转, 然后这里跳转的登录页也做成可配置的,同时提供一个默认的页面, 这个默认的页面就提示一下用户配置自己的注册就可以了,因为每个调用方的注册可能都是不一样的,所以由调用方自己实现就好了
在demo项目中加入配置1
2
3
4core:
security:
browser:
signUpPage: /mySignUp.html
然后提供一个注册页面,如下:
这里提供了一个注册和一个绑定的按钮, 因为用户可能之前就有这个网站的账号,只是第一次使用第三方登录,所以就需要一个绑定的接口
最后在 BrowserSecurityConfig
中,记得把登录的跳转路径和登录页面的路径加到不需要权限里面去
这时候,启动项目,然后QQ登录,授权之后跳转到的应该是注册的页面,如下:
获取第三方用户信息
在注册/绑定的时候,可能会用到第三方的用户信息, 比如显示第三方用户的昵称或者头像等信息,这些信息可以通过 social 提供的一个工具 ProviderSignInUtils
来获取到,这个工具类再后面做绑定的时候也会用到
首先需要做一下配置, 在 SocialConfig
加入以下代码:1
2
3
4@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator));
}
之后就可以通过注入来使用这个工具类了
web项目里新建一个获取 social 用户信息的 controller,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20@RestController
public class SocialUserController {
@Autowired
private ProviderSignInUtils providerSignInUtils;
@GetMapping(SecurityConstants.GET_SOCIAL_USER_URL)
private SocialUserInfo getSocialUser(HttpServletRequest request){
SocialUserInfo userInfo = new SocialUserInfo();
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;
}
}
然后把这个路径也加到不需要权限的路径里面去, 启动项目访问效果如下:
这个原理就是从session中去拿 Connection
对象, 看本文第二张图, social 会在重定向注册路径之前,把获取 Connection
先存在session里面
注册逻辑处理
上面的注册页面中,表单是发送到了 /user/register
这个请求里,所以我们还需要这个接口,来处理用户登录的具体逻辑,注册逻辑还是由调用方去实现,所以还是写到 demo 项目中去1
2
3
4
5
6
7
8
9
10
11
12
13
14
15@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private ProviderSignInUtils providerSignInUtils;
@PostMapping("/register")
public void register(User user, HttpServletRequest request){
// ..省略和数据库的交互
// 最终要拿到业务系统中的 用户的唯一标识
providerSignInUtils.doPostSignUp(String.valueOf(1L), new ServletWebRequest(request));
}
}
这里不管是注册还是绑定, 最终都会拿到一个业务系统中的唯一标识,然后通过调用 providerSignInUtils
的 doPostSignUp()
方法来完成注册的,这个方法会往 userconnection 表里添加数据,把业务系统的用户和第三方的用户做关联,我这里先写死用户id是1的数据了
最后要把 /user/register
添加到不需要权限的路径里面, 这个其实是调用方自定义的路径,正常来说不应该去配到 BrowserSecurityConfig
里,但是这里为了方便,先这样写,后面再去优化
这次启动项目,使用QQ登录,然后授权,跳转到注册页面之后,点注册,这时候再userconnection表中就会有一条记录1
2
3userId providerId providerUserId rank displayName profileUrl imageUrl accessToken secret refreshToken expireTime
------ ----------- -------------------------------- ------ ----------- ---------- ----------------------------------------------------------------------------- -------------------------------- ------ -------------------------------- ---------------
1 callback.do 1B815954CDDAA9E29193E3E185FB293F 1 周。 (NULL) http://thirdqq.qlogo.cn/g?b=oidb&k=cKomQts7QiascqicHy2ZY4lw&s=40&t=1554802302 1582672832E0F1AA99B95059A8A72DA8 (NULL) 4645CB01B0C04604E2B1AB6F0CEC7E5C 1574064303538
这次已经绑定了业务系统中的id, 下次登录的时候,就会通过 providerId
和 providerUserId
查询到业务系统的id,然后进入 SocialUserDetailsService
通过业务系统的id去查询用户,就不会再跳转到登录页面了
踩坑记录
这里我在运行的过程中有遇到一个问题,就是 SocialConfig
中配置的 UsersConnectionRepository
没有生效,导致去数据库查询的时候用了它的默认实现 InMemoryUsersConnectionRepository
,然后就导致每次去数据库查询都查不到, 原因是类加载的时候顺序出了点问题, 这个 SocialConfig
先注册了,没等 JdbcUsersConnectionRepository
注册就结束了,导致 UsersConnectionRepository
只能使用默认的实现了
我这里解决方案是 在 SocialConfig
类上加一个 @Order(10)
注解,让他晚点注册
不需要注册的场景
上面的场景中,是用户去第三方登录,然后我们业务系统中没有对应的用户,就跳转注册页面让用户去注册,其实在很多的系统中都是第三方可以直接登录的,并不需要去创建业务系统中的用户,这样的话用户体验会更好
这里的实现原理,就是在登录的时候,发现业务系统中没有对应的用户,就我们后台去给他生成一个用户
首先来看一个方法,是 SocialAuthenticationProvider
中,调用 toUserId()
去查询用户id的方法里面的 findUserIdsWithConnection()
,代码如下:
这里首先会去数据库里面查询,如果没有查到的话,那就是没有这个用户,他底下有个判断,是在没有查询到,而且 connectionSignUp
不为空的情况下, 去调用 connectionSignUp
的 execute()
方法,然后返回一个新用户的id, 创建完成后把新用户的id返回去
connectionSignUp
是一个接口,里面之后一个 execute()
方法,我们要做的就是实现这个接口
这个实现也应该由调用方去决定是否去实现,所以 也写在demo项目中, 新建 MyConnectionSignUp
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@Component
public class MyConnectionSignUp implements ConnectionSignUp {
@Autowired
private UserService userService;
@Override
public String execute(Connection<?> connection) {
// 这里应该去根据自己的业务去创建一个用户,并返回用户的唯一标识
User user = new User(6L, connection.getDisplayName(), "123456", false, true);
userService.insert(user);
return user.getId().toString();
}
}
这里能拿到的就是 Connection
对象,也就是第三方用户的一些信息,然后可以结合自己的业务,创建一个新的用户,反正最终要返回业务系统中的用户唯一标识
最后需要在构建 UsersConnectionRepository
的时候把 ConnectionSignUp
的实现设置进去,在 SocialConfig
中代码如下:1
2
3
4
5
6
7
8
9
10
11
12@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 设置表的前缀
// jdbcUsersConnectionRepository.setTablePrefix("");
jdbcUsersConnectionRepository.setConnectionSignUp(connectionSignUp);
return jdbcUsersConnectionRepository;
}
这里需要注意的是 @Autowired
要设置成非必须的,因为调用方可能并不会去实现这个接口
测试
以上配置完成之后,在 UserConnection
表中,把之前测试的数据清空掉,然后重启项目授权,这时候就不会去跳转注册页去了,而且会直接在 UserConnection
表里面生成一条新的数据