过滤器|微服务 & SSO

三种架构 前后端半分离架构 过滤器|微服务 & SSO
文章图片
前后端半分离架构.png 前后端分离架构 过滤器|微服务 & SSO
文章图片
前后端分离架构.png 将 Postman 升级成前端服务器 过滤器|微服务 & SSO
文章图片
将 Postman 升级成前端服务器.png 前后端分离架构 | 实现概述

  • admin 项目的角色是:前端服务器;
  • admin 项目原本应该是个 Node.js + Angular 的项目,这里使用 Springboot + Angular 来代替;
  • Angular Build 完的结果,直接放在 Springboot 的 java/main/resource/static 下;
  • admin 项目向 zuul 发送请求;
  • 在认证服务器的客户端应用列表中,要加上 admin 项目;
真实坏境下,Web 应用部署在 NodeJS 中,浏览器向 NodeJS 发请求,NodeJS 把请求发到 Zuul,Zuul 中完成限流、认证、审计、授权,完了调用业务系统;
SSO & Authorizatin code grant & 前端服务器
  • OAuth2 的 Authorizatin code grant 认证模式的实现,需要引入前端服务器;
  • 引入了前端服务器,就实现了 SSO;
  • 当前端服务器重启后,浏览器访问前端服务器,直接就是登录状态;因为在 Authorizatin code grant 认证模式下,登录的位置是认证服务器,只要登录服务器上的 Session 没过期,认证服务器就知道,从浏览器来的请求是谁,不用再输用户名和密码了,然后直接跳到客户端应用,如果之前颁发给这个用户的 Token 没有过期,就把之前的 Token 返回给前端服务器;如果无效,就生成一个新 Token 发给前端服务器;这带来的一个问题是,如果用户的登出,只是清空前端服务器的 Session,会导致用户无法登出;
  • 前端服务器可以有多个,任何一个登录成功,Session 信息都会存在认证服务器中,当浏览器再发登录请求到第二个前端服务器中,前端服务器去认证服务器中拿的 Token 都是一样的,这就是 SSO;
基于 Session 的 SSO 2 个 Session
  • 认证服务器的 Session,存的是用户信息和 Session ID,Session ID 返回给浏览器作为 Cookie,这个 Cookie 和前端服务器返回给浏览器的 Cookie(JSESSIONID) 不是同一个;
  • 前端服务器的 Session,存的是用户的 Token;
3 个有效期
  • 认证服务器 Session 的有效期,控制多长时间需要用户输一次用户名和密码;
  • 前端服务器 Session 的有效期,控制的是多长时间跳转一次认证服务器;
  • Token 的有效期,控制登录一次能访问多长时间的微服务;
优点
  • Session 信息都是存储在服务器里,不论是前端服务器还是认证服务器;
  • Token 信息和 Session 信息都存储在数据库中,可控性高,可以看到系统中所有的登录状态,向让谁下线就让谁下线;
  • 没有跨域问题,客户端应用不管部署在什么域名下都可以和认证服务器交互,基于 Token 的 SSO 会有同域的问题;
缺点
  • 复杂度高:2 套机制,Session + Token;
  • 性能比较低,占用应用服务器的内存存储 Session;
适用
  • 适用与百万用户以下或有一定规模的公司的内部管理系统;
Logout 时同步前端服务器 & 认证服务器的 Session
  • 点击 Logout 按钮,在前端服务器将 session 失效后,要向认证服务器发一个请求 http://auth.imooc.com:9090/logout?redirect_uri=http://admin.imooc.com:8080;
  • 重写 Spring Security 的 DefaultLogoutPageGeneratingFilter 过滤器,这个过滤器原本会生成一个是否确定登出的 Form 表单,重写后将其逻辑变成,Form 表单渲染好了之后,直接 Submit,并且通过一个 hidden 的 input 把参数 redirect_uri=http://admin.imooc.com:8080 提交给认证服务器的 Logout 逻辑;
  • 写一个 Logout 成功后要做什么的 Handler:OAuth2LogoutSuccessHandler,在 Logout 成功后,重定向到前端服务器的首页;
  • 把自己写的 OAuth2LogoutSuccessHandler 配置到认证服务器中:
@Configuration @EnableWebSecurity // 让安全配置生效 public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {// ...@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() // 这里可以配置自己的登录页 .httpBasic().and() // 自己写的,logout 成功以后的 handler .logout().logoutSuccessHandler(logoutSuccessHandler) ; }}

重要的事情说三遍
  • 跨域!跨域!跨域!
  • Logout 的 window.location.href = 'http://localhost:9090/logout?redirect_uri=http://localhost:8080' 和 Login 的 window.location.href = 'http://localhost:9090/oauth/authorize?' 的域名必须一样,否则,Logout 和 Login 带的 SESSION 会不一样,用户在数据库中的 session 信息删不掉,用户登出失败;
Token 的有效期 【过滤器|微服务 & SSO】Token 是短活的令牌,Token 过期的时候,可能 Session 还没过期。
refresh token 过滤器|微服务 & SSO
文章图片
refresh token.png
  • access_token:短生命周期;
  • refresh_token:长生命周期;
  • 客户端应用拿 refresh_token 不断的刷 access_token;
access_token vs access_token
  • 拿到 access_token 可以任意访问微服务,而且是合法的;
  • refresh_token 在使用的时候,必须和 client_id,client_secret 一起使用;
refresh_token 的启用
  • 字段 oauth_client_details.refresh_token_validity 必须要填,单位是 s,只要这个字段有值,在发 access_token 的同时,会发 refresh_token;
  • 去认证服务器刷新 Token 的客户端应用,在认证服务器中配置的信息的 authorized_grant_types 字段中,要加上 refresh_token
  • @EnableAuthorizationServer 的配置类中,要给 refresh_token 请求专门配置一个 userDetailsService:
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints // 这个 userDetailsService 是专门给 refresh_token 用的 .userDetailsService(userDetailsService) .tokenStore(tokenStore()) // 这个是支持前 4 种认证模式的:password, code, ... , ... .authenticationManager(authenticationManager); }

  • refresh_token 请求的 grant_typerefresh_token
refresh_token 过期了怎么办 方案一 | 强制 Logout
  • 当 refresh_token 失败后,返回前端 500,完了带一个专门标记 refresh_token 失败的标记;
  • 前端的 HttpClient 加一个拦截器,在请求之后拦截,如果收到 refresh_token 失败的标记,调用 Logout 接口,前端服务器中的 session 失效,认证服务器中的 session 失效,让用户重新登录;
方案二 | 让认证服务器决定
  • 直接请求认证服务器的 /oauth/authorize 接口,让认证服务器决定是否要重新登录;
  • 如果认证服务器中的 session 没过期,回调到前端服务器,前端服务器获取 Token 后存储在自己的 session 中;
  • 如果认证服务器中的 session 过期,让浏览器重定向到登录页面;
基于 Token 的 SSO 步骤
  • 浏览器访问首页,请求会先经过前端服务器的 CookieTokenFilter,发现请求既没有 access_token,也没有 refresh_token 后,会调用认证服务器的登录接口,认证服务器返回登录页,输入用户名密码后发请求到认证服务器登录;
  • 在认证服务器上登录成功后,重定向到前端服务器后,前端服务器获取到 Token 后,不存储在前端服务器本地的 session 中,而是将 Token(access_token, refresh_token) 作为 Cookie 返回给浏览器;
  • 浏览器重定向到首页,请求还是会经过 CookieTokenFilter,此时有 access_token,CookieTokenFilter 会把 access_token 放入 Header 中,然后放行到网关,请求进入网关限流拦截器,认证拦截器,审计拦截器,授权拦截器后进入 /user/me 拦截器,/user/me 拦截器判断请求的 Header 中有 username 信息(授权拦截器放进去的),返回 username 给前端,前端看到有返回数据,就进入登录后的页面;
  • 浏览器带着 Cookie(token) 访问 order 服务,经过 CookieTokenFilter 后,CookieTokenFilter 会把 token 放进 Header 中,经过网关的限流、认证、审计、授权后,会进入 order 服务,完成对 order 服务的访问;
  • 登出的时候,浏览器先把 Cookie 清空,然后访问认证服务器的登出接口,登出后,重定向到首页,首页会先访问 /user/me 接口,但是会被 CookieTokenFilter 拦截下来,CookieTokenFilter 发现没有 access_token 和 refresh_token,会访问认证服务器的登录接口,认证服务器返回登录页;
基于 Token 的 SSO vs 基于 Session 的 SSO
  • 基于 Session 的 SSO,每当前端服务器的 session 失效后,就要访问认证服务器去判断 session 是否过期,由认证服务器决定放回前端服务器旧的 Token,还是返回浏览器登录页;
  • 基于 Token 的 SSO,当浏览器中的 Cookie 失效后,才会去认证服务器一次认证;
  • 不管那种方案,在认证服务器上,都会有用户的 session, 基于 Session 的 SSO 需要认证服务器上的 session 有效期较长,这样才不会导致前端服务器频繁的访问认证服务器;基于 Token 的 SSO 不需要认证服务器上的 session 有效期很长,只要浏览器中 Cookie 有效,就能访问服务,哪怕认证服务器中的 session 已经失效,只要 Cookie 中的 access_token 没失效,任然可以继续访问微服务;
优点
  • 复杂度较低,只需要考虑两个 Token 过期要怎么处理就可以了;
  • 更适合海量用户的场景,比如有上千万的用户,不可能把这些用户的信息都存在 前端服务器的 session 中;
缺点
  • 安全性较低,access_token 存储在 Cookie 中了,可以使用 HTTPS 保证 Cookie 的传递,还有就是放在 Cookie 中的 Token 不会有很长的有效期;如果是 JWT 的话,浏览器中还会存用户信息,那样的安全风险更高 ;
  • 可控性较低,因为 access_token 是存储在浏览器的 Cookie 中,没法由管理人员手动失效掉 access_token;
  • 没法跨域,因为给浏览器的 Cookie 是有域名限制的,不在同一个一级域名下的前端应用,是无法做到 SSO 的;可以通过往返回给浏览器的 Cookie 中加多个域名;

    推荐阅读