重构注册逻辑
在浏览器中的第三方登录回顾:
- social 在拿到用户信息之后
- 查询数据库没有绑定的用户会跳转到默认的/signUp路径
- 提供了一个我们自己的注册页面,拿到用户提交的注册信息,调用social数据库服务,把关联信息写入数据库中。完成注册
- 再次登录,数据库中有用户信息,则登录成功
问题:
1. 上面这个流程问题所在就是 第三方的信息存放在了 session 中;
2. 还有一个问题,就是第2步会302.需要客户端信息判定并跳转到登录页
所以现在开始改造,改造方案:
- 流程完成后,更改跳转的页面到app指定页面,
- 根据设备id,我们把信息存放在redis中
- 用户注册完成后,提交,再把第三方信息拿出来,合并完成注册
改造
注意: 在改造测试之前把默认注册用户的功能关闭掉
也就是 com.example.demo.security.DemoConnectionSignUp 类
之前的注册地址是在
cn.mrcode.imooc.springsecurity.securitycore.social.SpringSocialConfig#imoocSocialSecurityConfig
@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
// 默认配置类,进行组件的组装
// 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
MySpringSocialConfigurer springSocialConfigurer = new MySpringSocialConfigurer();
springSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
springSocialConfigurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
return springSocialConfigurer;
}
中设置的,那么先把这个地址更改掉,由于这里在浏览器环境下工作得很好,不要直接修改这里。使用一个技巧替换掉
package cn.mrcode.imooc.springsecurity.securityapp;
import cn.mrcode.imooc.springsecurity.securitycore.social.SpringSocialConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.social.security.SpringSocialConfigurer;
import org.springframework.stereotype.Component;
/**
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/8 23:49
*/
@Component
public class SpringSocialConfigurerPostProcessor implements BeanPostProcessor {
// 任何bean初始化回调之前
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
//任何bean初始化回调之后
// 在这里把之前浏览器中配置的注册地址更改为app中的处理控制器
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
/**
* @see SpringSocialConfig#imoocSocialSecurityConfig()
*/
if (beanName.equals("imoocSocialSecurityConfig")) {
SpringSocialConfigurer config = (SpringSocialConfigurer) bean;
config.signupUrl("/social/signUp");
return bean;
}
return bean;
}
}
编写处理跳转接收的控制器;用户把信息传递给前段,引用用户注册;
这里的流程还是之前的拿到code,带着client获得我们系统的accessToken信息
由于数据库中没有该openid的用户信息,所以是未授权状态。
这里先简单写下,然后测试看是否能跳转到这里来。是否能从session中获取到第三方信息;
package cn.mrcode.imooc.springsecurity.securityapp;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletRequest;
/**
* 处理登录的控制器
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/8 23:56
*/
@RestController
public class AppSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ProviderSignInUtils providerSignInUtils;
@GetMapping(value = "/social/signUp")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Connection signUp(HttpServletRequest request) {
Connection<?> connectionFromSession = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
logger.info(ReflectionToStringBuilder.toString(connectionFromSession, ToStringStyle.JSON_STYLE));
return connectionFromSession;
}
}
我使用postman,跟踪源码的确是走了Redirect;但是postman里面没有302状态,
直接走到上面的控制器里面去了。。搞不明白啊;
一直有一个疑惑,不是说app没有session吗?302的话相当于ajax响应。再次发起请求不是同一个session了,怎么拿到信息的呢?
回答:
postman中settings中有一个选项 Automatically follow redirects;关闭掉也就是变成OFF,就不会自动跳转了关于 /social/signUp 能获取到session信息,也就是302能获取到session:
是因为postman中有服务器带回来的cookie,禁止掉cookie,就会发现获取不到了
// connection unknown, register new user?
if (signupUrl != null) {
// store ConnectionData in session and redirect to register page
sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
}
流程测试通了。来把第三方信息存储在redis中,完成解析来的功能
改造第三方信息存储redis中
utils中的写法 参考 ProviderSignInUtils
package cn.mrcode.imooc.springsecurity.securityapp.social;
import cn.mrcode.imooc.springsecurity.securityapp.AppConstants;
import cn.mrcode.imooc.springsecurity.securityapp.AppSecretException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
/**
* @author zhuqiang
* @version 1.0.1 2018/8/9 14:28
* @date 2018/8/9 14:28
* @see ProviderSignInUtils 模拟其中部分的功能
* @since 1.0
*/
@Component
public class AppSignUpUtils {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
// 目前为止都是自动配置的,直接获取即可
@Autowired
private UsersConnectionRepository usersConnectionRepository;
@Autowired
private ConnectionFactoryLocator connectionFactoryLocator;
public void saveConnection(ServletWebRequest request, ConnectionData connectionData) {
redisTemplate.opsForValue().set(buildKey(request), connectionData);
}
/**
* @param userId
* @param request
* @see ProviderSignInAttempt#addConnection(java.lang.String, org.springframework.social.connect.ConnectionFactoryLocator, org.springframework.social.connect.UsersConnectionRepository)
*/
public void doPostSignUp(String userId, ServletWebRequest request) {
String key = buildKey(request);
ConnectionData connectionData = (ConnectionData) redisTemplate.opsForValue().get(key);
usersConnectionRepository.createConnectionRepository(userId).addConnection(getConnection(connectionFactoryLocator, connectionData));
}
public Connection<?> getConnection(ConnectionFactoryLocator connectionFactoryLocator, ConnectionData connectionData) {
return connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId()).createConnection(connectionData);
}
private String buildKey(ServletWebRequest request) {
String deviceId = request.getHeader(AppConstants.DEFAULT_HEADER_DEVICE_ID);
if (StringUtils.isBlank(deviceId)) {
throw new AppSecretException("设备id参数不能为空");
}
return "imooc:security:social.connect." + deviceId;
}
}
改造相关代码处,使用写好的工具类
package cn.mrcode.imooc.springsecurity.securityapp;
import cn.mrcode.imooc.springsecurity.securityapp.social.AppSignUpUtils;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletRequest;
/**
* 处理登录的控制器
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/8 23:56
*/
@RestController
public class AppSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ProviderSignInUtils providerSignInUtils;
@Autowired
private AppSignUpUtils appSignUpUtils;
@GetMapping(value = "/social/signUp")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ConnectionData signUp(HttpServletRequest request) {
Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
// 这里还不能直接放 Connection 因为这个里面包含了很多对象
ConnectionData connectionData = connection.createData();
logger.info(ReflectionToStringBuilder.toString(connection, ToStringStyle.JSON_STYLE));
appSignUpUtils.saveConnection(new ServletWebRequest(request), connectionData);
// 注意:如果真的在客户端无session的情况下,这里是复发获取到providerSignInUtils中的用户信息的
// 因为302重定向,是客户端重新发起请求,如果没有cookie的情况下,就不会有相同的session
// 教程中这里应该是一个bug
// 为了进度问题,先默认可以获取到
// 最后要调用这一步:providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
// 那么在demo注册控制器中这一步之前,就要把这里需要的信息获取到
// 跟中该方法的源码,转换成使用redis存储
return connectionData;
}
}
注册的地方也要更改
com.example.demo.web.controller.UserController#regist
@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {
//不管是注册用户还是绑定用户,都会拿到一个用户唯一标识。
String userId = user.getUsername();
appSignUpUtils.doPostSignUp(userId, new ServletWebRequest(request));
}
测试
测试时候需要不停的在浏览器和app之间切换
这里把qq登录页的地址复制下来。可以在项目关闭下扫码登录后,再启动项目,把code拿到工具中继续接下来的流程
获取到code后,用工具访问以下地址,(如果设置了自动跳转302)则不需要再手动访问一次 /social/signUp了
如果手动访问/social/signUp的话,还是在刚在那个窗口访问,因为有相同的sessionId;
需要带上client和设备id信息
GET /auth/qq?code=D93FEF61930FCCC3C0339935B70B1215&state=03ff3841-295a-4b03-8bbf-36ef353c146a HTTP/1.1
Host: mrcode.cn
Authorization: Basic bXlpZDpteWlk
deviceId: 1
返回用户信息后,提交注册用户,完成绑定第三方登录的账户
POST /user/regist HTTP/1.1
Host: mrcode.cn
Authorization: Basic bXlpZDpteWlk
deviceId: 1
Cache-Control: no-cache
Postman-Token: 4195a53a-8d2c-4417-94ec-b1252f9e5285
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
admin
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="password"
123456