javascript
spring 多租户_使用Spring Security的多租户应用程序的无状态会话
spring 多租戶
從前, 我發(fā)表了一篇文章,解釋了構(gòu)建無狀態(tài)會(huì)話的原理 。 巧合的是,我們?cè)俅螢槎嘧鈶魬?yīng)用程序執(zhí)行同一任務(wù)。 這次,我們將解決方案集成到Spring Security框架中,而不是自己構(gòu)建身份驗(yàn)證機(jī)制。
本文將解釋我們的方法和實(shí)現(xiàn)。
業(yè)務(wù)需求
我們需要為Saas應(yīng)用程序建立身份驗(yàn)證機(jī)制。 每個(gè)客戶都通過專用子域訪問該應(yīng)用程序。 由于該應(yīng)用程序?qū)⒉渴鹪谠粕?#xff0c;因此很明顯,無狀態(tài)會(huì)話是首選,因?yàn)樗刮覀兡軌蜉p松部署其他實(shí)例。
在項(xiàng)目詞匯表中,每個(gè)客戶都是一個(gè)站點(diǎn)。 每個(gè)應(yīng)用程序都是一個(gè)應(yīng)用程序。 例如,站點(diǎn)可以是Microsoft或Google。 應(yīng)用可以是Gmail,GooglePlus或Google云端硬盤。 用戶用于訪問應(yīng)用程序的子域?qū)☉?yīng)用程序和網(wǎng)站。 例如,它可能看起來像microsoft.mail.somedomain.com或google.map.somedomain.com
用戶一旦登錄到一個(gè)應(yīng)用程序,就可以訪問同一站點(diǎn)的任何其他應(yīng)用程序。 在一定的非活動(dòng)時(shí)間后,會(huì)話將超時(shí)。
背景
無狀態(tài)會(huì)話
具有超時(shí)的無狀態(tài)應(yīng)用程序并不是什么新鮮事物。 Play框架從2007年的第一個(gè)版本開始就一直是無狀態(tài)的。很多年前,我們也切換到了無狀態(tài)會(huì)話。 好處很明顯。 您的負(fù)載均衡器不需要粘性; 因此,它更易于配置。 在瀏覽器中進(jìn)行會(huì)話時(shí),我們可以簡單地引入新服務(wù)器以立即增加容量。 但是,缺點(diǎn)是您的會(huì)話不太大,也不是那么機(jī)密。
與會(huì)話存儲(chǔ)在服務(wù)器中的有狀態(tài)應(yīng)用程序相比,無狀態(tài)應(yīng)用程序?qū)?huì)話存儲(chǔ)在HTTP cookie中,該cookie不能超過4KB。 此外,由于它是cookie,因此建議開發(fā)人員僅將文本或數(shù)字存儲(chǔ)在會(huì)話中,而不要存儲(chǔ)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。 會(huì)話存儲(chǔ)在瀏覽器中,并在每個(gè)單個(gè)請(qǐng)求中傳輸?shù)椒?wù)器。 因此,我們應(yīng)該使會(huì)話盡可能小,并避免在其上放置任何機(jī)密數(shù)據(jù)。 簡而言之,無狀態(tài)會(huì)話迫使開發(fā)人員改變應(yīng)用程序使用會(huì)話的方式。 應(yīng)該是用戶身份,而不是方便存儲(chǔ)。
安全框架
Security Framework背后的想法非常簡單,它有助于確定執(zhí)行代碼的原理,檢查他是否有權(quán)執(zhí)行某些服務(wù),如果用戶沒有權(quán)限則拋出異常。 在實(shí)現(xiàn)方面,安全框架以AOP樣式體系結(jié)構(gòu)與您的服務(wù)集成。 每次檢查都將在調(diào)用方法之前由框架進(jìn)行。 實(shí)現(xiàn)權(quán)限檢查的機(jī)制可以是過濾器或代理。
通常,安全框架會(huì)將主體信息存儲(chǔ)在線程存儲(chǔ)中(Java中的ThreadLocal)。 這就是為什么它可以隨時(shí)為開發(fā)人員提供靜態(tài)方法訪問主體的原因。 我認(rèn)為這是開發(fā)人員應(yīng)該知道的一些事情; 否則,他們可能會(huì)在單獨(dú)線程中運(yùn)行的某些后臺(tái)作業(yè)中實(shí)施權(quán)限檢查或獲取委托人。 在這種情況下,很明顯,安全框架將無法找到主體。
單點(diǎn)登錄
單一登錄主要使用身份驗(yàn)證服務(wù)器來實(shí)現(xiàn)。 它獨(dú)立于實(shí)現(xiàn)會(huì)話(無狀態(tài)或有狀態(tài))的機(jī)制。 每個(gè)應(yīng)用程序仍保持自己的會(huì)話。 首次訪問應(yīng)用程序時(shí),它將與身份驗(yàn)證服務(wù)器聯(lián)系以對(duì)用戶進(jìn)行身份驗(yàn)證,然后創(chuàng)建自己的會(huì)話。
思想的食物
從頭開始構(gòu)架或構(gòu)建
由于無狀態(tài)會(huì)話是標(biāo)準(zhǔn),因此我們最大的顧慮是使用或不使用安全框架。 如果使用的話,那么Spring Security是最便宜,最快的解決方案,因?yàn)槲覀円呀?jīng)在應(yīng)用程序中使用了Spring Framework。 為了利益,任何安全框架都為我們提供了快速和聲明性的方式來聲明評(píng)估規(guī)則。 但是,它不是業(yè)務(wù)邏輯感知的訪問規(guī)則。 例如,我們可以定義僅代理可以訪問產(chǎn)品,而不能定義一個(gè)代理只能訪問屬于他的某些產(chǎn)品。
在這種情況下,我們有兩種選擇,從頭開始構(gòu)建我們自己的業(yè)務(wù)邏輯許可權(quán)檢查,或者構(gòu)建兩層許可權(quán)檢查,一種僅基于角色,一種是業(yè)務(wù)邏輯感知。 比較兩種方法之后,我們選擇了后一種方法,因?yàn)樗阋饲覙?gòu)建速度更快。 我們的應(yīng)用程序的功能將類似于任何其他Spring Security應(yīng)用程序。 這意味著如果在沒有會(huì)話的情況下訪問受保護(hù)的內(nèi)容,則用戶將被重定向到登錄頁面。 如果會(huì)話存在,則用戶將獲得狀態(tài)碼403。如果用戶訪問具有有效角色但受未經(jīng)授權(quán)的記錄的受保護(hù)內(nèi)容,則將獲得401。
認(rèn)證方式
接下來的問題是如何將我們的身份驗(yàn)證和授權(quán)機(jī)制與Spring Security集成在一起。 一個(gè)標(biāo)準(zhǔn)的Spring Security應(yīng)用程序可以處理如下請(qǐng)求:
該圖已簡化,但仍給我們一個(gè)原始的想法。 如果請(qǐng)求是登錄或注銷,則前兩個(gè)過濾器將更新服務(wù)器端會(huì)話。 此后,另一個(gè)過濾器幫助檢查請(qǐng)求的訪問權(quán)限。 如果權(quán)限檢查成功,則另一個(gè)過濾器將幫助將用戶會(huì)話存儲(chǔ)到線程存儲(chǔ)中。 之后,控制器將在正確的設(shè)置環(huán)境下執(zhí)行代碼。
對(duì)于我們來說,我們更喜歡創(chuàng)建身份驗(yàn)證機(jī)制,因?yàn)閼{據(jù)需要包含網(wǎng)站域。 例如,我們可能有Xerox的Joe和WDS的Joe訪問Saas應(yīng)用程序。 由于Spring Security控制著準(zhǔn)備身份驗(yàn)證令牌和身份驗(yàn)證提供程序的控制,因此我們發(fā)現(xiàn)在控制器級(jí)別實(shí)現(xiàn)自己的登錄和注銷要便宜得多,而不是花很多精力來定制Spring Security。
當(dāng)我們實(shí)現(xiàn)無狀態(tài)會(huì)話時(shí),我們需要在這里實(shí)現(xiàn)兩項(xiàng)工作。 首先,我們需要在進(jìn)行任何授權(quán)檢查之前從cookie構(gòu)造會(huì)話。 我們還需要更新會(huì)話時(shí)間戳,以便每次瀏覽器向服務(wù)器發(fā)送請(qǐng)求時(shí)刷新會(huì)話。
由于先前決定在控制器中進(jìn)行身份驗(yàn)證,因此我們?cè)谶@里面臨挑戰(zhàn)。 我們不應(yīng)該在控制器執(zhí)行之前刷新會(huì)話,因?yàn)槲覀冊(cè)诖颂庍M(jìn)行身份驗(yàn)證。 但是,View Resolver附帶了一些控制器方法,這些方法可立即寫入輸出流。 因此,執(zhí)行控制器后,我們沒有機(jī)會(huì)刷新Cookie。 最后,我們使用HandlerInterceptorAdapter選擇一個(gè)稍有妥協(xié)的解決方案。 該處理程序攔截器使我們可以在每種控制器方法之前和之后進(jìn)行額外的處理。 如果方法用于身份驗(yàn)證,則在控制器方法之后,而出于其他任何目的,則在控制器方法之前,我們實(shí)現(xiàn)刷新cookie。 新圖應(yīng)如下所示
曲奇餅
為了有意義,用戶應(yīng)該只有一個(gè)會(huì)話cookie。 由于會(huì)話總是在每次請(qǐng)求后更改時(shí)間戳,因此我們需要在每個(gè)響應(yīng)上更新會(huì)話。 通過HTTP協(xié)議,只有在Cookie與名稱,路徑和域匹配時(shí)才能執(zhí)行此操作。
在滿足此業(yè)務(wù)需求時(shí),我們更喜歡嘗試通過共享會(huì)話cookie來實(shí)現(xiàn)SSO的新方法。 如果每個(gè)應(yīng)用程序都在相同的父域下并且理解相同的會(huì)話cookie,則實(shí)際上我們擁有一個(gè)全局會(huì)話。 因此,不再需要認(rèn)證服務(wù)器。 為了實(shí)現(xiàn)這一愿景,我們必須將域設(shè)置為所有應(yīng)用程序的父域。
性能
從理論上講,無狀態(tài)會(huì)話應(yīng)該更慢。 假設(shè)服務(wù)器實(shí)現(xiàn)將會(huì)話表存儲(chǔ)在內(nèi)存中,則傳入JSESSIONID cookie只會(huì)觸發(fā)一次從會(huì)話表讀取對(duì)象,以及一次可選的寫入操作以更新上一次訪問(用于計(jì)算會(huì)話超時(shí))。 相反,對(duì)于無狀態(tài)會(huì)話,我們需要計(jì)算哈希值以驗(yàn)證會(huì)話cookie,從數(shù)據(jù)庫加載主體,分配新的時(shí)間戳并再次哈希。
但是,以今天的服務(wù)器性能而言,散列不應(yīng)增加服務(wù)器響應(yīng)時(shí)間的太多延遲。 更大的問題是從數(shù)據(jù)庫查詢數(shù)據(jù),為此,我們可以使用緩存來加快速度。
在最佳情況下,如果沒有進(jìn)行數(shù)據(jù)庫調(diào)用,則無狀態(tài)會(huì)話可以與有狀態(tài)會(huì)話足夠接近地執(zhí)行。 代替從由容器維護(hù)的會(huì)話表中加載,而是從由應(yīng)用程序維護(hù)的內(nèi)部緩存中加載會(huì)話。 在最壞的情況下,請(qǐng)求被路由到許多不同的服務(wù)器,并且主體對(duì)象存儲(chǔ)在許多實(shí)例中。 這增加了額外的工作量,即每個(gè)服務(wù)器一次將主體加載到緩存。 盡管成本可能很高,但它僅偶爾出現(xiàn)一次。
如果我們將粘性路由應(yīng)用于負(fù)載均衡器,則我們應(yīng)該能夠?qū)崿F(xiàn)最佳情況。 這樣,我們可以將無狀態(tài)會(huì)話cookie視為與JSESSIONID相似的機(jī)制,但具有重建會(huì)話對(duì)象的后備功能。
實(shí)作
我已將此實(shí)現(xiàn)的示例發(fā)布到https://github.com/tuanngda/sgdev-blog存儲(chǔ)庫。 請(qǐng)檢查無狀態(tài)會(huì)話項(xiàng)目。 該項(xiàng)目需要一個(gè)mysql數(shù)據(jù)庫才能工作。 因此,請(qǐng)?jiān)赽uild.properties之后設(shè)置一個(gè)模式,或者修改屬性文件以適合您的模式。
該項(xiàng)目包括用于在端口8686上啟動(dòng)tomcat服務(wù)器的maven配置。因此,您只需鍵入mvn cargo:run即可啟動(dòng)服務(wù)器。
這是項(xiàng)目層次結(jié)構(gòu):
我打包了Tomcat 7服務(wù)器和數(shù)據(jù)庫,以便它能在沒有MySQL以外的任何其他安裝的情況下工作。 Tomcat配置文件TOMCAT_HOME / conf / context.xml包含數(shù)據(jù)源聲明和項(xiàng)目屬性文件。
現(xiàn)在,讓我們仔細(xì)看看實(shí)現(xiàn)。
屆會(huì)
我們需要兩個(gè)會(huì)話對(duì)象,一個(gè)代表會(huì)話cookie,一個(gè)代表我們?cè)赟pring安全框架內(nèi)部構(gòu)建的會(huì)話對(duì)象:
public class SessionCookieData {private int userId;private String appId;private int siteId;private Date timeStamp; }和
public class UserSession {private User user;private Site site;public SessionCookieData generateSessionCookieData(){return new SessionCookieData(user.getId(), user.getAppId(), site.getId());} }通過此組合,我們有了將會(huì)話對(duì)象存儲(chǔ)在cookie和內(nèi)存中的對(duì)象。 下一步是實(shí)現(xiàn)一種方法,該方法允許我們從cookie數(shù)據(jù)構(gòu)建會(huì)話對(duì)象。
public interface UserSessionService {public UserSession getUserSession(SessionCookieData sessionData); }現(xiàn)在,又有一項(xiàng)服務(wù)可以從Cookie數(shù)據(jù)中檢索并生成Cookie。
public class SessionCookieService {public Cookie generateSessionCookie(SessionCookieData cookieData, String domain);public SessionCookieData getSessionCookieData(Cookie sessionCookie);public Cookie generateSignCookie(Cookie sessionCookie); }到目前為止,我們提供的服務(wù)可幫助我們進(jìn)行轉(zhuǎn)換
Cookie –> SessionCookieData –> UserSession
和
會(huì)話–> SessionCookieData –> Cookie
現(xiàn)在,我們應(yīng)該有足夠的資料將無狀態(tài)會(huì)話與Spring Security框架集成在一起。
與Spring安全性集成
首先,我們需要添加一個(gè)過濾器以根據(jù)Cookie構(gòu)造會(huì)話。 因?yàn)檫@應(yīng)該在權(quán)限檢查之前發(fā)生,所以最好使用AbstractPreAuthenticatedProcessingFilter
@Component(value="cookieSessionFilter") public class CookieSessionFilter extends AbstractPreAuthenticatedProcessingFilter {...@Overrideprotected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {SecurityContext securityContext = extractSecurityContext(request);if (securityContext.getAuthentication()!=null? && securityContext.getAuthentication().isAuthenticated()){UserAuthentication userAuthentication = (UserAuthentication) securityContext.getAuthentication();UserSession session = (UserSession) userAuthentication.getDetails();SecurityContextHolder.setContext(securityContext);return session;}return new UserSession();}...}上面的過濾器根據(jù)會(huì)話cookie構(gòu)造主體對(duì)象。 篩選器還會(huì)創(chuàng)建一個(gè)PreAuthenticatedAuthenticationToken,稍后將用于身份驗(yàn)證。 顯然,Spring不會(huì)理解該負(fù)責(zé)人。 因此,我們需要提供自己的AuthenticationProvider,它可以基于此主體來對(duì)用戶進(jìn)行身份驗(yàn)證。
public class UserAuthenticationProvider implements AuthenticationProvider { @Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {PreAuthenticatedAuthenticationToken token = (PreAuthenticatedAuthenticationToken) authentication;UserSession session = (UserSession)token.getPrincipal();if (session != null && session.getUser() != null){SecurityContext securityContext = SecurityContextHolder.getContext();securityContext.setAuthentication(new UserAuthentication(session));return new UserAuthentication(session);}throw new BadCredentialsException("Unknown user name or password");} }這是春天的方式。 如果我們?cè)O(shè)法提供有效的身份驗(yàn)證對(duì)象,則對(duì)用戶進(jìn)行身份驗(yàn)證。 實(shí)際上,我們讓用戶針對(duì)每個(gè)單個(gè)請(qǐng)求通過會(huì)話cookie登錄。
但是,有時(shí)我們需要更改用戶會(huì)話,并且可以像往常一樣在控制器方法中進(jìn)行操作。 我們只需覆蓋SecurityContext,它已在過濾器中更早設(shè)置。
還將UserSession存儲(chǔ)到SecurityContextHolder,這有助于設(shè)置環(huán)境。 因?yàn)樗穷A(yù)身份驗(yàn)證過濾器,所以它對(duì)大多數(shù)請(qǐng)求(身份驗(yàn)證除外)都可以很好地工作。
我們應(yīng)該手動(dòng)更新身份驗(yàn)證方法中的SecurityContext:
public ModelAndView login(String login, String password, String siteCode) throws IOException{if(StringUtils.isEmpty(login) || StringUtils.isEmpty(password)){throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "Missing login and password");}User user = authService.login(siteCode, login, password);if(user!=null){SecurityContext securityContext = SecurityContextHolder.getContext();UserSession userSession = new UserSession();userSession.setSite(user.getSite());userSession.setUser(user);securityContext.setAuthentication(new UserAuthentication(userSession));}else{throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED, "Invalid login or password");}return new ModelAndView(new MappingJackson2JsonView());}刷新會(huì)議
到目前為止,您可能會(huì)注意到我們從未提到過編寫cookie。 假設(shè)我們有一個(gè)有效的Authentication對(duì)象,并且我們的SecurityContext包含UserSession,則需要將此信息發(fā)送到瀏覽器很重要。 在生成HttpServletResponse之前,我們必須將會(huì)話cookie附加到它。 具有相同域和路徑的cookie將替換瀏覽器保留的較舊的會(huì)話。
如上所述,刷新會(huì)話最好在控制器方法之后完成,因?yàn)槲覀冊(cè)诖颂帉?shí)現(xiàn)了身份驗(yàn)證。 但是,挑戰(zhàn)是由Spring MVC的ViewResolver引起的。 有時(shí),它這么快就寫入OutputStream,以至于將cookie添加到響應(yīng)中的任何嘗試都是沒有用的。 最后,我們提出了一種折衷解決方案,該解決方案在用于常規(guī)請(qǐng)求的控制器方法之前和在用于身份驗(yàn)證請(qǐng)求的控制器方法之后刷新會(huì)話。 要知道請(qǐng)求是否用于身份驗(yàn)證,我們?cè)谏矸蒡?yàn)證方法上放置一個(gè)注釋。
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (handler instanceof HandlerMethod){HandlerMethod handlerMethod = (HandlerMethod) handler;SessionUpdate sessionUpdateAnnotation = handlerMethod.getMethod().getAnnotation(SessionUpdate.class);if (sessionUpdateAnnotation == null){SecurityContext context = SecurityContextHolder.getContext();if (context.getAuthentication() instanceof UserAuthentication){UserAuthentication userAuthentication = (UserAuthentication)context.getAuthentication();UserSession session = (UserSession) userAuthentication.getDetails();persistSessionCookie(response, session);}}}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {if (handler instanceof HandlerMethod){HandlerMethod handlerMethod = (HandlerMethod) handler;SessionUpdate sessionUpdateAnnotation = handlerMethod.getMethod().getAnnotation(SessionUpdate.class);if (sessionUpdateAnnotation != null){SecurityContext context = SecurityContextHolder.getContext();if (context.getAuthentication() instanceof UserAuthentication){UserAuthentication userAuthentication = (UserAuthentication)context.getAuthentication();UserSession session = (UserSession) userAuthentication.getDetails();persistSessionCookie(response, session);}}}}結(jié)論
該解決方案對(duì)我們來說效果很好,但是我們沒有把握這可能是最佳實(shí)踐。 但是,它很簡單,并且不需要花費(fèi)很多精力來實(shí)施(大約需要3天的測試時(shí)間)。
如果您有更好的想法來與Spring建立無狀態(tài)會(huì)話,請(qǐng)?zhí)峁┓答仭?
翻譯自: https://www.javacodegeeks.com/2014/09/stateless-session-for-multi-tenant-application-using-spring-security.html
spring 多租戶
總結(jié)
以上是生活随笔為你收集整理的spring 多租户_使用Spring Security的多租户应用程序的无状态会话的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一亿等于多少万 一亿是多少万
- 下一篇: jsr303自定义验证_JSR 310新