【Spring Security OAuth2】- App认证框架- 重构注册逻辑

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

重构注册逻辑

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

  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 : 2023/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 : 2023/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 2023/8/9 14:28
     * @date 2023/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 : 2023/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拿到工具中继续接下来的流程
QQ帐号安全登录

获取到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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值