掘金 后端 ( ) • 2024-04-08 15:10

theme: cyanosis highlight: vs2015

oauth2

引言

读了《设计模式之美》和《凤凰架构》架构安全篇之后,决定写一个OAuth2.0的认证流程的Demo,也算是一个阶段性的总结,具体原理实现见《凤凰架构》( 架构安全设计篇)。

涉及到的源码可以从https://github.com/WeiXiao-Hyy/oauth2获取,欢迎Star!

OAuth2.0原理

主要解决的问题

面向解决第三方应用(Third-Party Application)的认证授权协议,使用Token代替用户密码作为授权的凭证。

  1. 有了令牌之后,哪怕令牌被泄漏,也不会导致密码的泄漏
  2. 令牌上可以设定访问资源的范围以及时效性
  3. 每个应用都持有独立的令牌,哪个失效都不会波及其他

OAuth2.0一共提出了四种不同的授权方式:

  • 授权码模式(Authorization Code)
  • 隐式授权模式(Implicit)
  • 密码模式(Resource Owner Password Credentials)
  • 客户端模式(Client Credentials)

本文介绍和实现的是授权码方式。

OAuth2.0流程图

  1. client请求授权服务端,获取Authorization Code;
  2. client通过Authorization Code再次请求授权服务端,获取Access Token;
  3. client通过服务端返回的Access Token获取用户的基本信息。

流程图如下所示:

oauth过程图.png

第一次使用github账号应用来登陆掘金,需要进行授权

掘金即是第三方应用, 请求GitHub。

oauth授权页面.png

注册一个新的OAuth Application

如果我们需要使用GitHub账号来关联我们自己的第三方应用则需要完整走一遍OAuth2.0流程。

注册一个新的OAuth APP.png

GitHub生成的client_id, client_secret(密钥), redirect_uri, homepage_url, application_name等等。

OAuth相关信息.png

clientId如何生成唯一的

在注册clientId以及申请clientSecret时,如何保证生成的clientId是不重复的呢?

和JWT以及Cookie-Session对比

参考《凤凰架构》架构安全篇 https://icyfenix.cn/architect-perspective/general-architecture/system-security/authorization.html

谈谈State参数为什么可以防止CSRF攻击

观察GitHub OAuth2.0实现文档,可以观察到在authroize接口需要传递一个state参数,并且在redirect_uri重定向时原封不动传递回来,所以为什么可以防止CSRF攻击呢?

GitHub认证流程.png

核心: 在于授权服务端进行token请求绑定时,会从session将本次会话的账号与生成access_token进行绑定,而对于用户是谁并不关心。

案例

CSRF攻击原理.png

  1. 用户B登录掘金网站,并且选择绑定自己的GitHub账号;
  2. 掘金网站将用户B重定向到GitHub,由于他之前已经登录过GitHub,所以GitHub直接向他显示是否授权掘金访问的页面;
  3. 用户B在点击"同意授权"之后,截获GitHub服务器返回的含有authorization code参数的HTTP响应;
  4. 用户B精心构造一个Web页面,它会触发掘金网站向GitHub发起令牌申请的请求,而这个请求中的authorization code 参数正是上一步截获到的code;
  5. 用户B将这个Web页面放到互联网上,等待或者诱骗受害者用户A来访问;
  6. 用户A之前登录了掘金 网站,只是没有把自己的账号和其他社交账号绑定起来。在用户A访问了用户B准备的这个Web页面,令牌申请流程在用户A的浏览器里被顺利触发,掘金 网站从GitHub那里获取到access_token,但是这个token以及通过它进一步获取到的用户信息却都是攻击者用户B;
  7. 掘金网站将用户B的GitHub账号同用户A的掘金账号关联绑定起来,从此以后,用户B就可以用自己的GitHub账号通过OAuth 登录到用户A在掘金网站中的账号,堂而皇之的冒充用户A的身份执行各种操作;

代码实现(知识碎片总结)

本部分将开发过程中遇到的难点记录下来,具体源码参考此repo!

postman 请求共享session问题

本文为单体应用,使用了HttpServletRequestHttpSession方便开发,将相关的数据保存在服务器的Session中,但是Postman在发送请求的时候, 不会将其视为同一个连接,导致获取不到Session中的数据,需要每次将用户登陆的Cookie赋值到相应的接口上才能获取到Session中的数据。

postman请求.png

实现HandlerInterceptor接口完成请求过滤

涉及到接口请求过滤条件时,可以通过实现WebMvcConfigurer接口来添加过滤规则,例如:

  1. 授权前用户没有登陆则需要重定向到登陆页面
  2. 如果access_token过期,则需要重新获取
  3. ......
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    //注意注入bean的写法
    @Bean
    public OauthInterceptor oauthInterceptor() {
        return new OauthInterceptor();
    }

    @Bean
    public AuthAccessTokenInterceptor accessTokenInterceptor() {
        return new AuthAccessTokenInterceptor();
    }

    @Bean
    public LoginInterceptor loginInterceptor() {
        return new LoginInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor()).addPathPatterns("/user/**", "/oauth2.0/authorizePage", "/oauth2.0/authorize", "/sso/token");
        registry.addInterceptor(oauthInterceptor()).addPathPatterns("/oauth2.0/authorize");
        registry.addInterceptor(accessTokenInterceptor()).addPathPatterns("/api/**");
    }
}

参考资料