2023-06-18
原文作者:代码有毒 mrcode 原文地址:https://mrcode.blog.csdn.net/article/details/81502532

重构注册逻辑

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

  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拿到工具中继续接下来的流程
https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101316278&response_type=code&redirect_uri=http%3A%2F%2Fmrcode.cn%2Fauth%2Fqq&state=03ff3841-295a-4b03-8bbf-36ef353c146a

获取到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
阅读全文