1.簡介
CAS:Yale 大學發起的一個開源項目,旨在為 Web 應用系統提供一種可靠的單點登錄方法。
Shiro:Apache Shiro是一個Java安全框架,可以幫助我們完成認證、授權、會話管理、加密等,並且提供與web集成、緩存、rememberMed等功能。
*Shiro支持與CAS進行整合使用.
2.CAS Server搭建
參考:https://www.cnblogs.com/funyoung/p/9234947.html
3.CAS Client搭建
3.1 添加Shiro自身以及整合CAS的相關依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.4.0</version>
</dependency>
3.2 配置Spring提供的過濾器代理
在web.xml中配置DelegatingFilterProxy並指定targetBeanName。
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
*DelegatingFilterProxy是一個標准的Filter代理,通過targetBeanName指定其要代理的Filter的bean的id(默認情況下將代理bean id為filter-name的Filter)
3.2 新增shiro.properties配置文件並設置相關屬性
shiro.loginUrl=http://127.0.0.1:8080/cas/login?service=http://127.0.0.1:8080/A/shiro-cas
shiro.logoutUrl=http://127.0.0.1:8080/cas/logout?service=http://127.0.0.1:8080/A/shiro-cas
shiro.cas.serverUrlPrefix=http://127.0.0.1:8080/cas
shiro.cas.service=http://127.0.0.1:8080/A/shiro-cas
shiro.successUrl=http://127.0.0.1:8081/front/index
shiro.failureUrl=http://127.0.0.1:8081/front/index
*在spring-shiro.xml中需要使用到此文件的配置項.
3.3 創建自定義的CasRealm
*Shiro為了與CAS進行整合,提供了CasRealm實現類,其已經對AuthozingRealm抽象類聲明的doGetAuthenticationInfo(AuthenticationToken token)、doGetAuthorizationInfo(PrincipalCollection principals)方法進行實現。
認證方法:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //CasToken是AuthenticationToken的實現類,其principal為null,credential為ticket. CasToken casToken = (CasToken) token; if (token == null) { return null; } String ticket = (String)casToken.getCredentials(); if (!StringUtils.hasText(ticket)) { return null; } //ticket檢驗器 TicketValidator ticketValidator = ensureTicketValidator(); try { // 去CAS服務端中驗證ticket的合法性並獲取用戶名進行補全 Assertion casAssertion = ticketValidator.validate(ticket, getCasService()); // 從CAS服務端中獲取相關屬性,包括用戶名、是否設置RememberMe等 AttributePrincipal casPrincipal = casAssertion.getPrincipal(); String userId = casPrincipal.getName(); log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{ ticket, getCasServerUrlPrefix(), userId }); Map<String, Object> attributes = casPrincipal.getAttributes(); // refresh authentication token (user id + remember me) casToken.setUserId(userId); String rememberMeAttributeName = getRememberMeAttributeName(); String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName); boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue); if (isRemembered) { casToken.setRememberMe(true); } // 最終創建SimpleAuthencationInfo實體返回給SecurityManager List<Object> principals = CollectionUtils.asList(userId, attributes); PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName()); return new SimpleAuthenticationInfo(principalCollection, ticket); } catch (TicketValidationException e) { throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e); } }
*AuthenticationToken的Credential是ticket,而返回的AuthenticationInfo的Credential仍是ticket,之間僅經過一層ticket校驗,並不需要經過分子系統的數據庫校驗,因為在CAS服務端進行認證時已經經過一次全局的數據庫校驗。
*若使用CasRealm,則在分子系統進行登錄時(CasFilter),SecurityManager會調用該Realm的doGetAuthenticationInfo方法獲取用戶的安全信息,通過AuthenticationToken的Credential與從Realm中獲取的AuthenticationInfo中的Credential進行密碼的比對,若應用添加了黑白名單功能則需要自定義Realm繼承CasRealm,重寫其提供的doGetAuthenticationInfo方法。
授權方法:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 恢復用戶信息 SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals; List<Object> listPrincipals = principalCollection.asList(); Map<String, String> attributes = (Map<String, String>) listPrincipals.get(1); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 為用戶添加默認的角色 addRoles(simpleAuthorizationInfo, split(defaultRoles)); // 為用戶添加默認的行為 addPermissions(simpleAuthorizationInfo, split(defaultPermissions)); // 為用戶添加預設置的角色 List<String> attributeNames = split(roleAttributeNames); for (String attributeName : attributeNames) { String value = attributes.get(attributeName); addRoles(simpleAuthorizationInfo, split(value)); } // 為用戶添加預設置的行為 attributeNames = split(permissionAttributeNames); for (String attributeName : attributeNames) { String value = attributes.get(attributeName); addPermissions(simpleAuthorizationInfo, split(value)); } return simpleAuthorizationInfo; }
*CasRealm實現的doGetAuthorizationInfo方法僅僅是為用戶添加默認和預定義的角色與行為,並不符合實際的應用場景,因此也需要進行自定義。
自定義Realm繼承CasRealm重寫其doGetAuthenticationInfo和doGetAuthorizationInfo方法
/** * 用戶登錄和授權用的realm */ public class CasUserRealm extends CasRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private MenuService menuService; /** * CAS認證 ,驗證用戶身份 * 將用戶基本信息設置到會話中 */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { //調用CasRealm實現的認證方法,其包含驗證ticket、填充CasToken的principal等操作) AuthenticationInfo authc = super.doGetAuthenticationInfo(token); String username = (String) authc.getPrincipals().getPrimaryPrincipal(); User user = userService.findByUsername(username); if (user != null) { //黑名單限制 if (Global.NO.equals(user.getLoginFlag())) { throw new AuthenticationException("msg:該帳號禁止登錄"); } //將用戶信息放在session SecurityUtils.getSubject().getSession().setAttribute("user", user); return authc; } else { return null; } } /** * 設置角色和權限信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); User user = userService.findByUsername(username); if (user != null) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //獲取用戶擁有的角色 List<Role> roles = roleService.findByUserId(user.getId()); for (Role role : roles) { authorizationInfo.addRole(role.getEnname()); //獲取用戶擁有的權限 List<Menu> menus = menuService.findByRoleId(role.getId()); for(Menu menu : menus){ if(StringUtils.isNotBlank(menu.getPermission())){ authorizationInfo.addStringPermission(menu.getPermission()); } } } return authorizationInfo; } else { return null; } } }
3.4 完整的spring-shiro.xml配置
<!-- 配置shiroFilterFactoryBean,bean的id默認情況下必須與web.xml文件中DelegatingFilterProxy過濾器的filter-name相同,可以通過filter的targetBeanName初始化參數進行修改 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 注入securityManager -->
<property name="securityManager" ref="securityManager"/>
<!-- 設置登錄URL,當用戶未認證,但訪問了需要認證后才能訪問的頁面,就會自動重定向到登錄URL -->
<property name="loginUrl" value="${shiro.loginUrl}"/>
<!-- 設置沒有權限的URL,當用戶認證后,訪問一個頁面卻沒有權限時,就會自動重定向到沒有權限的URL,若用戶未認證訪問一個需要權限的URL時,會跳轉到登錄URL -->
<property name="unauthorizedUrl" value="/unauthorized.html"/>
<!-- 將Filter添加到Shiro過濾器鏈中,用於對資源設置權限 -->
<property name="filters">
<map>
<entry key="casFilter" value-ref="casFilter"/>
<entry key="logoutFilter" value-ref="logoutFilter"/>
</map>
</property>
<!-- 配置哪些請求需要受保護,以及訪問這些頁面需要的權限 -->
<property name="filterChainDefinitions">
<value> /shiro-cas=casFilter /logout = logoutFilter /** = authc </value>
</property>
</bean>
<!-- 單點登錄過濾器 -->
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<!-- 配置驗證成功時跳轉的URL -->
<property name="successUrl" value="${shiro.successUrl}"/>
<!-- 配置驗證錯誤時跳轉的URL -->
<property name="failureUrl" value="${shiro.failureUrl}"/>
</bean>
<!--單點登出過濾器-->
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<!-- 注銷時重定向的URL -->
<property name="redirectUrl" value="${shiro.logoutUrl}"/>
</bean>
<!-- 注冊自定義CasRealm -->
<bean id="casRealm" class="com.realm.CasUserRealm">
<!-- cas服務端地址前綴,作為ticket校驗 -->
<property name="casServerUrlPrefix" value="${shiro.cas.serverUrlPrefix}"/>
<!-- 應用服務地址,用來接收CAS服務端的票據 -->
<property name="casService" value="${shiro.cas.service}"/>
</bean>
<!-- 配置securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="subjectFactory" ref="casSubjectFactory"/>
<property name="realm" ref="casRealm"/>
</bean>
<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"></bean>
<!-- 配置lifecycleBeanPostProcessor,shiro bean的生命周期管理器,可以自動調用Spring IOC容器中shiro bean的生命周期方法(初始化/銷毀)-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 為了支持Shiro的注解需要定義DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor兩個bean -->
<!-- 配置DefaultAdvisorAutoProxyCreator,必須配置了lifecycleBeanPostProcessor才能使用 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<!-- 配置AuthorizationAttributeSourceAdvisor -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!-- 自動注入properties屬性文件 -->
<bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath:conf/shiro.properties</value>
</list>
</property>
</bean>
*/shiro-cas必須可以在未認證的狀態下被訪問,可以將其排在第一位。
*當直接訪問shiro.loginUrl進行登錄時,若登錄成功則跳轉到casFilter配置的successUrl,若是訪問受限的URL被Shiro重定向到shiro.loginUrl進行登錄時,登錄成功則會跳轉到原訪問的被Shiro攔截的URL。
*當在CAS服務端認證不通過時,並不會將請求重定向到casFilter,因此casFilter配置的failureUrl不會生效。
4.Shiro整合CAS原理分析
4.1 項目架構圖
4.2 用戶首次訪問項目A
1.用戶的請求首先到達項目A的ShiroFilter。
2.若用戶訪問需認證的URL(非user攔截器),由於未進行認證,Subject的isAuthenticated()方法返回false,則Shiro將請求重定向到ShiroFilter配置好的loginUrl進行登錄處理。
3.由於TGC不能成功匹配TGT,因此CAS服務端認為用戶未進行登錄,將請求轉發到登錄頁面。
4.輸入用戶名/密碼進行登錄,若CAS服務端認證成功,則生成TGC Cookie保存到客戶端瀏覽器進程所占用的內存,生成TGT保存在CAS服務端位於的內存,通過TGT簽發ST,最終回調service參數中的URL並攜帶ticket參數傳遞ST,若CAS服務端認證失敗,則提示密碼錯誤。
5.若CAS認證成功則請求最終到達項目A的CasFilter,執行其executeLogin方法(即執行分子系統的Shiro認證)
5.1 從HTTP請求中獲取ticket參數,將其構造成CasToken實例。
5.2 執行Subject.login(AuthenticationToken token)方法。
5.3 SecurityManager首先判斷緩存中是否存在用戶對應的AuthenticationInfo實體,若不存在則調用CasRealm的doGetAuthenticationInfo方法進行獲取並將其放入到緩存中,執行分子系統的認證操作,最終將authenticated屬性設置為true標識用戶已進行登錄並將用戶的身份信息放入Subject的PrincipalCollection實體。
5.4 若用戶訪問的資源需要權限,此時Shiro就會調用Subject的isPermitted(String str)方法來檢驗用戶的權限,首先判斷Subject中的PincipalCollection實體是否包用戶的身份信息 (已登錄才有),若包含則判斷緩存中是否存在用戶對應的AuthorizationInfo實體,若存在則從緩存中獲取,否則將調用Realm的doGetAuthorizationInfo方法進行獲取,最終將AuthorizationInfo實體放入緩存。
5.5 若用戶具有特定的權限則允許訪問資源,否則將跳轉到ShiroFilter配置好的unauthorizedUrl。
*Shiro是通過Subject的isAuthenticated()方法判斷當前用戶是否已經登錄的,當執行登錄操作后會將Subject的authenticated屬性設值為true並將用戶的身份信息放入Subject的PrincipalCollection實體中。
*若用戶訪問的URL是user攔截器的,則Subject根據isAuthenticated()方法和isRememberMe()方法判斷用戶是否需要進行登錄,若任意一個方法返回true則表示用戶不需進行登錄。
*當關閉瀏覽器重新訪問時將產生新的Subject對象,isAuthenticated()方法返回false,除非設置了RememberMe否則都需要重新進行登錄。
*loginUrl的值為CAS服務端的登錄處理URL,並且需在URL后拼接Service參數傳遞當認證成功后的回調地址,回調地址必須進入預定義好的CasFilter過濾器且能支持匿名訪問。
*若是通過訪問URL被重定向到loginUrl的請求,當認證成功后將會跳轉原訪問的URL。
*若是直接訪問loginUrl請求,當認證成功后會跳轉casFilter配置好的successUrl。
CasFilter:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { //獲取AuthenticationToken實體 AuthenticationToken token = createToken(request, response); if (token == null) { String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " + "must be created in order to execute a login attempt."; throw new IllegalStateException(msg); } try { Subject subject = getSubject(request, response); //執行分子系統的Shrio認證與授權 subject.login(token); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } } protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { //獲取CAS Server回調請求中的ticket參數,構造CasToken實體,其principal為null,credential為ticket. HttpServletRequest httpRequest = (HttpServletRequest) request; String ticket = httpRequest.getParameter(TICKET_PARAMETER); return new CasToken(ticket); }
AuthorizingRealm的getAuthorizationIfno獲取用戶權限信息:
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { if (principals == null) { return null; } AuthorizationInfo info = null; if (log.isTraceEnabled()) { log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]"); } //獲取緩存實體Cache Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache(); if (cache != null) { if (log.isTraceEnabled()) { log.trace("Attempting to retrieve the AuthorizationInfo from cache."); } Object key = getAuthorizationCacheKey(principals); //從緩存中獲取用戶的權限信息,即AuthorizationInfo實體. info = cache.get(key); if (log.isTraceEnabled()) { if (info == null) { log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]"); } else { log.trace("AuthorizationInfo found in cache for principals [" + principals + "]"); } } } //若緩存中沒有用戶的權限信息則調用Realm的doGetAuthorizationInfo方法通過數據庫進行查詢 if (info == null) { info = doGetAuthorizationInfo(principals); if (info != null && cache != null) { if (log.isTraceEnabled()) { log.trace("Caching authorization info for principals: [" + principals + "]."); } //構造緩存Key,將用戶的權限信息放入緩存中 Object key = getAuthorizationCacheKey(principals); cache.put(key, info); } } return info; }
*Subject的PrincipalCollection實體存放着用戶的身份信息,當已登錄的情況下訪問需授權的資源時就會調用其isPermitted(String str)方法將通過緩存或Realm的doGetAuthorizationInfo方法獲取用戶的權限信息。
4.3 用戶再次訪問項目A
1.請求首先到達項目A的ShiroFilter,由於已進行認證,Subject的isAuthenticated()方法返回true(無關閉瀏覽器的前提下)
2.若用戶訪問需要權限的資源時,就會調用Subject的isPermitted(String str)方法來檢驗用戶的權限,其底層調用AuthorizingRealm的getAuthorizationInfo(PrincipalCollection principals)方法獲取用戶的權限信息。
3.若用戶具有特定的權限則允許訪問資源,否則將跳轉到ShiroFilter配置好的unauthorizedUrl。
*若用戶訪問的URL是user攔截器的,則Subject根據isAuthenticated()方法和isRememberMe()方法判斷用戶是否需要進行登錄,若任意一個方法返回true則表示用戶不需進行登錄.。
4.4 用戶第一次訪問項目B
1.請求首先到達項目B的ShiroFilter。
2.若用戶訪問需認證的URL,由於未進行認證,Subject的isAuthenticated()方法返回false,則Shiro將請求重定向到ShiroFilter配置好的loginUrl。
3.由於之前已登錄過,存在TGC Cookie,因此TGC能成功匹配TGT,因此CAS服務端認為用戶已進行登錄。
4.CAS服務端使用TGT簽發ST,回調service參數中的URL並攜帶ticket參數傳遞ST。
5.請求到達CasFilter,執行其executeLogin方法( 即執行分子系統的Shiro認證 )
5.1 從HTTP請求中獲取ticket參數,將其構造成CasToken實例。
5.2 執行Subject.login(AuthenticationToken token)方法。
5.3 SecurityManager調用自定義的CasRealm的doGetAuthenticationInfo方法,執行分子系統的認證操作,最終將authenticated屬性設置為true標識用戶已進行登錄並將用戶的身份信息放入Subject的PrincipalCollection實體。
5.4 若用戶訪問的資源需要權限,此時Shiro就會調用Subject的isPermitted(String str)方法來檢驗用戶的權限,其底層調用AuthorizingRealm的getAuthorizationIfno(PrincipalCollection principals)方法獲取用戶的權限信息。
5.5 若用戶具有特定的權限則允許訪問資源,否則將跳轉到ShiroFilter配置好的unauthorizedUrl。
5.實現單點登錄整合時需解決的問題
5.1 用戶統一
各個分子系統的用戶都需要統一,即項目A的admin要與項目B的admin是同一個用戶,因為各個CAS Client是通過從CAS Server獲取用戶登錄的用戶名進行分子系統的認證與授權,如果用戶不統一,若Admin在CAS Client1登錄后其訪問CAS Client2,CAS Client2將會使用admin用戶名進行登錄,但是此用戶名可能是別人的。
*另外需要提供給CAS Server進行用戶校驗的用戶表,表中的數據應與各個分子系統的用戶表統一。
5.2 注冊同步
各個分子系統注冊時跳轉到統一的注冊頁面,用戶注冊后需將用戶信息同步到所有分子系統中(通過HTTP方式或分布式服務框架)
5.3 提供單點登錄管理系統
統一管理用戶信息(CRUD)、機構信息(CRUD)、各個分子系統的角色信息、菜單信息、角色菜單關聯信息、為用戶分配各個分子系統的角色。
*各個分子系統的角色信息、菜單信息、角色菜單關聯信息等需與分子系統交互的都使用HTTP的方式或分布式服務框架調取接口實現。
*對於歷史系統的整合可以在分子系統相關表中添加一個字段用於存儲單點登錄管理系統表中的id,避免因為id引起的沖突,在與分子系統進行交互時依賴該字段進行記錄的定位。
5.4 分子系統取消登錄頁面
由於整合單點登錄后需要在CAS Server中統一進行登錄,因此不需要分子系統的登錄頁面,直接暴露歡迎頁給用戶,歡迎頁中提供登錄和注冊的入口,登錄進主頁面后提供進入后台系統的入口。
5.5 單點登錄下的權限控制
若各個分子系統都使用Shiro或其他的安全框架,其依賴sys_user、sys_role、sys_menu、sys_office等表中的數據,因此各個分子系統的權限由其自身安全框架進行管理。
5.6 歷史系統整合單點登錄
若要為歷史分子系統整合成單點登錄,則需要將各個分子系統的用戶進行並集整合成統一的一張用戶表供CAS Server進行校驗( 此表也提供給單點登錄后台系統使用 ),再將新增的用戶分別錄入到各個分子系統的用戶表,但可能已存在A系統的admin與B系統的admin不是同一個人的情況,因此需要使用能代表用戶的唯一標識作為整合標准,可以使用手機號以及郵箱等,那么就需要借助手機號或郵箱進行登錄。
整合后的分子系統其用戶表中的記錄的id字段就與新整合后統一的用戶表的id字段不一致,因此需要在各個分子系統的用戶表中新增一個字段用於存儲統一用戶表的id,當要與分子系統進行交互時需要借助此id進行記錄的定位。
6. 設計方案
6.1 前后端分離的設計方案
前后端分離模式,前端與后台使用Ajax的方式進行交互,當Ajax訪問后台資源時,請求進入Shiro Filter,當用戶未認證訪問了需認證的資源時,Shiro將會把請求重定向到CAS登錄處理,返回302狀態碼,但由於是通過AJax訪問,因此無法處理302狀態碼,無法跳轉到CAS登錄處理,因此可以使用JSONP請求方式進行解決,JSONP請求方式能夠響應瀏覽器返回的302狀態碼,因此當HTTP響應報文的狀態碼是302時,通過JSONP的方式能重定向到指定的地址。
1.提供歡迎頁,當用戶訪問產品域名時直接進入歡迎頁,歡迎頁中提供登錄和注冊的入口,登錄按鈕是跳轉到CAS Server登錄處理的超鏈接,而注冊按鈕是跳轉到統一單點登錄系統的注冊頁面超鏈接。
2.當歡迎頁渲染完畢后,前端需要通過JSONP請求方式,請求后台任意一個需要認證了才能訪問的資源。
3.當JSONP請求,其對應的HTTP響應報文的狀態碼是200時,將頁面跳轉到產品的首頁,在首頁渲染完后通過Ajax訪問getUser接口,用於獲取用戶的身份以及權限信息,通過getUser接口返回的身份和權限信息進行頁面元素的控制。
4.當JSONP請求,其對應的HTTP響應報文的狀態碼非200時(即302),則不跳轉到首頁,用戶訪問產品域名后停留在產品的歡迎頁,其可以通過點擊登錄和注冊按鈕進行相關操作。
5.當用戶進入了首頁,當點擊首頁中任意一個需認證后才能訪問的接口時,前端需要預先通過Ajax訪問getUser接口,若getUser接口能成功返回用戶的身份和權限信息,則允許其進行下一步操作,跳轉到新頁面發送Ajax請求,若getUser接口返回空,則表示用戶已經登錄超時,此時前端需重新通過JSONP請求方式,走一遍CAS完整的登錄處理,當JSONP原訪問接口返回200狀態碼則允許進行下一步操作,否則手動跳轉到CAS Server進行登錄處理。
*在歡迎頁渲染完通過JSONP訪問任意一個需認證的接口,是為了讓用戶能走一遍完整的CAS登錄處理,而且只有該一個請求使用JSONP方式,對於其他數據訪問的請求仍然是使用Ajax。
用戶第一次訪問項目A
用戶再次訪問項目A
用戶第一次訪問項目B
6.2 前后端不分離的設計方案
前后端不分離的模式,則將由后台進行頁面跳轉,在產品域名對應后台首頁跳轉的Controller中需要通過Subject的isAuthenticate()方法判斷當前用戶是否已經登錄,若未登錄則將請求轉發到歡迎頁,否則重定向到首頁中。
1.當用戶訪問產品域名時,請求到達后台頁面跳轉Controller,調用Subject的isAuthenticate()方法判斷當前用戶是否已經登錄,若未登錄則將請求轉發到歡迎頁,否則重定向到首頁。
2.當用戶進入歡迎頁,歡迎頁中內嵌一個<iframe>標簽,其src屬性是任意一個需認證后才能訪問的資源,<iframe>體中reload當前頁面,當頁面解析<iframe>的時候,就會請求src屬性指定的地址。
3.當該請求對應的HTTP響應報文的狀態碼是200時,則表示用戶已經進行登錄,此時將會渲染<iframe>,執行頁面reload,那么將會重新訪問后台首頁跳轉Controller,此時Subject的isAuthenticate()方法返回true,那么請求將會重定向到首頁。
4.當該請求對應的HTTP響應報文的狀態碼非200時(即302),則不會渲染<iframe>,頁面停留在歡迎頁中,用戶可以點擊登錄和注冊進行相關的操作。
5.當用戶進入了首頁,在首頁渲染時將會發送Ajax請求訪問getUser接口,獲取用戶的身份和權限信息進行頁面元素的控制,當用戶在首頁訪問需要認證后才能訪問的接口時,不需要預先調用getUser()接口判斷用戶是否已經過期,直接訪問地址即可,因為是使用前后端不分離的模式,當Shiro判斷當前用戶未進行登錄,則會將請求重定向到CAS Server登錄處理,返回302狀態碼,由於不是使用Ajax,因此能進行處理。
*在歡迎頁中使用<iframe>的src屬性訪問任意一個需要認證后才能訪問的資源,是為了讓用戶能走一遍完整的CAS登錄處理,在此模式中也可以使用JSONP的方式,當JSONP訪問接口返回的狀態碼為200時,手動reload頁面即可。
用戶第一次訪問項目A
用戶再次訪問項目A
用戶第一次訪問項目B