shiro安全框架擴充套件教程--整合cas框架擴充套件自定義CasRealm
阿新 • • 發佈:2018-12-27
這次我給大家講講如何在shiro中整合cas框架,以及擴充套件自定義的角色和資源體系,囉嗦話不多說了,直接上程式碼說明
第一步,搭建cas伺服器,我也不說拉,這個大家用現有的cas服務就行了
第二步,先加入cas-client的包到我們的專案,然後再下載個shiro-cas.jar也放到專案裡
第三步配置shiro中的cas設定
<description>shiro配置</description> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="shiroCacheManager" /> <property name="sessionManager" ref="sessionManager" /> <property name="realm" ref="casRealm" /> <property name="subjectFactory" ref="casSubjectFactory" /> <!-- <property name="realm" ref="simpleUserRealm" /> --> </bean> <!-- 會話管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="sessionValidationSchedulerEnabled" value="false" /> <property name="sessionDAO" ref="sessionDAO" /> <property name="globalSessionTimeout" value="600000" /> </bean> <!-- 快取管理器 --> <bean id="shiroCacheManager" class="com.silvery.security.shiro.cache.SimpleShiroCacheManager"> <property name="cache" ref="shiroCache" /> </bean> <!-- 快取實現類,注入自定義快取機制 --> <bean id="shiroCache" class="com.silvery.security.shiro.cache.SimpleShiroCache"> <property name="cacheManager" ref="simpleCacheManager" /> </bean> <!-- 會話讀寫實現類 --> <bean id="sessionDAO" class="com.silvery.security.shiro.session.CacheSessionDAO" /> <!-- 使用者認證實現 --> <bean id="simpleUserRealm" class="com.silvery.security.shiro.realm.SimpleUserRealm" /> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <bean id="casFilter" class="org.apache.shiro.cas.CasFilter"> <!-- 配置驗證錯誤時的失敗頁面 --> <property name="failureUrl" value="https://cas.test.com:8443/login?service=http://test.com/mh/cas/login.do" /> </bean> <bean id="casRealm" class="com.silvery.security.shiro.realm.SimpleCasRealm"> <property name="defaultRoles" value="ROLE_USER" /> <property name="casServerUrlPrefix" value="https://cas.test.com:8443" /> <!-- 客戶端的回撥地址設定,必須和下面的過濾器攔截的地址一致 --> <property name="casService" value="http://test.com/mh/cas/login.do" /> </bean> <!-- 如果要實現cas的remember me的功能,需要用到下面這個bean,並設定到securityManager的subjectFactory中 --> <bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory" /> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" /> <property name="arguments" ref="securityManager" /> </bean> <!-- 過濾鏈配置 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="https://cas.test.com:8443/login?service=http://test.com/mh/cas/login.do" /> <property name="filters"> <map> <entry key="cas" value-ref="casFilter" /> <entry key="role"> <bean class="com.silvery.security.shiro.filter.SimpleRoleAuthorizationFilter" /> </entry> <entry key="authc"> <bean class="com.silvery.security.shiro.filter.SimpleFormAuthenticationFilter" /> </entry> <entry key="exec"> <bean class="com.silvery.security.shiro.filter.SimpleExecutiveFilter" /> </entry> </map> </property> </bean> <!-- 許可權資源配置 --> <bean id="filterChainDefinitionsService" class="com.silvery.security.shiro.service.ini.impl.SimpleFilterChainDefinitionsService"> <property name="definitions"> <value> /mh/cas/login.do = cas /mh/casUrl.do = role[ROLE_USER] /static/** = anon /** = exec </value> </property> </bean>
關於這一個步驟的配置裡面註釋寫的比較清楚了,至於一些類是自己重寫的,可以自己參考前面的文章,/mh/cas/login.do其實就是cas攔截器的指定路徑,如果想登入就請求這個路徑即可,如果沒有登入他會跳轉cas的login頁面
第四步就是需要重寫我們的casrealm,你可以看到上面的配置有SimpleCasRealm,這個類是我自己重寫的,是為了方便分配自己本地系統的許可權體系,因為shiro-cas提供的預設CasRealm功能比較有限,不能動態角色體系,下面可以看看這個原始的CasRealm原始碼
public class CasRealm extends AuthorizingRealm { public CasRealm() { validationProtocol = "CAS"; rememberMeAttributeName = "longTermAuthenticationRequestTokenUsed"; setAuthenticationTokenClass(org/apache/shiro/cas/CasToken); } protected void onInit() { super.onInit(); ensureTicketValidator(); } protected TicketValidator ensureTicketValidator() { if(ticketValidator == null) ticketValidator = createTicketValidator(); return ticketValidator; } protected TicketValidator createTicketValidator() { String urlPrefix = getCasServerUrlPrefix(); if("saml".equalsIgnoreCase(getValidationProtocol())) return new Saml11TicketValidator(urlPrefix); else return new Cas20ServiceTicketValidator(urlPrefix); } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { CasToken casToken = (CasToken)token; if(token == null) return null; String ticket = (String)casToken.getCredentials(); if(!StringUtils.hasText(ticket)) return null; TicketValidator ticketValidator = ensureTicketValidator(); try { Assertion casAssertion = ticketValidator.validate(ticket, getCasService()); AttributePrincipal casPrincipal = casAssertion.getPrincipal(); String userId = casPrincipal.getName(); log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[] { ticket, getCasServerUrlPrefix(), userId }); Map attributes = casPrincipal.getAttributes(); casToken.setUserId(userId); String rememberMeAttributeName = getRememberMeAttributeName(); String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName); boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue); if(isRemembered) casToken.setRememberMe(true); List principals = CollectionUtils.asList(new Object[] { userId, attributes }); PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName()); return new SimpleAuthenticationInfo(principalCollection, ticket); } catch(TicketValidationException e) { throw new CasAuthenticationException((new StringBuilder()).append("Unable to validate ticket [").append(ticket).append("]").toString(), e); } } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimplePrincipalCollection principalCollection = (SimplePrincipalCollection)principals; List listPrincipals = principalCollection.asList(); Map attributes = (Map)listPrincipals.get(1); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); addRoles(simpleAuthorizationInfo, split(defaultRoles)); addPermissions(simpleAuthorizationInfo, split(defaultPermissions)); List attributeNames = split(roleAttributeNames); String value; for(Iterator i$ = attributeNames.iterator(); i$.hasNext(); addRoles(simpleAuthorizationInfo, split(value))) { String attributeName = (String)i$.next(); value = (String)attributes.get(attributeName); } attributeNames = split(permissionAttributeNames); String value; for(Iterator i$ = attributeNames.iterator(); i$.hasNext(); addPermissions(simpleAuthorizationInfo, split(value))) { String attributeName = (String)i$.next(); value = (String)attributes.get(attributeName); } return simpleAuthorizationInfo; } private List split(String s) { List list = new ArrayList(); String elements[] = StringUtils.split(s, ','); if(elements != null && elements.length > 0) { String arr$[] = elements; int len$ = arr$.length; for(int i$ = 0; i$ < len$; i$++) { String element = arr$[i$]; if(StringUtils.hasText(element)) list.add(element.trim()); } } return list; } private void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo, List roles) { String role; for(Iterator i$ = roles.iterator(); i$.hasNext(); simpleAuthorizationInfo.addRole(role)) role = (String)i$.next(); } private void addPermissions(SimpleAuthorizationInfo simpleAuthorizationInfo, List permissions) { String permission; for(Iterator i$ = permissions.iterator(); i$.hasNext(); simpleAuthorizationInfo.addStringPermission(permission)) permission = (String)i$.next(); } public String getCasServerUrlPrefix() { return casServerUrlPrefix; } public void setCasServerUrlPrefix(String casServerUrlPrefix) { this.casServerUrlPrefix = casServerUrlPrefix; } public String getCasService() { return casService; } public void setCasService(String casService) { this.casService = casService; } public String getValidationProtocol() { return validationProtocol; } public void setValidationProtocol(String validationProtocol) { this.validationProtocol = validationProtocol; } public String getRememberMeAttributeName() { return rememberMeAttributeName; } public void setRememberMeAttributeName(String rememberMeAttributeName) { this.rememberMeAttributeName = rememberMeAttributeName; } public String getDefaultRoles() { return defaultRoles; } public void setDefaultRoles(String defaultRoles) { this.defaultRoles = defaultRoles; } public String getDefaultPermissions() { return defaultPermissions; } public void setDefaultPermissions(String defaultPermissions) { this.defaultPermissions = defaultPermissions; } public String getRoleAttributeNames() { return roleAttributeNames; } public void setRoleAttributeNames(String roleAttributeNames) { this.roleAttributeNames = roleAttributeNames; } public String getPermissionAttributeNames() { return permissionAttributeNames; } public void setPermissionAttributeNames(String permissionAttributeNames) { this.permissionAttributeNames = permissionAttributeNames; } public static final String DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME = "longTermAuthenticationRequestTokenUsed"; public static final String DEFAULT_VALIDATION_PROTOCOL = "CAS"; private static Logger log = LoggerFactory.getLogger(org/apache/shiro/cas/CasRealm); private String casServerUrlPrefix; private String casService; private String validationProtocol; private String rememberMeAttributeName; private TicketValidator ticketValidator; private String defaultRoles; private String defaultPermissions; private String roleAttributeNames; private String permissionAttributeNames; }
其實跟我們普通用的UserRealm或者是JdbcRealm差別不大,但是裡面增加了casToken的驗證,所以我們應該直接拿過來用,下面再加載出我們的自己的邏輯即可,所以我們可以選擇繼承當前的CasRealm過載一下他的兩個方法
/** * * 擴充套件CAS橋接器,訂製角色體系和資源體系 * * @author shadow * */ public class SimpleCasRealm extends CasRealm { @Autowired private CacheManager cacheManager; private final static Logger log = LoggerFactory.getLogger(SimpleCasRealm.class); public SimpleCasRealm() { super(); setCacheManager(cacheManager); } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { CasToken casToken = (CasToken) token; if (token == null) return null; String ticket = (String) casToken.getCredentials(); if (!StringUtils.hasText(ticket)) return null; TicketValidator ticketValidator = ensureTicketValidator(); try { Assertion casAssertion = ticketValidator.validate(ticket, getCasService()); AttributePrincipal casPrincipal = casAssertion.getPrincipal(); String userId = casPrincipal.getName(); log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[] { ticket, getCasServerUrlPrefix(), userId }); Map attributes = casPrincipal.getAttributes(); casToken.setUserId(userId); String rememberMeAttributeName = getRememberMeAttributeName(); String rememberMeStringValue = (String) attributes.get(rememberMeAttributeName); boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue); if (isRemembered) casToken.setRememberMe(true); List principals = CollectionUtils.asList(new Object[] { userId, attributes }); PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName()); // 這裡可以拿到Cas的登入賬號資訊,載入到對應許可權體系資訊放到快取中... return new SimpleAuthenticationInfo(principalCollection, ticket); } catch (TicketValidationException e) { throw new CasAuthenticationException((new StringBuilder()).append("Unable to validate ticket [") .append(ticket).append("]").toString(), e); } } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals; List listPrincipals = principalCollection.asList(); Map attributes = (Map) listPrincipals.get(1); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 這裡可以載入快取的中的資料到認證實體... addRoles(simpleAuthorizationInfo, split(getDefaultRoles())); return simpleAuthorizationInfo; } protected List split(String s) { List list = new ArrayList(); String elements[] = StringUtils.split(s, ','); if (elements != null && elements.length > 0) { String arr$[] = elements; int len$ = arr$.length; for (int i$ = 0; i$ < len$; i$++) { String element = arr$[i$]; if (StringUtils.hasText(element)) list.add(element.trim()); } } return list; } protected void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo, List roles) { String role; for (Iterator i$ = roles.iterator(); i$.hasNext(); simpleAuthorizationInfo.addRole(role)) role = (String) i$.next(); } protected void addPermissions(SimpleAuthorizationInfo simpleAuthorizationInfo, List permissions) { String permission; for (Iterator i$ = permissions.iterator(); i$.hasNext(); simpleAuthorizationInfo .addStringPermission(permission)) permission = (String) i$.next(); } /** 重寫退出時快取處理方法 */ protected void doClearCache(PrincipalCollection principals) { Object principal = principals.getPrimaryPrincipal(); try { getCache().remove(principal); log.debug(new StringBuffer().append(principal).append(" on logout to remove the cache [").append(principal) .append("]").toString()); } catch (CacheException e) { log.error(e.getMessage()); } } /** 獲取快取管理器的快取堆例項 */ protected Cache<Object, Object> getCache() throws CacheException { return cacheManager.getCache(CacheEmnu.MEMCACHED_DATA_CACHE); } public CacheManager getCacheManager() { return cacheManager; } public void setCacheManager(CacheManager cacheManager) { this.cacheManager = cacheManager; } }
值得提醒大家的一個關鍵點,如何獲取cas返回過來的物件資訊呢?
Subject subject = SecurityUtils.getSubject();
Object principal = subject.getPrincipal();
PrincipalCollection principals = subject.getPrincipals();
第一個物件是可以獲取到當前登入賬號
第二個物件是一個List集合其中0元素是當前登入賬號,1元素是一個map集合,這裡就存放了我們cas服務給我返回的使用者資訊
我們寫的攔截器判斷是否有登入就用第一個Object判斷是否有null即可
第五步既然有登入了,那就必須有退出功能,那如何才能完整退出呢?流程應該是先執行當前系統的登出,然後再執行cas的logout,這樣就比較完整了,不會出現莫名其妙的問題
呼叫當前的shiro的subject.logout();登出當前系統的物件,然後返回到頁面
@RequestMapping("/mh/cas/logout.do")
public ModelAndView casLogout(HttpServletRequest request, HttpServletResponse response, UserDetailsVo vo) {
SimpleUtils.getSubject().logout();
return createModelAndView("/mh/logout");
}
頁面再重定向到cas的logout,這樣就把cas的ticket也登出成功
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>正在登出...</title>
<script type="text/javascript" src="${staticHost}/static/plugin/jquery/core.js"></script>
<script type="text/javascript">
location.href="https://cas.test.com:8443/logout?service=http://test.com/mh/index.do";
</script>
</head>
<body>
</body>
</html>
我想改造大概很明白了,其實shiro-cas.jar已經大部分攔截處理已經幫我們做好了,所以我們很安心地按照以往的方式來操控shiro的登入方式,希望對還沒爬過這個坑的同學有幫助