这次我给大家讲讲如何在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>
第四步就是需要重写我们的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; }
/** * * 扩展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的登录方式,希望对还没爬过这个坑的同学有帮助