SpringSecurity基于OAuth2协议实现第三方登录源码剖析

1. 基础介绍

1.1 应用场景

  1. 目前互联网系统应用登录方式除了常规的账号密码登录、手机验证码登录方式之外,通过第三方平台进行认证登录的情景也逐渐成为主流的登录方式;
  2. 在国内通过第三方登录主流的平台有QQ、微信、微博等;

1.2 OAuth2.0协议

  1. 一句话解释:OAuth2.0是目前主流的授权机制,用来授权第三方应用,目的是从第三方获取用户数据。

  2. OAuth2.0 与 1.0 的区别:国外系统用得比较多的还是OAuth1.0,国内因为这种授权机制普及的时间比较晚,所以国内主流用的还是OAuth2.0;(本文不细剖协议內部细节)

  3. OAuth认证流程中涉及的主体对象:

    1. User: 用户
    2. 第三方Client:即我们自己开发的系统
    3. 服务提供商Provider:
      1. 认证服务器(Authentication Server):与第三方应用交互,完成授权认证流程
      2. 资源服务器(Resources Server):存放用户数据

    SpringSecurity基于OAuth2协议实现第三方登录源码剖析_第1张图片

  4. 用户授权方式:

    1. **授权码模式:**最普遍的授权模式
      1. 将用户授权导向认证服务器
      2. 用户告诉认证服务器同意授权
      3. 认证服务器返回client并携带授权码
      4. 功能最完整,流程最严密
    2. 密码模式
    3. 客户端模式
    4. 简化模式
      1. 在授权码模式中返回会的授权码直接变为令牌
      2. 直接将令牌返回到第三方的浏览器中,这种第三方通常没有服务器;
        SpringSecurity基于OAuth2协议实现第三方登录源码剖析_第2张图片

2. 第三方认证流程框架流程

2.1 自定义实现涉及的核心组件

  1. Connection:存放从服务提供商获取的用户信息;
  2. UserConnection:可以简单理解为一张数据库表,维护第三方系统的用户信息与服务提供商用户信息的关联关系
  3. UserConnectionRepository:负责对UserConnection表进行增删改查,以维护用户关系;这个例子用到的是JdbcUserConnectionRepository的具体实现;
  4. ConnectionFactory:负责授权认证流程,最终创建Connection;并且由两个部分组成
    1. ServiceProvider
      1. OAuth2Operations: 负责完成封装授权的参数,并带着授权码去申请令牌的过程
      2. Api:负责向服务提供商获取用户信息
    2. ApiAdapter:负责将从服务提供商获取的信息放入ConnectionValues中,将被拿去封装成connection;

SpringSecurity基于OAuth2协议实现第三方登录源码剖析_第3张图片

2.2 源码执行过程梳理

  1. 用户通过第三方client登录页面点击QQ登录,访问登录链接,如/qqLogin/qq;这条链接由两部分组成:
    1. 第一部分/qqLogin:通过配置SocialAuthenticationFilter中的filterProcessesUrl`属性
    2. 第二部分qq: 通过配置OAuth2ConnectionFactory中的providerId属性
  2. 用户点击该路径发起请求后,将经过SocialAuthenticationFilter,该过滤器的doFilter方法将根据requiresAuthentication方法判断该链接是否符合要求;即是否以filterProcessesUrl为开头,并且是否能够在authServiceLocator中加载到对应的ProviderId;
  3. 接着过滤器准备加载该providerId对应的SocialAuthenticationService;
    1. SocialAuthenticationService初始化的时候会通过SocialAuthenticationServiceRegistry注册器进行注册
    2. 在注册器中有一个以providerId作为key,SocialAuthenticationService作为值的Map来管理认证服务;
  4. 过滤器在拿到对应的SocialAuthenticationService后将执行其getAuthToken以获得认证后的SocialAuthenticaionToken;
  5. getAuthToken是一个重要的方法,这里面完成了授权码的获取以及令牌的申请过程;(即完成授权码模式认证流程第2、4、5、6步)
  6. 将用户请求导向认证服务器并获得授权码的过程是通过捕获重定向异常,在请求中没有获得code值则抛出重定向异常,从处理异常过程中来请求获取授权码;(即完成授权码模式认证流程第2步)
  7. 授权请求后,服务提供商将通过我们创建应用时填写的回调地址返回给我们的应用,继续被SocialAuthenticationFilter拦截,回到getAuthToken方法中;
  8. 第一次进入getAuthToken的请求参数中是没有携带code(即授权码),第二次进入的时候才携带了code;
  9. 因此在第一次进入的时候就会抛出一个携带authorizeUrl的异常,SocialAuthenticationFailureHandler登录失败处理器则会将该异常进行捕获,并根据异常携带的链接进行重定向,向认证服务器请求授权码;
  10. 第二次进入getAuthToken的时候则是通过认证服务器调用创建应用时配置的回调地址发起的的,此时请求中已经携带了授权码,进入令牌申请阶段;(即完成授权码模式流程第4步)
  11. getAuthToken中通过getConnectionFactory().getOAuthOperations()拿到OAuth2Template,执行exchangeForAccess封装申请令牌请求所需要的参数,并通过调用postForAccessGrant方法发起令牌申请并处理响应结果;(即完成授权码模式第5,6步)
  12. 此时返回的结果已经可以拿到access_token了,即最终的令牌;将该令牌进行封装,并通过ConnectionFaccorycreateConnection方法构建connection,并封装到SocialAuthenticationToken中;(即组件执行流程第3、4步)
  13. 拿到SocialAuthenticaionToken之后则走类似用户认证一样的流程,通过ProviderManager找到能够support匹配的provider;SocialAuthenticaionToken对应的Provider即SocialAuthenticationProvider; (即组件执行流程第5、6步)
  14. 通过Provider的authenticate方法调用usersConnectionRepository去数据库查出与之关联的用户ID;(即组件执行流程第7、8步)
  15. 找到用户ID后则去调用实现了SocialUserDetailService的类,执行loadUserByUserId()方法,获得用户信息;(即组件执行流程第9步)

SpringSecurity基于OAuth2协议实现第三方登录源码剖析_第4张图片

2.3 无法获得用户UserConnection对应关系如何处理?

2.3.1 配置注册页面

  1. 如果UsersConnectionRepository查找数据库找不到对应的userId,则会抛出BadCredentialsException异常

  2. 抛出BadCredentialsException,该异常判断如果signUp注册页面路径不为空时则抛出跳转异常,跳转到用户配置的注册页面;

  3. 编写自定义注册页面,并通过SpringSocialConfigure配置注册页面路径;

@Bean
public SpringSocialConfigurer systemSpringSocialConfigurer() {
    String filterProcessesUrl = systemSecurityProperties.getSocial().getFilterProcessesUrl();
    SystemSpringSocialConfigurer configurer = new SystemSpringSocialConfigurer(filterProcessesUrl);
    //配置注册页面
    configurer.signupUrl("signIn.html");
    return configurer;
}
  1. 在页面上拿到授权用户信息:

    1. 编写控制器,通过Connection connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));,从request中拿到Connection信息;自行进行封装并返回给页面;
    2. 这个用户认证信息(Connection)什么时候放入request中的? 在执行跳转注册页面的异常之前,将用户认证数据放入了request中;
  2. 在注册接口中获得授权获得的信息并进行绑定操作:

    1. 在用户注册接口中进行业务逻辑编写,在保存完本系统的用户信息后返回一个唯一标识
    2. 通过ProviderSignInUtil工具类将该唯一标识与openId进行绑定
    3. providerSignInUtils.doPostSignUp("userId",new ServletWebRequest(request));
    4. 记得将用户注册接口放行

2.3.2 无需注册用户(自动绑定)

  1. SocialAuthenticationProvider中会调用usersConnectionRepository去数据库查找关联信息
  2. 当查询用户的信息为空时,还会根据另一个参数(ConnectionSignUp)是否为空去判断是否跳转到注册页面;
  3. 如果不想用户显式地去注册地话,可以通过在UsersConnectionRepository中配置实现了ConnectionSignUp的对象
  4. ConnectionSignUp在这个地方可以自己实现接口,基于社交用户信息编写用户创建逻辑
@Component
public class SystemConnectionSignUp implements ConnectionSignUp {
    @Override
    public String execute(Connection<?> connection) {
        //根据社交用户信息创建用户,并返回用户唯一标识
        //DEMO 
        return "id";//
    }
}
  1. 记得在UsersConnectionRepository中进行配置

你可能感兴趣的:(SpringSecurity)