日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

Session(数据)共享的前后端分离Shiro实战

發(fā)布時(shí)間:2023/12/18 编程问答 65 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Session(数据)共享的前后端分离Shiro实战 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
1,前言 本文期望描述如何使用Shiro構(gòu)建基本的安全登錄和權(quán)限驗(yàn)證。本文實(shí)戰(zhàn)場(chǎng)景有如下特殊需求:1,在集群和分布式環(huán)境實(shí)現(xiàn)session共享;2,前端只使用HTML/CSS/JS。因此無(wú)法直接使用Shiro提供的SessionManager,以及Shiro針對(duì)web應(yīng)用提供的Filter攔截方式。當(dāng)然,除非是一定要通過(guò)共享緩存的方式共享session,否則還是使用Shiro默認(rèn)的session管理,畢竟增加獨(dú)立緩存就意味著維護(hù)成本的提高和可用性的下降。 2, Shiro架構(gòu) 首先一睹官方給出的Shiro架構(gòu)圖,如圖1所示。刨除最右側(cè)的加密工具類(lèi),主要圍繞SercurityManager來(lái)闡述。SercurityManager是Shiro安全框架里的頂層安全管理中心,所有安全控制相關(guān)邏輯都是在SercurityManager里面通過(guò)delegate的方式,調(diào)用到真正的動(dòng)作執(zhí)行者。從圖1可以清楚看到主要管理的組件:authentication管理,authorization管理,session管理,session緩存管理,cache管理,realms管理。(本文不想重復(fù)已有的文字,想要更好的了解Shiro,詳見(jiàn)官方推薦的Shiro full intro: https://www.infoq.com/articles/apache-shiro) 1)Shiro提供的CacheManager比較單薄,提供實(shí)現(xiàn)是MemoryConstrainedCacheManager,主要是依賴SoftHashMap來(lái)做基于內(nèi)存條件的緩存,也即是當(dāng)內(nèi)存吃緊,沒(méi)有新的內(nèi)存空間來(lái)存放new出來(lái)的對(duì)象時(shí),會(huì)去釋放SoftHashMap中存放的對(duì)象,在本文中的應(yīng)用場(chǎng)景是面向集群和分布式應(yīng)用環(huán)境,使用了Redi緩存登錄用戶的相關(guān)信息,所以需要自定義cache處理。 2)Shiro對(duì)于session的緩存管理,定義了SessionDAO抽象,并提供了兩個(gè)存放于本地JVM內(nèi)存的EnterpriseCacheSessionDAO和MemorySessionDAO,兩者主要區(qū)別是EnterpriseCacheSessionDAO的session存放在SoftHashMap中,原則上可以自己實(shí)現(xiàn)SessionDAO 接口,實(shí)際存儲(chǔ)使用Redis來(lái)做到完整的session共享,但是缺陷是:a,不安全,因?yàn)榘阉袛?shù)據(jù)都共享出去了;b,當(dāng)每次需要獲取session數(shù)據(jù)時(shí),都需要通過(guò)網(wǎng)絡(luò)來(lái)把整個(gè)session反序列化回來(lái),而考慮很多情況下,只是間斷的需要幾個(gè)key的數(shù)據(jù),這樣在session數(shù)據(jù)量大一些的時(shí)候,就會(huì)產(chǎn)生大量消耗。因此在共享session時(shí),不去替換默認(rèn)SessionDao的實(shí)現(xiàn),而是通過(guò)@overwrite AbstractNativeSessionManager getter/setter attribute方法,實(shí)現(xiàn)有選擇的共享session的基本初始化和指定attribute key的數(shù)據(jù)。 3)Shiro的authentication和authorization過(guò)程主要是依據(jù)用戶定義的 AuthorizingRealm中提供的AuthenticationInfo和AuthorizationInfo。特別地,authentication 還提供類(lèi)似驗(yàn)證鏈的authentication策略,允許用戶提供多個(gè)Realm。第3部分會(huì)具體的示例Shiro集成Spring的使用范例,并詳細(xì)解釋AuthorizingRealm 。 圖 1 Shiro官方架構(gòu)圖 3, Shiro使用范例 官方提供了集成Spring Web應(yīng)用的使用例子,但是就如前文提到的,這里前端只能使用JS的Http和后端通信,因此無(wú)法直接使用ShiroFilterFactoryBean來(lái)做Request的Filter。本文鑒于簡(jiǎn)單和初期的原則,可以選擇定義一個(gè)RequestInterceptor類(lèi)繼承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定義非常簡(jiǎn)單,主要是在preHandler方法中驗(yàn)證了一下請(qǐng)求是否是登錄用戶發(fā)出的,否則響應(yīng)給前端一個(gè)重定向。然后看一下PermissionUtils.isLogin(request)是怎樣做登錄驗(yàn)證的。 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的方式是通過(guò)token的形式。也即是每次request中的header部分都攜帶了登錄成功后獲取的token,以token為標(biāo)識(shí)獲取登錄用戶的session。特別地,對(duì)于Shiro而言,session并非特定于Web應(yīng)用,Shiro有自己的session定義,可以獨(dú)立于應(yīng)用環(huán)境而存在。因此為了追求簡(jiǎn)單(既已棄用了Shiro針對(duì)web.xml應(yīng)用提供的Filter),直接使用Shiro創(chuàng)建的默認(rèn)session(實(shí)際是SimpleSession)。此外,需要說(shuō)明的一個(gè)細(xì)節(jié)是通過(guò)Shiro的SecurityManager 返回的session實(shí)際都是一個(gè)代理(DelegatingSession的實(shí)例)。因此,通過(guò) SecurityManager獲取的session,然后對(duì)session執(zhí)行的動(dòng)作實(shí)際都是通過(guò) SecurityManager的SessionManager來(lái)完成的(因?yàn)楣蚕韘ession,每一次session的touch動(dòng)作都應(yīng)該反映到共享session中,后文,可以看到overwrite SessionManager#touch(SessionKey key)和start session)。Shiro提供的默認(rèn)SessionManager都繼承了AbstractValidatingSessionManager$sessionValidationSchedulerEnabled屬性,該屬性控制了是否執(zhí)行一個(gè)后臺(tái)守護(hù)線程(Thread#setDaemon(true))在給定的一個(gè)固定時(shí)間間隔(默認(rèn)1個(gè)小時(shí))內(nèi)周期性的檢查session是否過(guò)期,并且在每一次獲取到session之后都會(huì)去檢查session是否過(guò)期(對(duì)于共享session的集群,共享緩存基本都已具備超時(shí)管理功能,所以可以重新實(shí)現(xiàn)后文提到的 AbstractNativeSessionManager#getSession(SessionKey))。PermissionUtils.java中定義了一個(gè)ThreadLocal類(lèi)型的sessionToken變量,該變量是用于暫存當(dāng)前request authentication成功之后的session標(biāo)識(shí),避免每次獲取token都要從request中拿(后文中使用到的每一個(gè)url的authorization都需要首先執(zhí)行一次checkPermission方法,通過(guò)token來(lái)驗(yàn)證是否有訪問(wèn)權(quán)限)。 接下來(lái)描述Authentication和Authorization,具體地說(shuō)明如何基于Shiro實(shí)現(xiàn)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的默認(rèn)SecurityManager,自定義基于Redis的session manager,繼承自Shiro的AuthorizingRealm的默認(rèn)實(shí)現(xiàn),以及自定義基于Redis的用戶權(quán)限相關(guān)的Cache<Object, AuthorizationInfo>實(shí)現(xiàn)。注意到,本文的應(yīng)用場(chǎng)景雖然是web.xml應(yīng)用,但是并沒(méi)有使用Shiro提供的 DefaultWebSecurityManager和DefaultWebSessionManager這兩個(gè)針對(duì)web應(yīng)用的拓展。使用針對(duì)web應(yīng)用的拓展實(shí)現(xiàn)自然也沒(méi)問(wèn)題,但是個(gè)人認(rèn)為對(duì)于純粹的前后端分離權(quán)限認(rèn)證的應(yīng)用場(chǎng)景中,前端和后端應(yīng)當(dāng)是完全獨(dú)立的,它們之間唯一的耦合是通過(guò)Http request交互的token。因此就目前簡(jiǎn)單和初期的原則,不需要DefaultWebSecurityManager和DefaultWebSessionManager。

?

圖2 Shiro組件交互過(guò)程 在講解程序具體怎樣執(zhí)行l(wèi)ogin和check permission之前,先看圖2所示的Shiro各組件的交互過(guò)程,可以看到Real是安全驗(yàn)證的依據(jù)。所以有必要先理解Shiro提供的abstract類(lèi)AuthorizingRealm,該類(lèi)定義了兩個(gè)抽象方法doGetAuthorizationInfo和doGetAuthenticationInfo,分別用于check permission和login驗(yàn)證。具體如下DefaultAuthorizingRealm.java的定義: DefaultAuthorizingRealm.java 1 public class DefaultAuthorizingRealm extends AuthorizingRealm { 2 3 @Autowired 4 private AuthorizingService authorizingService; 5 6 /** 7 * 獲取登錄用戶角色和功能權(quán)限信息, 8 * 使用{@link org.apache.shiro.cache.CacheManager}和{@link org.apache.shiro.cache.Cache}獲取數(shù)據(jù). 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 * 根據(jù)登錄用戶token,獲取用戶信息。 21 * 對(duì)于session timeout時(shí)間較短的場(chǎng)景可以考慮使用AuthenticationCache 22 * 若驗(yàn)證失敗,會(huì)拋出異常 {@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 //對(duì)于session timeout時(shí)間較短的場(chǎng)景,可緩存用戶authentication信息 30 //Cache<Object, AuthenticationInfo> infoCache = getAuthenticationCache(); 31 //return infoCache.get(username); 32 return authorizingService.authentication(username); 33 } 34 } DefaultAuthorizingRealm.java的實(shí)現(xiàn),可以看到用戶只需要通過(guò) doGetAuthorizationInfo和doGetAuthenticationInfo兩個(gè)方法給Shiro的SecurityManager提供Authorization和Authentication信息,SecurityManager就會(huì)在執(zhí)行check permission和login操作時(shí)自動(dòng)調(diào)用這兩個(gè)函數(shù)來(lái)驗(yàn)證操作。下面我們?cè)倏磮?zhí)行l(wèi)ogin和check permission操作時(shí)具體做了什么。
  • Authentication
下面在LoginController.java定義了login請(qǐng)求操作。 LoginController.java 1 @Controller 2 @RequestMapping("/admin") 3 public class LoginController { 4 Logger logger = LoggerFactory.getLogger(LoginController.class); 5 6 @Autowired 7 private AuthorizingService authorizingService; 8 9 @RequestMapping("/login") 10 @ResponseBody 11 public LoginToken login(User user, HttpServletRequest request){ 12 Subject subject = new Subject.Builder().buildSubject(); 13 UsernamePasswordToken token = new UsernamePasswordToken(userName, UtilTool.md5Tool(password)); 14 token.setRememberMe(true); 15 LoginToken loginToken = new LoginToken(); 16 try{ 17 subject.login(token); 18 Session session = subject.getSession(); 19 user.setToken((String) session.getId()); 20 loginToken.setResultCode(WebConstants.RESULT_SUCCESS_CODE); 21 } catch (AuthenticationException e) { 22 loginToken.setResultCode(WebConstants.RESULT_FAIL_CODE); 23 loginToken.setMessage("用戶名或密碼錯(cuò)誤!"); 24 } 25 return loginToken; 26 } 27 }

?

上述login代碼只做了非常簡(jiǎn)單用戶名和密碼的驗(yàn)證示例。可以看出login如果沒(méi)有拋出AuthenticationExeception,則說(shuō)明登錄成功。
  • Authorization
訪問(wèn)權(quán)限控制需要在所有的訪問(wèn)controller的函數(shù)中配置,因此使用工具類(lèi)最合適(在工具類(lèi)的基礎(chǔ)上做成spring annotation也可以很方便),既是PermissionUtils.java。 PermissionUtils.java 1 private static AuthorizingService authorizingService; 2 3 private static ThreadLocal<String> sessionToken = new ThreadLocal<String>(); 4 5 /** 6 * 7 * @param url eg: /admin/review 8 * @param argv eg: WAIT_BIZ_MANAGER 9 */ 10 public static void checkPermission(String url, @Nullable String argv){ 11 Subject subject = getSubject(); 12 String permissionCode = authorizingService.uriMappingCode(url, argv); 13 if(StringUtils.isEmpty(permissionCode)) 14 throw new IllegalArgumentException("不明操作"); 15 subject.checkPermission(permissionCode); 16 } 17 18 public static Subject getSubject(){ 19 String token = sessionToken.get(); 20 if(StringUtils.isEmpty(token)) 21 throw new AuthenticationException("未經(jīng)認(rèn)證"); 22 return new Subject.Builder() 23 .sessionId(sessionToken.get()) 24 .buildSubject(); 25 } 26 27 public static void setAuthorizingService(AuthorizingService authorizingService) { 28 PermissionUtils.authorizingService = authorizingService; 29 }

?

從上述代碼來(lái)看,每一個(gè)request的checkPermission操作,都需要依賴前文RequestInterceptor.java中提到的,從request中獲取的token,并依賴該token找到緩存的session 。在權(quán)限控制的設(shè)計(jì)時(shí),不同的業(yè)務(wù)場(chǎng)景可能需要不同粒度的權(quán)限控制,在這里做到了request參數(shù)級(jí)別的權(quán)限控制(在workflow應(yīng)用中,一個(gè)流程涉及多個(gè)角色的參與,但很可能只抽象一個(gè)接口,如下文的/review操作)。在實(shí)現(xiàn)的時(shí),靈活的方式是可以維護(hù)一張uri和permission_code之間的關(guān)系表(簡(jiǎn)單可以propertites文件)。對(duì)于前端用戶而言,為了提升用戶體驗(yàn),擁有不同權(quán)限的用戶得到的界面會(huì)有相應(yīng)的隱藏和顯示,因此會(huì)給前端的登錄用戶提供一張可訪問(wèn)權(quán)限表。在這里一個(gè)細(xì)節(jié)的設(shè)計(jì),個(gè)人覺(jué)得有意義的是,在返回給前端的權(quán)限表的Key值不應(yīng)當(dāng)是permission_code,而是uri。因?yàn)閜ermission_code對(duì)于前端而言毫無(wú)意義,而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 //檢查訪問(wèn)權(quán)限 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
由于要實(shí)現(xiàn)有選擇的共享session數(shù)據(jù),因此session管理成了最棘手的問(wèn)題,因?yàn)槟悴皇谴直┑貙⒄麄€(gè)session序列化到緩存并仍以local session的方式管理,其間需要額外得小心處理共享的session數(shù)據(jù)和本地的session數(shù)據(jù)。下面給出RedisSessionManager.java的實(shí)現(xiàn): RedisSessionManager.java 1 /** 2 * 根據(jù) attributeKey,有選擇的緩存session信息; 3 * 設(shè)置 {@parm enabledSharedSessionData}來(lái)有選擇的啟用共享session功能。 4 */ 5 public class RedisSessionManager extends DefaultSessionManager { 6 7 private static Logger logger = LoggerFactory.getLogger(RedisSessionManager.class); 8 9 private boolean enabledSharedSessionData; 10 11 private Set<String> sharedSessionDataKeys; 12 13 public RedisSessionManager() { 14 enabledSharedSessionData = true; 15 sharedSessionDataKeys = new HashSet<String>(); 16 } 17 18 @Override 19 public Collection<Object> getAttributeKeys(SessionKey key) { 20 21 Collection<Object> keys = super.getAttributeKeys(key); 22 if(enabledSharedSessionData) { 23 /** 24 * 從redis獲取 {@param key} 對(duì)應(yīng)session的所有attribute key 25 */ 26 Set sharedKeys = RedisClient.extractAttributeKey((String) key.getSessionId()); 27 keys.addAll(sharedKeys); 28 } 29 return keys; 30 } 31 32 @Override 33 public Object getAttribute(SessionKey sessionKey, Object attributeKey) 34 throws InvalidSessionException { 35 if(checkSharedStrategy(attributeKey)){ 36 Object object = RedisClient.getValue((String) attributeKey, (String) sessionKey.getSessionId()); 37 return object; 38 } 39 return super.getAttribute(sessionKey, attributeKey); 40 } 41 42 @Override 43 public void setAttribute(SessionKey sessionKey, Object attributeKey, Object value) 44 throws InvalidSessionException { 45 if(checkSharedStrategy(attributeKey)) { 46 if(value instanceof Serializable) 47 RedisClient.setValue((String) attributeKey, (String) sessionKey.getSessionId(), 48 (Serializable) value, getGlobalSessionTimeout(), TimeUnit.MILLISECONDS); 49 else 50 throw new IllegalArgumentException("不可共享非序列化value"); 51 return; 52 } 53 super.setAttribute(sessionKey, attributeKey, value); 54 } 55 56 private boolean checkSharedStrategy(Object attributeKey){ 57 return enabledSharedSessionData && sharedSessionDataKeys.contains(attributeKey); 58 } 59 60 /** 61 * 如果是集群, session只在一臺(tái)機(jī)器上創(chuàng)建,因此必須共享 SessionId。 62 * 當(dāng)request發(fā)過(guò)來(lái),獲取request中攜帶的 SessionId,使用 SessionId 在本地獲取session, 63 * 如果為null,則用 SessionId 去redis檢查是否存在,如果存在則在本地構(gòu)建session返回 64 * (實(shí)際就是{@link SimpleSession}的代理{@link DelegatingSession},{@see RedisSessionManager#restoreSession}), 65 * 否則返回空, 請(qǐng)求重新登錄。 66 * {@link org.apache.shiro.session.mgt.AbstractNativeSessionManager#getSession(SessionKey)} 67 * @param key 68 * @return 69 * @throws SessionException 70 */ 71 @Override 72 public Session getSession(SessionKey key) throws SessionException { 73 Session session = null; 74 try { 75 session = getLocalSession(key); 76 } catch (UnknownSessionException use){ 77 //ignored 78 session = null; 79 } 80 if(!enabledSharedSessionData || session != null) 81 return session; 82 /** 83 * 檢查redis,判斷session是否已創(chuàng)建, 84 * 若已創(chuàng)建,則使用SessionFactory在本地構(gòu)建SimpleSession 85 */ 86 Serializable sid = RedisClient.getValue((String) key.getSessionId()); 87 if(sid != null){ 88 session = restoreSession(key); 89 } 90 91 return session; 92 } 93 94 /** 95 * 每一次通過(guò) 96 * {@link org.apache.shiro.session.mgt.AbstractValidatingSessionManager#doGetSession(SessionKey)}} 97 * 獲取session 98 * 或是通過(guò){@link org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler} 99 * 定時(shí)檢查,都會(huì)去調(diào)用 100 * {@link org.apache.shiro.session.mgt.AbstractValidatingSessionManager#doValidate(Session)} 101 * 驗(yàn)證session是否過(guò)期。 102 * 共享session過(guò)期的標(biāo)準(zhǔn)是該redis中sessionId過(guò)期, 由于redis已經(jīng)幫助完成了session過(guò)期檢查, 103 * 所以這里只需要定期清理本地內(nèi)存中的過(guò)期session。 104 * 然而{@link org.apache.shiro.session.mgt.AbstractValidatingSessionManager#doGetSession(SessionKey)}} 105 * 是一個(gè)final方法,無(wú)法被overwrite,所以只能copy Shiro原來(lái)的代碼實(shí)現(xiàn)來(lái)定義getLocalSession(SessionKey key) 106 * @param key 107 * @return 108 */ 109 private Session getLocalSession(SessionKey key){ 110 Session session = lookupSession(key); 111 return session != null ? createExposedSession(session, key) : null; 112 } 113 private Session lookupSession(SessionKey key) throws SessionException { 114 if (key == null) { 115 throw new NullPointerException("SessionKey argument cannot be null."); 116 } 117 //enableSessionValidationIfNecessary 118 SessionValidationScheduler scheduler = getSessionValidationScheduler(); 119 if (enabledSharedSessionData || 120 (isSessionValidationSchedulerEnabled() && (scheduler == null || !scheduler.isEnabled())) 121 ) { 122 enableSessionValidation(); 123 } 124 Session s = retrieveSession(key); 125 if (!enabledSharedSessionData && s != null) { 126 validate(s, key); 127 } 128 return s; 129 } 130 131 /** 132 * 根據(jù){@link SessionKey}以及繼承自{@link DefaultSessionManager}的默認(rèn)創(chuàng)建方法, 133 * 重新在本地構(gòu)建session。 134 * @param key 135 * @return 136 */ 137 private Session restoreSession(SessionKey key){ 138 SimpleSession restoreSession = (SimpleSession) getSessionFactory().createSession(null); 139 restoreSession.setId(key.getSessionId()); 140 restoreSession.setTimeout(getGlobalSessionTimeout()); 141 create(restoreSession); 142 return createExposedSession(restoreSession, key); 143 } 144 145 /** 146 * 開(kāi)啟一個(gè)新的session, 并且在新的session開(kāi)啟之后做一系列的session共享工作。 147 * {@link org.apache.shiro.session.mgt.AbstractNativeSessionManager#start(SessionContext)} 148 * @param context 149 * @return 150 */ 151 @Override 152 public Session start(SessionContext context) { 153 Session session = super.start(context); 154 if(enabledSharedSessionData){ 155 shareSessionData(session); 156 } 157 return session; 158 } 159 /** 160 * 完成session基本數(shù)據(jù)共享 161 */ 162 private void shareSessionData(Session session){ 163 refreshTTL(session.getId()); 164 } 165 /** 166 * 刷新session存活時(shí)間 167 */ 168 private void refreshTTL(Serializable sessionId){ 169 RedisClient.setValue((String) sessionId, new Date(), 170 getGlobalSessionTimeout(), TimeUnit.MILLISECONDS); 171 } 172 173 /** 174 * {@link org.apache.shiro.session.mgt.AbstractNativeSessionManager#touch(SessionKey)} 175 * @param key 176 * @throws InvalidSessionException 177 */ 178 @Override 179 public void touch(SessionKey key) throws InvalidSessionException { 180 if(enabledSharedSessionData){ 181 //刷新session存活時(shí)間 182 refreshTTL(key.getSessionId()); 183 } 184 super.touch(key); 185 } 186 187 /** 188 * 當(dāng)主動(dòng)調(diào)用{@link Subject#logout()}時(shí),相應(yīng)會(huì)調(diào)用該方法來(lái)停止session。 189 * 因此,如果共享了session,也需要即時(shí)清除共享session。 190 * {@link org.apache.shiro.session.mgt.AbstractNativeSessionManager#stop(SessionKey)} 191 * @param key 192 * @throws InvalidSessionException 193 */ 194 @Override 195 public void stop(SessionKey key) throws InvalidSessionException { 196 super.stop(key); 197 if(enabledSharedSessionData) 198 RedisClient.delete((String) key.getSessionId()); 199 } 200 201 /** 202 * {@link org.apache.shiro.session.mgt.AbstractNativeSessionManager#getLastAccessTime(SessionKey)} 203 * @param key 204 * @return 205 */ 206 @Override 207 public Date getLastAccessTime(SessionKey key) { 208 Serializable lastAccessTime = enabledSharedSessionData ? 209 RedisUtils.getValue((String) key.getSessionId()) : 210 super.getLastAccessTime(key); 211 if(lastAccessTime == null) 212 throw new SessionTimeoutException(); 213 return (Date) lastAccessTime; 214 } 215 216 /** 217 * 通知session manager那些attribute key對(duì)應(yīng)的數(shù)據(jù)需要共享。 218 * @param key 219 */ 220 public void registerSharedAttributeKey(String key){ 221 if(!enabledSharedSessionData) 222 throw new IllegalArgumentException("不允許共享session數(shù)據(jù)"); 223 if(sharedSessionDataKeys == null) 224 sharedSessionDataKeys = new HashSet<String>(); 225 sharedSessionDataKeys.add(key); 226 } 227 } View Code 由于Redis本身就是單線程模型,所以作為客戶端基本不需要考慮線程安全問(wèn)題。下面就各個(gè)問(wèn)題來(lái)詳細(xì)說(shuō)明?RedisSessionManager。既然需求是想要實(shí)現(xiàn)在集群和分布式環(huán)境下,有選擇的共享session數(shù)據(jù),這意味著有一下問(wèn)題需要處理:1,怎樣做到有選擇的共享session數(shù)據(jù)?2,本地session過(guò)期了怎樣清理,以及怎樣避免Shiro每次獲取本地session都會(huì)進(jìn)行過(guò)期驗(yàn)證和Redis的過(guò)期驗(yàn)證之間的重復(fù)? 3,怎樣管理session存活時(shí)間?4,session只在一臺(tái)機(jī)器上創(chuàng)建,既然不是共享了整個(gè)session,那么其它機(jī)器如何重建session? 對(duì)于第1個(gè)問(wèn)題,RedisSessionManager.java定義了enabledSharedSessionData和sharedSessionDataKeys兩個(gè)變量來(lái)控制session數(shù)據(jù)共享,如果要求共享session數(shù)據(jù),則需要通過(guò)registerSharedAttributeKey(String key)來(lái)告知session manager那些attribute key需要被共享,并定義checkSharedStrategy(Object attributeKey) 方法來(lái)檢查attribute key是否共享。余下就是overwrite getter/setter attribute方法就可以了。這里再提一下,對(duì)于設(shè)置enabledSharedSessionData=true,除非是一定要通過(guò)共享緩存的方式共享session,否則還是使用Shiro默認(rèn)的session管理,畢竟增加獨(dú)立緩存就意味著維護(hù)成本的提高和可用性的下降。 對(duì)于第2個(gè)問(wèn)題,Shiro提供的session manager已經(jīng)完成了local session的管理動(dòng)作,因此我們只需要把local session的管理操作直接交給Shiro提供的默認(rèn)session manager就可以了,而對(duì)于共享的session數(shù)據(jù),Redis已經(jīng)提供了數(shù)據(jù)過(guò)期管理功能(或者其它緩存工具基本都提供了)。因?yàn)镾hiro提供的session manager清理session的原則是session已經(jīng)過(guò)期或已經(jīng)stop,那么session manager是怎樣自動(dòng)讓session進(jìn)入過(guò)期狀態(tài)的呢?從AbstractNativeSessionManager#getSession(SessionKey)方法就可以追溯到,每一次通過(guò)該方法獲取session不為空,都會(huì)調(diào)用SimpleSesion#validate()方法來(lái)驗(yàn)證session是否過(guò)期。此外,Shiro也提供了ExecutorServiceSessionValidationScheduler類(lèi)來(lái)開(kāi)啟一個(gè)后臺(tái)的固定周期執(zhí)行的守護(hù)線程來(lái)執(zhí)行session驗(yàn)證。既然Redis已經(jīng)可以做到session有效性管理,那就沒(méi)必要在每次獲取session的時(shí)候都去主動(dòng)的驗(yàn)證一次session。然而,getSession操作實(shí)際,Shiro提供的實(shí)現(xiàn)實(shí)際是調(diào)用了一個(gè)final類(lèi)型AbstractValidatingSessionManager#doGetSession(SessionKey)方法,這意味著無(wú)法通過(guò)overwrite的方式來(lái)避免主動(dòng)調(diào)用SimpleSesion#validate()。因此,在自定義sesssion manager中定義了getLocalSession(SessionKey key)方法,該方法本質(zhì)實(shí)際是參照Shiro提供的實(shí)現(xiàn),并在基礎(chǔ)之上加上場(chǎng)景約束。 對(duì)于第3個(gè)問(wèn)題,在解釋第2問(wèn)題時(shí)已提到,Redis已自帶超時(shí)管理功能,因此session存活時(shí)間只需要由Redis管理即可,而Shiro只需要開(kāi)啟一個(gè)固定周期的后臺(tái)任務(wù)來(lái)清理本地?zé)o效session即可。 對(duì)于第4個(gè)問(wèn)題,在前后端完全分離的應(yīng)用場(chǎng)景下,用戶authentication通過(guò)之后由Shiro自動(dòng)創(chuàng)建的session,里面包含的大部分?jǐn)?shù)據(jù)都是可選共享的,而Shiro提供的最核心的Session實(shí)現(xiàn),實(shí)際就是允許空參構(gòu)造函數(shù)的SimpleSession。所以,實(shí)際我們只需共享出全局唯一的sessionId(shareSessionData(Session session) 方法實(shí)現(xiàn)),即可使用session manager提供的getSessionFactory()方法獲取默認(rèn)session factory,然后通過(guò)該factory即可創(chuàng)建SimpleSession并設(shè)置相應(yīng)的共享數(shù)據(jù),即restoreSession(SessionKey key)方法定義的過(guò)程。在Shiro提供的默認(rèn)session manager中可以看到,所有的session創(chuàng)建都是通過(guò)AbstractNativeSessionManager#start(SessionContext)完成的,所以只需要overwrite這個(gè)方法并共享新創(chuàng)建session的必要數(shù)據(jù)即可。最后,結(jié)合問(wèn)題2中提到的getLocalSession(SessionKey key)方法,獲取session的方法getSession(SessionKey key)的實(shí)現(xiàn)分為兩步:第一步是通過(guò) getLocalSession(SessionKey key) 獲取;如果第一步返回null且Redis中session未過(guò)期,則第二步通過(guò)restoreSession(SessionKey key)在本地重建session 。特別地,從refreshTTL(Serializable sessionId)方法的定義,可以看到共享sessionId的同時(shí),對(duì)應(yīng)的存放了該session的LastAccessTime。 4,Authentication和Authorization執(zhí)行時(shí)序 在第3部分,已經(jīng)給出了一個(gè)基本的基于Shiro的前后端分離的共享session實(shí)戰(zhàn)范例,因此在這一部分將基于第3部分,通過(guò)時(shí)序圖來(lái)表述Authentication和Authorization的執(zhí)行流程。
  • 簡(jiǎn)要的合并時(shí)序

?

圖3 合并時(shí)序
  • Authentication時(shí)序

圖4 Authentication時(shí)序

  • Authorization時(shí)序

圖4 Authorization時(shí)序

5,總結(jié) 在使用Shiro框架進(jìn)行Authentication和Authorization實(shí)踐時(shí),雖然根據(jù)不同的業(yè)務(wù)場(chǎng)景需要做不同的修改或調(diào)整,但是基本也是最佳的實(shí)踐方式是時(shí)刻圍繞Shiro的設(shè)計(jì)原則和已有可借鑒的實(shí)現(xiàn)方案來(lái)操作,盡可能少或者不修改,從而避免一些預(yù)想不到的Bug。最后,重提前言部分說(shuō)到的,除非是一定要通過(guò)共享緩存的方式共享session,否則還是使用Shiro默認(rèn)的session管理。

轉(zhuǎn)載于:https://www.cnblogs.com/shenjixiaodao/p/7426594.html

總結(jié)

以上是生活随笔為你收集整理的Session(数据)共享的前后端分离Shiro实战的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。