Session(数据)共享的前后端分离Shiro实战
生活随笔
收集整理的這篇文章主要介紹了
Session(数据)共享的前后端分离Shiro实战
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
1,前言 本文期望描述如何使用Shiro構建基本的安全登錄和權限驗證。本文實戰場景有如下特殊需求:1,在集群和分布式環境實現session共享;2,前端只使用HTML/CSS/JS。因此無法直接使用Shiro提供的SessionManager,以及Shiro針對web應用提供的Filter攔截方式。當然,除非是一定要通過共享緩存的方式共享session,否則還是使用Shiro默認的session管理,畢竟增加獨立緩存就意味著維護成本的提高和可用性的下降。 2, Shiro架構 首先一睹官方給出的Shiro架構圖,如圖1所示。刨除最右側的加密工具類,主要圍繞SercurityManager來闡述。SercurityManager是Shiro安全框架里的頂層安全管理中心,所有安全控制相關邏輯都是在SercurityManager里面通過delegate的方式,調用到真正的動作執行者。從圖1可以清楚看到主要管理的組件:authentication管理,authorization管理,session管理,session緩存管理,cache管理,realms管理。(本文不想重復已有的文字,想要更好的了解Shiro,詳見官方推薦的Shiro full intro: https://www.infoq.com/articles/apache-shiro) 1)Shiro提供的CacheManager比較單薄,提供實現是MemoryConstrainedCacheManager,主要是依賴SoftHashMap來做基于內存條件的緩存,也即是當內存吃緊,沒有新的內存空間來存放new出來的對象時,會去釋放SoftHashMap中存放的對象,在本文中的應用場景是面向集群和分布式應用環境,使用了Redi緩存登錄用戶的相關信息,所以需要自定義cache處理。 2)Shiro對于session的緩存管理,定義了SessionDAO抽象,并提供了兩個存放于本地JVM內存的EnterpriseCacheSessionDAO和MemorySessionDAO,兩者主要區別是EnterpriseCacheSessionDAO的session存放在SoftHashMap中,原則上可以自己實現SessionDAO 接口,實際存儲使用Redis來做到完整的session共享,但是缺陷是:a,不安全,因為把所有數據都共享出去了;b,當每次需要獲取session數據時,都需要通過網絡來把整個session反序列化回來,而考慮很多情況下,只是間斷的需要幾個key的數據,這樣在session數據量大一些的時候,就會產生大量消耗。因此在共享session時,不去替換默認SessionDao的實現,而是通過@overwrite AbstractNativeSessionManager getter/setter attribute方法,實現有選擇的共享session的基本初始化和指定attribute key的數據。 3)Shiro的authentication和authorization過程主要是依據用戶定義的 AuthorizingRealm中提供的AuthenticationInfo和AuthorizationInfo。特別地,authentication 還提供類似驗證鏈的authentication策略,允許用戶提供多個Realm。第3部分會具體的示例Shiro集成Spring的使用范例,并詳細解釋AuthorizingRealm 。 圖 1 Shiro官方架構圖 3, Shiro使用范例 官方提供了集成Spring Web應用的使用例子,但是就如前文提到的,這里前端只能使用JS的Http和后端通信,因此無法直接使用ShiroFilterFactoryBean來做Request的Filter。本文鑒于簡單和初期的原則,可以選擇定義一個RequestInterceptor類繼承HandlerInterceptorAdapter并overwrite preHandle 方法。Interceptor的applicationContext和源碼定義如下: applicationContext.xml 1 <mvc:interceptors>
2 <mvc:interceptor>
3 <mvc:mapping path="/**"/>
4 <!--攔截的url -->
5 <mvc:mapping path="/admin/**"/>
6 <!-- 不攔截的url start -->
7 <mvc:exclude-mapping path="/admin/login"/>
8 <mvc:exclude-mapping path="/admin/code"/>
9 <mvc:exclude-mapping path="/admin/logout"/>
10 <mvc:exclude-mapping path="/admin/msgErrorInfo"/>
11 <!--不攔截的url end -->
12 <bean class="authorizing.RequestInterceptor">
13 <property name="unauthenticatedUrl" value="/admin/msgErrorInfo" />
14 </bean>
15 </mvc:interceptor>
16 </mvc:interceptors> RequestInterceptor.java 1 public class RequestInterceptor extends HandlerInterceptorAdapter {
2
3 private String unauthenticatedUrl;
4
5 public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
6 Object handler) throws Exception {
7 if(PermissionUtils.isLogin(request)){
8 return true;
9 }
10 //token已失效,返回提示信息
11 request.getRequestDispatcher(unauthenticatedUrl).forward(request, response);
12 return false;
13 }
14
15 public void setUnauthenticatedUrl(String unauthenticatedUrl) {
16 this.unauthenticatedUrl = unauthenticatedUrl;
17 }
18 }
?
RequestInterceptor.java定義非常簡單,主要是在preHandler方法中驗證了一下請求是否是登錄用戶發出的,否則響應給前端一個重定向。然后看一下PermissionUtils.isLogin(request)是怎樣做登錄驗證的。 PermissionUtils.java 1 public class PermissionUtils { 2 private static ThreadLocal<String> sessionToken = new ThreadLocal<String>(); 3 4 public static boolean isLogin(HttpServletRequest request){ 5 String token = sessionToken(request); 6 if(StringUtils.isEmpty(token)) 7 return false; 8 /** 9 * 使用token檢查是否存在登錄session 10 */ 11 //Session session = SecurityUtils.getSecurityManager().getSession(new WebSessionKey(token, request, response)); 12 Session session = SecurityUtils.getSecurityManager().getSession(new DefaultSessionKey(token)); 13 if(session != null){ 14 session.touch(); 15 sessionToken.set(token); 16 return true; 17 } 18 return false; 19 } 20 21 private static String sessionToken(HttpServletRequest request){ 22 return request.getHeader("token"); 23 } 24 }?
從PermissionUtils.java可以判斷,保存前后端session的方式是通過token的形式。也即是每次request中的header部分都攜帶了登錄成功后獲取的token,以token為標識獲取登錄用戶的session。特別地,對于Shiro而言,session并非特定于Web應用,Shiro有自己的session定義,可以獨立于應用環境而存在。因此為了追求簡單(既已棄用了Shiro針對web.xml應用提供的Filter),直接使用Shiro創建的默認session(實際是SimpleSession)。此外,需要說明的一個細節是通過Shiro的SecurityManager 返回的session實際都是一個代理(DelegatingSession的實例)。因此,通過 SecurityManager獲取的session,然后對session執行的動作實際都是通過 SecurityManager的SessionManager來完成的(因為共享session,每一次session的touch動作都應該反映到共享session中,后文,可以看到overwrite SessionManager#touch(SessionKey key)和start session)。Shiro提供的默認SessionManager都繼承了AbstractValidatingSessionManager$sessionValidationSchedulerEnabled屬性,該屬性控制了是否執行一個后臺守護線程(Thread#setDaemon(true))在給定的一個固定時間間隔(默認1個小時)內周期性的檢查session是否過期,并且在每一次獲取到session之后都會去檢查session是否過期(對于共享session的集群,共享緩存基本都已具備超時管理功能,所以可以重新實現后文提到的 AbstractNativeSessionManager#getSession(SessionKey))。PermissionUtils.java中定義了一個ThreadLocal類型的sessionToken變量,該變量是用于暫存當前request authentication成功之后的session標識,避免每次獲取token都要從request中拿(后文中使用到的每一個url的authorization都需要首先執行一次checkPermission方法,通過token來驗證是否有訪問權限)。 接下來描述Authentication和Authorization,具體地說明如何基于Shiro實現login和check permission。下面先給出applicationContext配置。 applicationContext.xml <bean id="securityManager" class="org.apache.shiro.mgt.DefaultSecurityManager"><property name="realm" ref="authorizingRealm" /><property name="sessionManager"><bean class="service.authorizing.shiro.RedisSessionManager" ><property name="globalSessionTimeout" value="${session.timeout}" /></bean></property> </bean> <bean id="realmCache" class="service.authorizing.shiro.cache.RedisShiroCache" /> <bean id="authorizingRealm" class="service.authorizing.shiro.DefaultAuthorizingRealm"><property name="authorizationCachingEnabled" value="true"/><property name="authorizationCache" ref="realmCache" /> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/><bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"><property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/><property name="arguments" ref="securityManager"/> </bean>?
applicationContext.xml中配置的DefaultSecurityManager,RedisSessionManager,DefaultAuthorizingRealm和RedisShiroCache,分別代表Shiro的默認SecurityManager,自定義基于Redis的session manager,繼承自Shiro的AuthorizingRealm的默認實現,以及自定義基于Redis的用戶權限相關的Cache<Object, AuthorizationInfo>實現。注意到,本文的應用場景雖然是web.xml應用,但是并沒有使用Shiro提供的 DefaultWebSecurityManager和DefaultWebSessionManager這兩個針對web應用的拓展。使用針對web應用的拓展實現自然也沒問題,但是個人認為對于純粹的前后端分離權限認證的應用場景中,前端和后端應當是完全獨立的,它們之間唯一的耦合是通過Http request交互的token。因此就目前簡單和初期的原則,不需要DefaultWebSecurityManager和DefaultWebSessionManager。?
圖2 Shiro組件交互過程 在講解程序具體怎樣執行login和check permission之前,先看圖2所示的Shiro各組件的交互過程,可以看到Real是安全驗證的依據。所以有必要先理解Shiro提供的abstract類AuthorizingRealm,該類定義了兩個抽象方法doGetAuthorizationInfo和doGetAuthenticationInfo,分別用于check permission和login驗證。具體如下DefaultAuthorizingRealm.java的定義: DefaultAuthorizingRealm.java 1 public class DefaultAuthorizingRealm extends AuthorizingRealm { 2 3 @Autowired 4 private AuthorizingService authorizingService; 5 6 /** 7 * 獲取登錄用戶角色和功能權限信息, 8 * 使用{@link org.apache.shiro.cache.CacheManager}和{@link org.apache.shiro.cache.Cache}獲取數據. 9 * @param principals 登錄用戶ID 10 * @return 11 */ 12 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 13 Object username =principals.getPrimaryPrincipal(); 14 Cache<Object, AuthorizationInfo> infoCache = getAuthorizationCache(); 15 AuthorizationInfo info = infoCache.get(username); 16 return info; 17 } 18 19 /** 20 * 根據登錄用戶token,獲取用戶信息。 21 * 對于session timeout時間較短的場景可以考慮使用AuthenticationCache 22 * 若驗證失敗,會拋出異常 {@link AuthenticationException} 23 * @param token 24 * @return 25 * @throws AuthenticationException 26 */ 27 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 28 Object username = token.getPrincipal(); 29 //對于session timeout時間較短的場景,可緩存用戶authentication信息 30 //Cache<Object, AuthenticationInfo> infoCache = getAuthenticationCache(); 31 //return infoCache.get(username); 32 return authorizingService.authentication(username); 33 } 34 } DefaultAuthorizingRealm.java的實現,可以看到用戶只需要通過 doGetAuthorizationInfo和doGetAuthenticationInfo兩個方法給Shiro的SecurityManager提供Authorization和Authentication信息,SecurityManager就會在執行check permission和login操作時自動調用這兩個函數來驗證操作。下面我們再看執行login和check permission操作時具體做了什么。- Authentication
?
上述login代碼只做了非常簡單用戶名和密碼的驗證示例。可以看出login如果沒有拋出AuthenticationExeception,則說明登錄成功。- Authorization
?
從上述代碼來看,每一個request的checkPermission操作,都需要依賴前文RequestInterceptor.java中提到的,從request中獲取的token,并依賴該token找到緩存的session 。在權限控制的設計時,不同的業務場景可能需要不同粒度的權限控制,在這里做到了request參數級別的權限控制(在workflow應用中,一個流程涉及多個角色的參與,但很可能只抽象一個接口,如下文的/review操作)。在實現的時,靈活的方式是可以維護一張uri和permission_code之間的關系表(簡單可以propertites文件)。對于前端用戶而言,為了提升用戶體驗,擁有不同權限的用戶得到的界面會有相應的隱藏和顯示,因此會給前端的登錄用戶提供一張可訪問權限表。在這里一個細節的設計,個人覺得有意義的是,在返回給前端的權限表的Key值不應當是permission_code,而是uri。因為permission_code對于前端而言毫無意義,而uri正是前后端溝通的橋梁。因此,check Permission操作可以如下: ReviewApiController.java 1 @RestController 2 @RequestMapping(value = "/review") 3 public class ReviewApiController { 4 5 @Autowired 6 private ReviewService reviewService; 7 8 @ResponseBody 9 @RequestMapping(value = "/review", method = POST) 10 public WebResult review(@RequestBody NewReviewVo reviewVo){ 11 //檢查訪問權限 12 PermissionUtils.checkPermission("/review/review", reviewVo.getFeatureCode()); 13 WebResult result = WebResult.successResult(); 14 try { 15 Review review = ReviewAssembler.voToReview(reviewVo); 16 reviewService.review(review); 17 }catch (Exception e){ 18 result = WebResult.failureResult(e.getMessage()); 19 } 20 return result; 21 } 22 }?
- SessionManager
- 簡要的合并時序
?
圖3 合并時序- Authentication時序
圖4 Authentication時序
- Authorization時序
圖4 Authorization時序
5,總結 在使用Shiro框架進行Authentication和Authorization實踐時,雖然根據不同的業務場景需要做不同的修改或調整,但是基本也是最佳的實踐方式是時刻圍繞Shiro的設計原則和已有可借鑒的實現方案來操作,盡可能少或者不修改,從而避免一些預想不到的Bug。最后,重提前言部分說到的,除非是一定要通過共享緩存的方式共享session,否則還是使用Shiro默認的session管理。轉載于:https://www.cnblogs.com/shenjixiaodao/p/7426594.html
總結
以上是生活随笔為你收集整理的Session(数据)共享的前后端分离Shiro实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 团队检查情况
- 下一篇: 修改Linux网卡由eth1变成eth0