重构注册逻辑

在浏览器中的第三方登录回顾:

  1. social 在拿到用户信息之后
  2. 查询数据库没有绑定的用户会跳转到默认的/signUp路径
  3. 提供了一个我们自己的注册页面,拿到用户提交的注册信息,调用social数据库服务,把关联信息写入数据库中。完成注册
  4. 再次登录,数据库中有用户信息,则登录成功

问题:
1. 上面这个流程问题所在就是 第三方的信息存放在了 session 中;
2. 还有一个问题,就是第2步会302.需要客户端信息判定并跳转到登录页

所以现在开始改造,改造方案:

  1. 流程完成后,更改跳转的页面到app指定页面,
  2. 根据设备id,我们把信息存放在redis中
  3. 用户注册完成后,提交,再把第三方信息拿出来,合并完成注册

改造

注意: 在改造测试之前把默认注册用户的功能关闭掉
也就是 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