【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下的验证结果当作最终结果的水货们请闭嘴】

重构社交登录

app里面的第三方登录不向浏览器中一样,一般是通过调用sdk,引导到第三方app应用登录后返回;

浏览器模式

202306181907520541.png

可能以下两种模式;

简化模式

202306181907556572.png


上图来看,拿到openId之后,只要我们支持使用openid登录,即可;

可以大部分模仿短信验证码登录的代码,只有一点不同,提交的openid是属于social表中的数据,
所以相关的用户信息SocialUserDetailsService和用户连接信息UsersConnectionRepository
需要通过 socaial提供的表来获取校验逻辑

先看配置

    package cn.mrcode.imooc.springsecurity.securityapp.social.openid;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.web.DefaultSecurityFilterChain;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.social.connect.UsersConnectionRepository;
    import org.springframework.social.security.SocialUserDetailsService;
    import org.springframework.stereotype.Component;
    
    /**
     * ${desc}
     * @author zhuqiang
     * @version 1.0.1 2023/8/8 15:59
     * @date 2023/8/8 15:59
     * @since 1.0
     */
    @Component
    public class OpenIdAuthenticationSecurityConfig
            extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
        @Autowired
        private SocialUserDetailsService userDetailsService;
    
        @Autowired
        private UsersConnectionRepository usersConnectionRepository;
    
        @Autowired
        private AuthenticationFailureHandler authenticationFailureHandler;
        @Autowired
        private AuthenticationSuccessHandler authenticationSuccessHandler;
    
        @Override
        public void configure(HttpSecurity builder) throws Exception {
            OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();
            provider.setUserDetailsService(userDetailsService);
            provider.setUsersConnectionRepository(usersConnectionRepository);
    
            OpenIdAuthenticationFilter filter = new OpenIdAuthenticationFilter();
            // 获取manager的是在源码中看到过
            filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
            filter.setAuthenticationFailureHandler(authenticationFailureHandler);
            filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
    
            // 需要一个服务提供商 和 一个过滤器
            builder.
                    authenticationProvider(provider)
                    .addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
        }
    }

服务商

    package cn.mrcode.imooc.springsecurity.securityapp.social.openid;
    
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.InternalAuthenticationServiceException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.social.connect.UsersConnectionRepository;
    import org.springframework.social.security.SocialUserDetailsService;
    
    import java.util.HashSet;
    import java.util.Set;
    
    /**
     * ${desc}
     * @author zhuqiang
     * @version 1.0.1 2023/8/8 16:14
     * @date 2023/8/8 16:14
     * @since 1.0
     */
    public class OpenIdAuthenticationProvider implements AuthenticationProvider {
        // 要使用social的
        private SocialUserDetailsService userDetailsService;
    
        private UsersConnectionRepository usersConnectionRepository;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // 这里和之前短信验证码登录 唯一不同的是
            // 这里是使用社交登录的userDetailsService 和 usersConnectionRepository
            // 因为只有社交信息里面才会存在相关信息
            OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
            Set<String> providerUserIds = new HashSet<>();
            providerUserIds.add((String) authenticationToken.getPrincipal());
            Set<String> userIds = usersConnectionRepository.findUserIdsConnectedTo(authenticationToken.getProviderId(), providerUserIds);
    
            if (CollectionUtils.isEmpty(userIds) || userIds.size() != 1) {
                throw new InternalAuthenticationServiceException("无法获取用户信息");
            }
    
            String userId = userIds.iterator().next();
    
            UserDetails user = userDetailsService.loadUserByUserId(userId);
    
            if (user == null) {
                throw new InternalAuthenticationServiceException("无法获取用户信息");
            }
    
            OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());
    
            authenticationResult.setDetails(authenticationToken.getDetails());
    
            return authenticationResult;
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
        }
    
        public SocialUserDetailsService getUserDetailsService() {
            return userDetailsService;
        }
    
        public void setUserDetailsService(SocialUserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    
        public UsersConnectionRepository getUsersConnectionRepository() {
            return usersConnectionRepository;
        }
    
        public void setUsersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
            this.usersConnectionRepository = usersConnectionRepository;
        }
    }

过滤器

    package cn.mrcode.imooc.springsecurity.securityapp.social.openid;
    
    import cn.mrcode.imooc.springsecurity.securitycore.properties.QQProperties;
    import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityConstants;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.util.Assert;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * ${desc}
     * @author zhuqiang
     * @version 1.0.1 2023/8/8 16:03
     * @date 2023/8/8 16:03
     * @since 1.0
     */
    public class OpenIdAuthenticationFilter extends
            AbstractAuthenticationProcessingFilter {
        // ~ Static fields/initializers
        // =====================================================================================
        private String openIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_OPEN_ID;
        // 服务提供商id,qq还是微信
        /** @see QQProperties#providerId */
        private String providerIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_PROVIDERID;
        private boolean postOnly = true;
    
        // ~ Constructors
        // ===================================================================================================
    
        public OpenIdAuthenticationFilter() {
            super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_OPEN_ID, "POST"));
        }
    
        // ~ Methods
        // ========================================================================================================
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request,
                                                    HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    
            String openId = obtainOpenId(request);
            String providerId = obtainProviderId(request);
    
            if (openId == null) {
                openId = "";
            }
            if (providerId == null) {
                providerId = "";
            }
            openId = openId.trim();
    
            OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(openId, providerId);
    
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
    
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    
    
        protected String obtainOpenId(HttpServletRequest request) {
            return request.getParameter(openIdParameter);
        }
    
        private String obtainProviderId(HttpServletRequest request) {
            return request.getParameter(providerIdParameter);
        }
    
        protected void setDetails(HttpServletRequest request,
                                  OpenIdAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }
    
        public void setOpenIdParameter(String openIdParameter) {
            Assert.hasText(openIdParameter, "Username parameter must not be empty or null");
            this.openIdParameter = openIdParameter;
        }
    
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        public final String getOpenIdParameter() {
            return openIdParameter;
        }
    }

token 实体

    package cn.mrcode.imooc.springsecurity.securityapp.social.openid;
    
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.SpringSecurityCoreVersion;
    
    import java.util.Collection;
    
    /**
     * ${desc}
     * @author zhuqiang
     * @version 1.0.1 2023/8/8 16:11
     * @date 2023/8/8 16:11
     * @since 1.0
     */
    public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {
    
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
        // ~ Instance fields
        // ================================================================================================
    
        private final Object principal;
        private String providerId;
    
        // ~ Constructors
        // ===================================================================================================
        public OpenIdAuthenticationToken(Object principal, String providerId) {
            super(null);
            this.principal = principal;
            this.providerId = providerId;
            super.setAuthenticated(true); // must use super, as we override
        }
    
        public OpenIdAuthenticationToken(Object principal,
                                         Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            super.setAuthenticated(true); // must use super, as we override
        }
    
        // ~ Methods
        // ========================================================================================================
    
    
        @Override
        public Object getPrincipal() {
            return this.principal;
        }
    
        @Override
        public Object getCredentials() {
            return null;
        }
    
        public String getProviderId() {
            return providerId;
        }
    
        @Override
        public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            if (isAuthenticated) {
                throw new IllegalArgumentException(
                        "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
            }
    
            super.setAuthenticated(false);
        }
    
        @Override
        public void eraseCredentials() {
            super.eraseCredentials();
        }
    
    
    }

资源服务器增加安全配置

    cn.mrcode.imooc.springsecurity.securityapp.MyResourcesServerConfig#configure
    .and().apply(openIdAuthenticationSecurityConfig)
    
    记得放行openid过滤器的拦截地址

测试

在数据库中imooc_userconnection找一个之前在网页qq授权登录的openid

    POST /authentication/openid HTTP/1.1
    Host: localhost:80
    Authorization: Basic bXlpZDpteWlk
    Content-Type: application/x-www-form-urlencoded
    
    openId=81F03E50B76D6D829F5A4875941567A6&providerId=qq

授权码模式

这个模式。只需要app端把拿到的授权码转发给服务器即可获得授权码;

这里需要注意的是: 这里拿到code。最终返回来的不是qq的accessToken,而是我们自己服务器的accessToken;

上面简化模式是客户端能直接拿到qq的accessToken和openid,

这里授权码模式是客户端只能拿到 code。还需要服务器去走social获取用户信息的步骤,

这里获取到qq的accessToken和openid后。我们拿着客户端传递的我们自己的client信息和这里获取到的openid;

然后走oath2的 token生成逻辑。最后返回

测试思路:

  1. demo引用浏览器环境
  2. 使用浏览器登录后,得到code
  3. 使用工具发送post请求到token地址,带上code和client信息
    org.springframework.social.security.provider.OAuth2AuthenticationService#getAuthToken
    
    public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
      String code = request.getParameter("code");
      if (!StringUtils.hasText(code)) {
        OAuth2Parameters params =  new OAuth2Parameters();
        params.setRedirectUri(buildReturnToUrl(request));
        setScope(request, params);
        params.add("state", generateState(connectionFactory, request));
        addCustomParameters(params);
        throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
      } else if (StringUtils.hasText(code)) {
        try {
          String returnToUrl = buildReturnToUrl(request);
          // 在这里打断点,使用浏览器模块访问qq登录后,会重定向到这里
          // 然后把服务器关闭掉
          // 浏览器中的地址就是带有code的
          AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
          // TODO avoid API call if possible (auth using token would be fine)
          Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
          return new SocialAuthenticationToken(connection, null);
        } catch (RestClientException e) {
          logger.debug("failed to exchange for access", e);
          return null;
        }
      } else {
        return null;
      }
    }

注意:在关闭服务时,需要点击 idea 左下角的 方块停止,需要快速点击,方块点完就会出现骷髅头,要连续快速点击,否则可能会导致断点继续往下走一会儿

202306181907590884.png

我们拿到浏览器中带code的地址,把服务切回app模块,然后在工具中带上client信息访问;

    GET /auth/qq?code=ACEB8728F5DE5B32F9C995BEFEB3C065&state=3cb4d5c7-60c6-4e88-b2a3-f34c5a8b176c HTTP/1.1
    Host: mrcode.cn
    Authorization: Basic bXlpZDpteWlk

我这里成功获取到了token(控制台打印的),但是postman中返回的错误信息

    {
        "error": "unauthorized",
        "error_description": "Full authentication is required to access this resource"
    }

我跟了源码,发现成功后跳转到了"/";但是为什么不是重定向而是异常? 这个有待跟踪源码了解下

自定义获取第三方用户信息成功后的逻辑处理

之前讲到可以获得过滤器更改自定义注册地址;这里过滤器里面也可以设置一个授权成功的自定义处理器

    cn.mrcode.imooc.springsecurity.securitycore.social.MySpringSocialConfigurer
    
    public class MySpringSocialConfigurer extends SpringSocialConfigurer {
        @Override
        protected <T> T postProcess(T object) {
            // org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess()
            // 在SocialAuthenticationFilter中配置死的过滤器拦截地址
            // 这样的方法可以更改拦截的前缀
            SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
            // filter.setFilterProcessesUrl("/oaths");
           filter.setAuthenticationSuccessHandler();  // 可以把处理器添加到这里
            return (T) filter;
        }
    }

现在来改造下

定义处理器接口,让使用处来实现,我们使用注解来获取初始化的bean

    package cn.mrcode.imooc.springsecurity.securitycore.social;
    
    import org.springframework.social.security.SocialAuthenticationFilter;
    
    /**
     * @author zhailiang
     *
     */
    public interface SocialAuthenticationFilterPostProcessor {
    
    	void process(SocialAuthenticationFilter socialAuthenticationFilter);
    
    }

编写配置文件,获取过滤器设置处理器

    
    package cn.mrcode.imooc.springsecurity.securitycore.social;
    
    import org.springframework.social.security.SocialAuthenticationFilter;
    import org.springframework.social.security.SpringSocialConfigurer;
    
    /**
     * @author : zhuqiang
     * @version : V1.0
     * @date : 2023/8/6 12:12
     */
    public class MySpringSocialConfigurer extends SpringSocialConfigurer {
        private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
    
        @Override
        protected <T> T postProcess(T object) {
            // org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess()
            // 在SocialAuthenticationFilter中配置死的过滤器拦截地址
            // 这样的方法可以更改拦截的前缀
            SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
    //        filter.setFilterProcessesUrl("/oaths");
    //        filter.setAuthenticationSuccessHandler();
            // 让使用处自己获取token成功的逻辑
            if (socialAuthenticationFilterPostProcessor != null) {
                // 在配置初始化的时候,把过滤器传递给使用方,让使用方把处理器注入
                socialAuthenticationFilterPostProcessor.process(filter);
            }
            return (T) filter;
        }
    
        public SocialAuthenticationFilterPostProcessor getSocialAuthenticationFilterPostProcessor() {
            return socialAuthenticationFilterPostProcessor;
        }
    
        public void setSocialAuthenticationFilterPostProcessor(SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor) {
            this.socialAuthenticationFilterPostProcessor = socialAuthenticationFilterPostProcessor;
        }
    }

配置文件引用

    cn.mrcode.imooc.springsecurity.securitycore.social.SpringSocialConfig
    
    @Autowired(required = false)
    private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
    
    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        // 默认配置类,进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        MySpringSocialConfigurer springSocialConfigurer = new MySpringSocialConfigurer();
        springSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
        // 通过注解获取到使用方注入的bean,给我们刚才写的配置类
        springSocialConfigurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
        return springSocialConfigurer;
    }

app实现该配置,配置处理成功的处理器

    /**
     *
     */
    package cn.mrcode.imooc.springsecurity.securityapp.social.impl;
    
    import cn.mrcode.imooc.springsecurity.securitycore.social.SocialAuthenticationFilterPostProcessor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.social.security.SocialAuthenticationFilter;
    import org.springframework.stereotype.Component;
    
    /**
     * @author zhailiang
     */
    @Component
    public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {
    
        @Autowired
        private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
    
        /**
         * @see cn.mrcode.imooc.springsecurity.securitycore.social.SocialAuthenticationFilterPostProcessor.process
         */
        @Override
        public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
            // 这里设置的其实就是之前  重构用户名密码登录里面实现的 MyAuthenticationSuccessHandler
            socialAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
        }
    
    }

最后再次测试,得到了我们自己系统的 accessToken

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值