三種架構(gòu)
前后端半分離架構(gòu)
前后端分離架構(gòu)
將 Postman 升級成前端服務(wù)器
前后端分離架構(gòu) | 實(shí)現(xiàn)概述
- admin 項(xiàng)目的角色是:前端服務(wù)器利虫;
- admin 項(xiàng)目原本應(yīng)該是個 Node.js + Angular 的項(xiàng)目,這里使用 Springboot + Angular 來代替糠惫;
- Angular Build 完的結(jié)果,直接放在 Springboot 的 java/main/resource/static 下硼讽;
- admin 項(xiàng)目向 zuul 發(fā)送請求;
- 在認(rèn)證服務(wù)器的客戶端應(yīng)用列表中壤躲,要加上 admin 項(xiàng)目备燃;
真實(shí)壞境下碉克,Web 應(yīng)用部署在 NodeJS 中并齐,瀏覽器向 NodeJS 發(fā)請求,NodeJS 把請求發(fā)到 Zuul撕贞,Zuul 中完成限流测垛、認(rèn)證麻掸、審計(jì)赐纱、授權(quán)熬北,完了調(diào)用業(yè)務(wù)系統(tǒng)诚隙;
SSO & Authorizatin code grant & 前端服務(wù)器
- OAuth2 的 Authorizatin code grant 認(rèn)證模式的實(shí)現(xiàn),需要引入前端服務(wù)器久又;
- 引入了前端服務(wù)器,就實(shí)現(xiàn)了 SSO地消;
- 當(dāng)前端服務(wù)器重啟后,瀏覽器訪問前端服務(wù)器疼阔,直接就是登錄狀態(tài);因?yàn)樵?Authorizatin code grant 認(rèn)證模式下婆廊,登錄的位置是認(rèn)證服務(wù)器巫橄,只要登錄服務(wù)器上的 Session 沒過期,認(rèn)證服務(wù)器就知道湘换,從瀏覽器來的請求是誰,不用再輸用戶名和密碼了彩倚,然后直接跳到客戶端應(yīng)用,如果之前頒發(fā)給這個用戶的 Token 沒有過期崎溃,就把之前的 Token 返回給前端服務(wù)器盯质;如果無效,就生成一個新 Token 發(fā)給前端服務(wù)器呼巷;這帶來的一個問題是,如果用戶的登出王悍,只是清空前端服務(wù)器的 Session,會導(dǎo)致用戶無法登出鲜漩;
- 前端服務(wù)器可以有多個,任何一個登錄成功孕似,Session 信息都會存在認(rèn)證服務(wù)器中,當(dāng)瀏覽器再發(fā)登錄請求到第二個前端服務(wù)器中喉祭,前端服務(wù)器去認(rèn)證服務(wù)器中拿的 Token 都是一樣的,這就是 SSO理卑;
基于 Session 的 SSO
2 個 Session
- 認(rèn)證服務(wù)器的 Session蔽氨,存的是用戶信息和 Session ID,Session ID 返回給瀏覽器作為 Cookie孵滞,這個 Cookie(SESSION) 和前端服務(wù)器返回給瀏覽器的 Cookie(JSESSIONID) 不是同一個鸯匹;
- 前端服務(wù)器的 Session,存的是用戶的 Token殴蓬;
3 個有效期
- 認(rèn)證服務(wù)器 Session 的有效期,控制多長時(shí)間需要用戶輸一次用戶名和密碼痘绎;
- 前端服務(wù)器 Session 的有效期肖粮,控制的是多長時(shí)間跳轉(zhuǎn)一次認(rèn)證服務(wù)器;
- Token 的有效期涩馆,控制登錄一次能訪問多長時(shí)間的微服務(wù);
優(yōu)點(diǎn)
- Session 信息都是存儲在服務(wù)器里魂那,不論是前端服務(wù)器還是認(rèn)證服務(wù)器;
- Token 信息和 Session 信息都存儲在數(shù)據(jù)庫中鲜结,可控性高,可以看到系統(tǒng)中所有的登錄狀態(tài)精刷,想讓誰下線就讓誰下線;
- 沒有跨域問題挤土,客戶端應(yīng)用不管部署在什么域名下都可以和認(rèn)證服務(wù)器交互误算,基于 Token 的 SSO 會有同域的問題仰美;
缺點(diǎn)
- 復(fù)雜度高:2 套機(jī)制,Session + Token咖杂;
- 性能比較低蚊夫,占用應(yīng)用服務(wù)器的內(nèi)存存儲 Session;
適用
- 適用與百萬用戶以下或有一定規(guī)模的公司的內(nèi)部管理系統(tǒng)知纷;
Logout 時(shí)同步前端服務(wù)器 & 認(rèn)證服務(wù)器的 Session
- 點(diǎn)擊 Logout 按鈕,在前端服務(wù)器將 session 失效后伍绳,要向認(rèn)證服務(wù)器發(fā)一個請求
http://auth.imooc.com:9090/logout?redirect_uri=http://admin.imooc.com:8080
; - 重寫 Spring Security 的
DefaultLogoutPageGeneratingFilter
過濾器乍桂,這個過濾器原本會生成一個是否確定登出的 Form 表單,重寫后將其邏輯變成睹酌,F(xiàn)orm 表單渲染好了之后,直接 Submit憋沿,并且通過一個 hidden 的 input 把參數(shù)redirect_uri=http://admin.imooc.com:8080
提交給認(rèn)證服務(wù)器的 Logout 邏輯; - 寫一個 Logout 成功后要做什么的 Handler:
OAuth2LogoutSuccessHandler
甥绿,在 Logout 成功后则披,重定向到前端服務(wù)器的首頁; - 把自己寫的
OAuth2LogoutSuccessHandler
配置到認(rèn)證服務(wù)器中:
@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 會不一樣隘梨,用戶在數(shù)據(jù)庫中的 session 信息刪不掉舷嗡,用戶登出失敗捻脖;
Token 的有效期
Token 是短活的令牌中鼠,Token 過期的時(shí)候可婶,可能 Session 還沒過期。
refresh token
- access_token:短生命周期矛渴;
- refresh_token:長生命周期熊杨;
- 客戶端應(yīng)用拿 refresh_token 不斷的刷 access_token盗舰;
access_token vs access_token
- 拿到 access_token 可以任意訪問微服務(wù),而且是合法的钻趋;
- refresh_token 在使用的時(shí)候,必須和 client_id较沪,client_secret 一起使用失仁;
refresh_token 的啟用
- 字段
oauth_client_details
.refresh_token_validity
必須要填,單位是 s萄焦,只要這個字段有值冤竹,在發(fā) access_token 的同時(shí)茬射,會發(fā) refresh_token; - 去認(rèn)證服務(wù)器刷新 Token 的客戶端應(yīng)用在抛,在認(rèn)證服務(wù)器中配置的信息的
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 種認(rèn)證模式的:password, code, ... , ...
.authenticationManager(authenticationManager);
}
- refresh_token 請求的
grant_type
是refresh_token
;
refresh_token 過期了怎么辦
方案一 | 強(qiáng)制 Logout
- 當(dāng) refresh_token 失敗后磨德,返回前端 500,完了帶一個專門標(biāo)記 refresh_token 失敗的標(biāo)記酥宴;
- 前端的 HttpClient 加一個攔截器您觉,在請求之后攔截,如果收到 refresh_token 失敗的標(biāo)記琳水,調(diào)用 Logout 接口,前端服務(wù)器中的 session 失效诚啃,認(rèn)證服務(wù)器中的 session 失效,讓用戶重新登錄始赎;
方案二 | 讓認(rèn)證服務(wù)器決定
- 直接請求認(rèn)證服務(wù)器的 /oauth/authorize 接口仔燕,讓認(rèn)證服務(wù)器決定是否要重新登錄;
- 如果認(rèn)證服務(wù)器中的 session 沒過期晰搀,回調(diào)到前端服務(wù)器,前端服務(wù)器獲取 Token 后存儲在自己的 session 中杆逗;
- 如果認(rèn)證服務(wù)器中的 session 過期,讓瀏覽器重定向到登錄頁面髓迎;
基于 Token 的 SSO
步驟
- 瀏覽器訪問首頁,請求會先經(jīng)過前端服務(wù)器的 CookieTokenFilter排龄,發(fā)現(xiàn)請求既沒有 access_token,也沒有 refresh_token 后尺铣,會調(diào)用認(rèn)證服務(wù)器的登錄接口争舞,認(rèn)證服務(wù)器返回登錄頁,輸入用戶名密碼后發(fā)請求到認(rèn)證服務(wù)器登錄竞川;
- 在認(rèn)證服務(wù)器上登錄成功后,重定向到前端服務(wù)器后委乌,前端服務(wù)器獲取到 Token 后,不存儲在前端服務(wù)器本地的 session 中戈咳,而是將 Token(access_token, refresh_token) 作為 Cookie 返回給瀏覽器壕吹;
- 瀏覽器重定向到首頁,請求還是會經(jīng)過 CookieTokenFilter耳贬,此時(shí)有 access_token,CookieTokenFilter 會把 access_token 放入 Header 中暂吉,然后放行到網(wǎng)關(guān)缎患,請求進(jìn)入網(wǎng)關(guān)限流攔截器,認(rèn)證攔截器挤渔,審計(jì)攔截器风题,授權(quán)攔截器后進(jìn)入 /user/me 攔截器嫉父,/user/me 攔截器判斷請求的 Header 中有 username 信息(授權(quán)攔截器放進(jìn)去的)眼刃,返回 username 給前端,前端看到有返回?cái)?shù)據(jù)擂红,就進(jìn)入登錄后的頁面;
- 瀏覽器帶著 Cookie(token) 訪問 order 服務(wù)树碱,經(jīng)過 CookieTokenFilter 后变秦,CookieTokenFilter 會把 token 放進(jìn) Header 中成榜,經(jīng)過網(wǎng)關(guān)的限流蹦玫、認(rèn)證、審計(jì)惑淳、授權(quán)后饺窿,會進(jìn)入 order 服務(wù),完成對 order 服務(wù)的訪問肚医;
- 登出的時(shí)候,瀏覽器先把 Cookie 清空肠套,然后訪問認(rèn)證服務(wù)器的登出接口,登出后瓷耙,重定向到首頁刁赖,首頁會先訪問 /user/me 接口,但是會被 CookieTokenFilter 攔截下來宇弛,CookieTokenFilter 發(fā)現(xiàn)沒有 access_token 和 refresh_token,會訪問認(rèn)證服務(wù)器的登錄接口彻况,認(rèn)證服務(wù)器返回登錄頁谁尸;
基于 Token 的 SSO vs 基于 Session 的 SSO
- 基于 Session 的 SSO纽甘,每當(dāng)前端服務(wù)器的 session 失效后,就要訪問認(rèn)證服務(wù)器去判斷 session 是否過期背镇,由認(rèn)證服務(wù)器決定放回前端服務(wù)器舊的 Token泽裳,還是返回瀏覽器登錄頁;
- 基于 Token 的 SSO涮总,當(dāng)瀏覽器中的 Cookie 失效后,才會去認(rèn)證服務(wù)器一次認(rèn)證烹笔;
- 不管那種方案抛丽,在認(rèn)證服務(wù)器上,都會有用戶的 session亿鲜, 基于 Session 的 SSO 需要認(rèn)證服務(wù)器上的 session 有效期較長,這樣才不會導(dǎo)致前端服務(wù)器頻繁的訪問認(rèn)證服務(wù)器蒿柳;基于 Token 的 SSO 不需要認(rèn)證服務(wù)器上的 session 有效期很長,只要瀏覽器中 Cookie 有效妓蛮,就能訪問服務(wù)圾叼,哪怕認(rèn)證服務(wù)器中的 session 已經(jīng)失效,只要 Cookie 中的 access_token 沒失效夷蚊,任然可以繼續(xù)訪問微服務(wù);
優(yōu)點(diǎn)
- 復(fù)雜度較低儿倒,只需要考慮兩個 Token 過期要怎么處理就可以了呜笑;
- 更適合海量用戶的場景,比如有上千萬的用戶叫胁,不可能把這些用戶的信息都存在 前端服務(wù)器的 session 中;
缺點(diǎn)
- 安全性較低微谓,access_token 存儲在 Cookie 中了输钩,可以使用 HTTPS 保證 Cookie 的傳遞豺型,還有就是放在 Cookie 中的 Token 不會有很長的有效期买乃;如果是 JWT 的話,瀏覽器中還會存用戶信息肴焊,那樣的安全風(fēng)險(xiǎn)更高 功戚;
- 可控性較低,因?yàn)?access_token 是存儲在瀏覽器的 Cookie 中啸臀,沒法由管理人員手動失效掉 access_token;
- 沒法跨域席揽,因?yàn)榻o瀏覽器的 Cookie 是有域名限制的谓厘,不在同一個一級域名下的前端應(yīng)用,是無法做到 SSO 的竟稳;可以通過往返回給瀏覽器的 Cookie 中加多個域名;