三种架构
前后端半分离架构
文章图片
前后端半分离架构.png 前后端分离架构
文章图片
前后端分离架构.png 将 Postman 升级成前端服务器
文章图片
将 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,存的是用户信息和 Session ID,Session ID 返回给浏览器作为 Cookie,这个 Cookie 和前端服务器返回给浏览器的 Cookie(JSESSIONID) 不是同一个;
- 前端服务器的 Session,存的是用户的 Token;
- 认证服务器 Session 的有效期,控制多长时间需要用户输一次用户名和密码;
- 前端服务器 Session 的有效期,控制的是多长时间跳转一次认证服务器;
- Token 的有效期,控制登录一次能访问多长时间的微服务;
- Session 信息都是存储在服务器里,不论是前端服务器还是认证服务器;
- Token 信息和 Session 信息都存储在数据库中,可控性高,可以看到系统中所有的登录状态,向让谁下线就让谁下线;
- 没有跨域问题,客户端应用不管部署在什么域名下都可以和认证服务器交互,基于 Token 的 SSO 会有同域的问题;
- 复杂度高:2 套机制,Session + Token;
- 性能比较低,占用应用服务器的内存存储 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 信息删不掉,用户登出失败;
refresh token
文章图片
refresh token.png
- access_token:短生命周期;
- refresh_token:长生命周期;
- 客户端应用拿 refresh_token 不断的刷 access_token;
- 拿到 access_token 可以任意访问微服务,而且是合法的;
- refresh_token 在使用的时候,必须和 client_id,client_secret 一起使用;
- 字段
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_type
是refresh_token
;
- 当 refresh_token 失败后,返回前端 500,完了带一个专门标记 refresh_token 失败的标记;
- 前端的 HttpClient 加一个拦截器,在请求之后拦截,如果收到 refresh_token 失败的标记,调用 Logout 接口,前端服务器中的 session 失效,认证服务器中的 session 失效,让用户重新登录;
- 直接请求认证服务器的 /oauth/authorize 接口,让认证服务器决定是否要重新登录;
- 如果认证服务器中的 session 没过期,回调到前端服务器,前端服务器获取 Token 后存储在自己的 session 中;
- 如果认证服务器中的 session 过期,让浏览器重定向到登录页面;
- 浏览器访问首页,请求会先经过前端服务器的 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,会访问认证服务器的登录接口,认证服务器返回登录页;
- 基于 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 中加多个域名;
推荐阅读
- java|单点登录方案
- cookie|一文了解Session
- Redis|面试官(熟悉Redis,那聊聊Redis主从复制(我画了13张图讲明白了))
- mysql|mysql查找删除重复数据并只保留一条
- 数据库|MySQL经典面试题(一)-搞定面试官背一套就够
- mysql|【面试不用背】作为一个CRUD工程师,你必须要知道的MySQL知识
- pyqt5|python PyQt5 数据库 表格动态增删改
- 数据库|Redis——作为sql数据库缓存
- redis|Redis集群——分布式缓存