javascript
spring 多租户_使用Spring Security的多租户应用程序的无状态会话
spring 多租戶
從前, 我發表了一篇文章,解釋了構建無狀態會話的原理 。 巧合的是,我們再次為多租戶應用程序執行同一任務。 這次,我們將解決方案集成到Spring Security框架中,而不是自己構建身份驗證機制。
本文將解釋我們的方法和實現。
業務需求
我們需要為Saas應用程序建立身份驗證機制。 每個客戶都通過專用子域訪問該應用程序。 由于該應用程序將部署在云上,因此很明顯,無狀態會話是首選,因為它使我們能夠輕松部署其他實例。
在項目詞匯表中,每個客戶都是一個站點。 每個應用程序都是一個應用程序。 例如,站點可以是Microsoft或Google。 應用可以是Gmail,GooglePlus或Google云端硬盤。 用戶用于訪問應用程序的子域將包括應用程序和網站。 例如,它可能看起來像microsoft.mail.somedomain.com或google.map.somedomain.com
用戶一旦登錄到一個應用程序,就可以訪問同一站點的任何其他應用程序。 在一定的非活動時間后,會話將超時。
背景
無狀態會話
具有超時的無狀態應用程序并不是什么新鮮事物。 Play框架從2007年的第一個版本開始就一直是無狀態的。很多年前,我們也切換到了無狀態會話。 好處很明顯。 您的負載均衡器不需要粘性; 因此,它更易于配置。 在瀏覽器中進行會話時,我們可以簡單地引入新服務器以立即增加容量。 但是,缺點是您的會話不太大,也不是那么機密。
與會話存儲在服務器中的有狀態應用程序相比,無狀態應用程序將會話存儲在HTTP cookie中,該cookie不能超過4KB。 此外,由于它是cookie,因此建議開發人員僅將文本或數字存儲在會話中,而不要存儲復雜的數據結構。 會話存儲在瀏覽器中,并在每個單個請求中傳輸到服務器。 因此,我們應該使會話盡可能小,并避免在其上放置任何機密數據。 簡而言之,無狀態會話迫使開發人員改變應用程序使用會話的方式。 應該是用戶身份,而不是方便存儲。
安全框架
Security Framework背后的想法非常簡單,它有助于確定執行代碼的原理,檢查他是否有權執行某些服務,如果用戶沒有權限則拋出異常。 在實現方面,安全框架以AOP樣式體系結構與您的服務集成。 每次檢查都將在調用方法之前由框架進行。 實現權限檢查的機制可以是過濾器或代理。
通常,安全框架會將主體信息存儲在線程存儲中(Java中的ThreadLocal)。 這就是為什么它可以隨時為開發人員提供靜態方法訪問主體的原因。 我認為這是開發人員應該知道的一些事情; 否則,他們可能會在單獨線程中運行的某些后臺作業中實施權限檢查或獲取委托人。 在這種情況下,很明顯,安全框架將無法找到主體。
單點登錄
單一登錄主要使用身份驗證服務器來實現。 它獨立于實現會話(無狀態或有狀態)的機制。 每個應用程序仍保持自己的會話。 首次訪問應用程序時,它將與身份驗證服務器聯系以對用戶進行身份驗證,然后創建自己的會話。
思想的食物
從頭開始構架或構建
由于無狀態會話是標準,因此我們最大的顧慮是使用或不使用安全框架。 如果使用的話,那么Spring Security是最便宜,最快的解決方案,因為我們已經在應用程序中使用了Spring Framework。 為了利益,任何安全框架都為我們提供了快速和聲明性的方式來聲明評估規則。 但是,它不是業務邏輯感知的訪問規則。 例如,我們可以定義僅代理可以訪問產品,而不能定義一個代理只能訪問屬于他的某些產品。
在這種情況下,我們有兩種選擇,從頭開始構建我們自己的業務邏輯許可權檢查,或者構建兩層許可權檢查,一種僅基于角色,一種是業務邏輯感知。 比較兩種方法之后,我們選擇了后一種方法,因為它更便宜且構建速度更快。 我們的應用程序的功能將類似于任何其他Spring Security應用程序。 這意味著如果在沒有會話的情況下訪問受保護的內容,則用戶將被重定向到登錄頁面。 如果會話存在,則用戶將獲得狀態碼403。如果用戶訪問具有有效角色但受未經授權的記錄的受保護內容,則將獲得401。
認證方式
接下來的問題是如何將我們的身份驗證和授權機制與Spring Security集成在一起。 一個標準的Spring Security應用程序可以處理如下請求:
該圖已簡化,但仍給我們一個原始的想法。 如果請求是登錄或注銷,則前兩個過濾器將更新服務器端會話。 此后,另一個過濾器幫助檢查請求的訪問權限。 如果權限檢查成功,則另一個過濾器將幫助將用戶會話存儲到線程存儲中。 之后,控制器將在正確的設置環境下執行代碼。
對于我們來說,我們更喜歡創建身份驗證機制,因為憑據需要包含網站域。 例如,我們可能有Xerox的Joe和WDS的Joe訪問Saas應用程序。 由于Spring Security控制著準備身份驗證令牌和身份驗證提供程序的控制,因此我們發現在控制器級別實現自己的登錄和注銷要便宜得多,而不是花很多精力來定制Spring Security。
當我們實現無狀態會話時,我們需要在這里實現兩項工作。 首先,我們需要在進行任何授權檢查之前從cookie構造會話。 我們還需要更新會話時間戳,以便每次瀏覽器向服務器發送請求時刷新會話。
由于先前決定在控制器中進行身份驗證,因此我們在這里面臨挑戰。 我們不應該在控制器執行之前刷新會話,因為我們在此處進行身份驗證。 但是,View Resolver附帶了一些控制器方法,這些方法可立即寫入輸出流。 因此,執行控制器后,我們沒有機會刷新Cookie。 最后,我們使用HandlerInterceptorAdapter選擇一個稍有妥協的解決方案。 該處理程序攔截器使我們可以在每種控制器方法之前和之后進行額外的處理。 如果方法用于身份驗證,則在控制器方法之后,而出于其他任何目的,則在控制器方法之前,我們實現刷新cookie。 新圖應如下所示
曲奇餅
為了有意義,用戶應該只有一個會話cookie。 由于會話總是在每次請求后更改時間戳,因此我們需要在每個響應上更新會話。 通過HTTP協議,只有在Cookie與名稱,路徑和域匹配時才能執行此操作。
在滿足此業務需求時,我們更喜歡嘗試通過共享會話cookie來實現SSO的新方法。 如果每個應用程序都在相同的父域下并且理解相同的會話cookie,則實際上我們擁有一個全局會話。 因此,不再需要認證服務器。 為了實現這一愿景,我們必須將域設置為所有應用程序的父域。
性能
從理論上講,無狀態會話應該更慢。 假設服務器實現將會話表存儲在內存中,則傳入JSESSIONID cookie只會觸發一次從會話表讀取對象,以及一次可選的寫入操作以更新上一次訪問(用于計算會話超時)。 相反,對于無狀態會話,我們需要計算哈希值以驗證會話cookie,從數據庫加載主體,分配新的時間戳并再次哈希。
但是,以今天的服務器性能而言,散列不應增加服務器響應時間的太多延遲。 更大的問題是從數據庫查詢數據,為此,我們可以使用緩存來加快速度。
在最佳情況下,如果沒有進行數據庫調用,則無狀態會話可以與有狀態會話足夠接近地執行。 代替從由容器維護的會話表中加載,而是從由應用程序維護的內部緩存中加載會話。 在最壞的情況下,請求被路由到許多不同的服務器,并且主體對象存儲在許多實例中。 這增加了額外的工作量,即每個服務器一次將主體加載到緩存。 盡管成本可能很高,但它僅偶爾出現一次。
如果我們將粘性路由應用于負載均衡器,則我們應該能夠實現最佳情況。 這樣,我們可以將無狀態會話cookie視為與JSESSIONID相似的機制,但具有重建會話對象的后備功能。
實作
我已將此實現的示例發布到https://github.com/tuanngda/sgdev-blog存儲庫。 請檢查無狀態會話項目。 該項目需要一個mysql數據庫才能工作。 因此,請在build.properties之后設置一個模式,或者修改屬性文件以適合您的模式。
該項目包括用于在端口8686上啟動tomcat服務器的maven配置。因此,您只需鍵入mvn cargo:run即可啟動服務器。
這是項目層次結構:
我打包了Tomcat 7服務器和數據庫,以便它能在沒有MySQL以外的任何其他安裝的情況下工作。 Tomcat配置文件TOMCAT_HOME / conf / context.xml包含數據源聲明和項目屬性文件。
現在,讓我們仔細看看實現。
屆會
我們需要兩個會話對象,一個代表會話cookie,一個代表我們在Spring安全框架內部構建的會話對象:
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());} }通過此組合,我們有了將會話對象存儲在cookie和內存中的對象。 下一步是實現一種方法,該方法允許我們從cookie數據構建會話對象。
public interface UserSessionService {public UserSession getUserSession(SessionCookieData sessionData); }現在,又有一項服務可以從Cookie數據中檢索并生成Cookie。
public class SessionCookieService {public Cookie generateSessionCookie(SessionCookieData cookieData, String domain);public SessionCookieData getSessionCookieData(Cookie sessionCookie);public Cookie generateSignCookie(Cookie sessionCookie); }到目前為止,我們提供的服務可幫助我們進行轉換
Cookie –> SessionCookieData –> UserSession
和
會話–> SessionCookieData –> Cookie
現在,我們應該有足夠的資料將無狀態會話與Spring Security框架集成在一起。
與Spring安全性集成
首先,我們需要添加一個過濾器以根據Cookie構造會話。 因為這應該在權限檢查之前發生,所以最好使用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();}...}上面的過濾器根據會話cookie構造主體對象。 篩選器還會創建一個PreAuthenticatedAuthenticationToken,稍后將用于身份驗證。 顯然,Spring不會理解該負責人。 因此,我們需要提供自己的AuthenticationProvider,它可以基于此主體來對用戶進行身份驗證。
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");} }這是春天的方式。 如果我們設法提供有效的身份驗證對象,則對用戶進行身份驗證。 實際上,我們讓用戶針對每個單個請求通過會話cookie登錄。
但是,有時我們需要更改用戶會話,并且可以像往常一樣在控制器方法中進行操作。 我們只需覆蓋SecurityContext,它已在過濾器中更早設置。
還將UserSession存儲到SecurityContextHolder,這有助于設置環境。 因為它是預身份驗證過濾器,所以它對大多數請求(身份驗證除外)都可以很好地工作。
我們應該手動更新身份驗證方法中的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());}刷新會議
到目前為止,您可能會注意到我們從未提到過編寫cookie。 假設我們有一個有效的Authentication對象,并且我們的SecurityContext包含UserSession,則需要將此信息發送到瀏覽器很重要。 在生成HttpServletResponse之前,我們必須將會話cookie附加到它。 具有相同域和路徑的cookie將替換瀏覽器保留的較舊的會話。
如上所述,刷新會話最好在控制器方法之后完成,因為我們在此處實現了身份驗證。 但是,挑戰是由Spring MVC的ViewResolver引起的。 有時,它這么快就寫入OutputStream,以至于將cookie添加到響應中的任何嘗試都是沒有用的。 最后,我們提出了一種折衷解決方案,該解決方案在用于常規請求的控制器方法之前和在用于身份驗證請求的控制器方法之后刷新會話。 要知道請求是否用于身份驗證,我們在身份驗證方法上放置一個注釋。
@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);}}}}結論
該解決方案對我們來說效果很好,但是我們沒有把握這可能是最佳實踐。 但是,它很簡單,并且不需要花費很多精力來實施(大約需要3天的測試時間)。
如果您有更好的想法來與Spring建立無狀態會話,請提供反饋。
翻譯自: https://www.javacodegeeks.com/2014/09/stateless-session-for-multi-tenant-application-using-spring-security.html
spring 多租戶
總結
以上是生活随笔為你收集整理的spring 多租户_使用Spring Security的多租户应用程序的无状态会话的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一亿等于多少万 一亿是多少万
- 下一篇: 建安文学指的是什么 建安文学的简介