Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能: 认证是指用户身份识别,常被称为用户“登录”;授权是指访问控制;密码加密是指保护或隐藏数据防止被偷窥;会话管理是指每用户相关的时间敏感的状态。其他关于Shiro这里就不介绍了,下面本站www.yoodb.com为大家说一说Shiro权限管理框架集成Cas扩展自定义CasRealm类,来完成Cas单点登录权限管理的功能。
搭建CAS服务端此处略过,本文采用cas-client-3.2.1版本为大家讲解,首先下载cas-client客户端项目,将/cas-client/modules/cas-client-core-3.2.1.jar包放到项目应用中, shrio关于CAS单点登录具体配置文件内容如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!-- 权限资源配置 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <!-- 设定用户的登录链接,这里为cas登录页面的链接地址可配置回调地址 --> <property name="loginUrl" value="${shiro.loginUrl}" /> <property name="filters"> <map> <!-- 添加casFilter到shiroFilter --> <entry key="casFilter" value-ref="casFilter" /> <entry key="logoutFilter" value-ref="logoutFilter" /> </map> </property> <property name="filterChainDefinitions"> <value> /shiro-cas = casFilter /logout = logoutFilter /users/** = user </value> </property> </bean> <bean id="casFilter" class="org.apache.shiro.cas.CasFilter"> <!-- 配置验证错误时的失败页面 --> <property name="failureUrl" value="${shiro.failureUrl}" /> <property name="successUrl" value="${shiro.successUrl}" /> </bean> <bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter"> <!-- 配置验证错误时的失败页面 --> <property name="redirectUrl" value="${shiro.logoutUrl}" /> </bean> <bean id="casRealm" class="com.spring.mybatis.realm.UserRealm"> <!-- 认证通过后的默认角色 --> <property name="defaultRoles" value="ROLE_USER" /> <!-- cas服务端地址前缀 --> <property name="casServerUrlPrefix" value="${shiro.cas.serverUrlPrefix}" /> <!-- 应用服务地址,用来接收cas服务端票据 --> <property name="casService" value="${shiro.cas.service}" /> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="sessionManager" ref="sessionManager" /> <property name="subjectFactory" ref="casSubjectFactory"></property> <property name="realm" ref="casRealm" /> </bean> <!-- 如果要实现cas的remember me的功能,需要用到下面这个bean,并设置到securityManager的subjectFactory中 --> <bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"></bean> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager" /> </bean> <!-- 会话管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="globalSessionTimeout" value="3600000" /> <property name="sessionDAO" ref="sessionDAO" /> </bean> <!-- 会话读写实现类 --> <bean id="sessionDAO" class="com.spring.mybatis.redis.RedisSessionDAO"/> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"></property> <property name="arguments" ref="securityManager"></property> </bean> </beans>
上述配置具体含义在注释中本人已经写的很详细,还有什么问题可以留言,此处就不一一介绍了,咱们继续。
关于CAS单点登录需要重写casrealm类,上述配置com.spring.mybatis.realm.UserRealm类,就是我重写的类文件,主要是为了方便分配本地系统权限体系,shiro-cas提供的默认CasRealm功能比较有限,不能满足动态角色体系。shiro-cas版本采用的是1.2.3,maven下载shiro安全框架和cas单点登录相关的jar包pom.xml配置如下:
<!-- apache shiro 相关jar --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-aspectj</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.4.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-cas</artifactId> <version>${shiro.version}</version> </dependency>
下面可以看看原始的CasRealm源码,它是不能满足我们的开发需求功能的,源码具体如下:
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shiro.cas; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.util.CollectionUtils; import org.apache.shiro.util.StringUtils; import org.jasig.cas.client.authentication.AttributePrincipal; import org.jasig.cas.client.validation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * This realm implementation acts as a CAS client to a CAS server for authentication and basic authorization. * <p/> * This realm functions by inspecting a submitted {@link org.apache.shiro.cas.CasToken CasToken} (which essentially * wraps a CAS service ticket) and validates it against the CAS server using a configured CAS * {@link org.jasig.cas.client.validation.TicketValidator TicketValidator}. * <p/> * The {@link #getValidationProtocol() validationProtocol} is {@code CAS} by default, which indicates that a * a {@link org.jasig.cas.client.validation.Cas20ServiceTicketValidator Cas20ServiceTicketValidator} * will be used for ticket validation. You can alternatively set * or {@link org.jasig.cas.client.validation.Saml11TicketValidator Saml11TicketValidator} of CAS client. It is based on * {@link AuthorizingRealm AuthorizingRealm} for both authentication and authorization. User id and attributes are retrieved from the CAS * service ticket validation response during authentication phase. Roles and permissions are computed during authorization phase (according * to the attributes previously retrieved). * * @since 1.2 */ public class CasRealm extends AuthorizingRealm { // default name of the CAS attribute for remember me authentication (CAS 3.4.10+) public static final String DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME = "longTermAuthenticationRequestTokenUsed"; public static final String DEFAULT_VALIDATION_PROTOCOL = "CAS"; private static Logger log = LoggerFactory.getLogger(CasRealm.class); // this is the url of the CAS server (example : http://host:port/cas) private String casServerUrlPrefix; // this is the CAS service url of the application (example : http://host:port/mycontextpath/shiro-cas) private String casService; /* CAS protocol to use for ticket validation : CAS (default) or SAML : - CAS protocol can be used with CAS server version < 3.1 : in this case, no user attributes can be retrieved from the CAS ticket validation response (except if there are some customizations on CAS server side) - SAML protocol can be used with CAS server version >= 3.1 : in this case, user attributes can be extracted from the CAS ticket validation response */ private String validationProtocol = DEFAULT_VALIDATION_PROTOCOL; // default name of the CAS attribute for remember me authentication (CAS 3.4.10+) private String rememberMeAttributeName = DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME; // this class from the CAS client is used to validate a service ticket on CAS server private TicketValidator ticketValidator; // default roles to applied to authenticated user private String defaultRoles; // default permissions to applied to authenticated user private String defaultPermissions; // names of attributes containing roles private String roleAttributeNames; // names of attributes containing permissions private String permissionAttributeNames; public CasRealm() { setAuthenticationTokenClass(CasToken.class); } @Override protected void onInit() { super.onInit(); ensureTicketValidator(); } protected TicketValidator ensureTicketValidator() { if (this.ticketValidator == null) { this.ticketValidator = createTicketValidator(); } return this.ticketValidator; } protected TicketValidator createTicketValidator() { String urlPrefix = getCasServerUrlPrefix(); if ("saml".equalsIgnoreCase(getValidationProtocol())) { return new Saml11TicketValidator(urlPrefix); } return new Cas20ServiceTicketValidator(urlPrefix); } /** * Authenticates a user and retrieves its information. * * @param token the authentication token * @throws AuthenticationException if there is an error during authentication. */ @Override @SuppressWarnings("unchecked") 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 { // contact CAS server to validate service ticket Assertion casAssertion = ticketValidator.validate(ticket, getCasService()); // get principal, user id and attributes 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); } // create simple authentication info 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); } } /** * Retrieves the AuthorizationInfo for the given principals (the CAS previously authenticated user : id + attributes). * * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved. * @return the AuthorizationInfo associated with this principals. */ @Override @SuppressWarnings("unchecked") protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // retrieve user information SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals; List<Object> listPrincipals = principalCollection.asList(); Map<String, String> attributes = (Map<String, String>) listPrincipals.get(1); // create simple authorization info SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // add default roles addRoles(simpleAuthorizationInfo, split(defaultRoles)); // add default permissions addPermissions(simpleAuthorizationInfo, split(defaultPermissions)); // get roles from attributes List<String> attributeNames = split(roleAttributeNames); for (String attributeName : attributeNames) { String value = attributes.get(attributeName); addRoles(simpleAuthorizationInfo, split(value)); } // get permissions from attributes attributeNames = split(permissionAttributeNames); for (String attributeName : attributeNames) { String value = attributes.get(attributeName); addPermissions(simpleAuthorizationInfo, split(value)); } return simpleAuthorizationInfo; } /** * Split a string into a list of not empty and trimmed strings, delimiter is a comma. * * @param s the input string * @return the list of not empty and trimmed strings */ private List<String> split(String s) { List<String> list = new ArrayList<String>(); String[] elements = StringUtils.split(s, ','); if (elements != null && elements.length > 0) { for (String element : elements) { if (StringUtils.hasText(element)) { list.add(element.trim()); } } } return list; } /** * Add roles to the simple authorization info. * * @param simpleAuthorizationInfo * @param roles the list of roles to add */ private void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> roles) { for (String role : roles) { simpleAuthorizationInfo.addRole(role); } } /** * Add permissions to the simple authorization info. * * @param simpleAuthorizationInfo * @param permissions the list of permissions to add */ private void addPermissions(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> permissions) { for (String permission : permissions) { simpleAuthorizationInfo.addStringPermission(permission); } } 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; } }
CasRealm和我们经常使用的UserRealm或JdbcRealm差异并不是很大,只是里边增加了casToken的验证,直接拿过来用再增加自己的逻辑就可以了,继承CasRealm类重载他的两个方法,具体代码如下:
package com.spring.mybatis.realm; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.cas.CasAuthenticationException; import org.apache.shiro.cas.CasRealm; import org.apache.shiro.cas.CasToken; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.util.CollectionUtils; import org.apache.shiro.util.StringUtils; import org.jasig.cas.client.authentication.AttributePrincipal; import org.jasig.cas.client.validation.Assertion; import org.jasig.cas.client.validation.TicketValidationException; import org.jasig.cas.client.validation.TicketValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 授权信息 * 来自 素文宅 * @author www.yoodb.com */ public class UserRealm extends CasRealm { private final Logger log = LoggerFactory.getLogger(getClass()); @Override 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<String, Object> 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<Object> principals = CollectionUtils.asList(userId, attributes); PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName()); //可以获取到Cas登录账号信息,此步将对应权限体系信息放到缓存 return new SimpleAuthenticationInfo(principalCollection, ticket); } catch (TicketValidationException e) { throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e); } } @Override @SuppressWarnings("unchecked") 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(getDefaultRoles())); return simpleAuthorizationInfo; } private List<String> split(String s) { List<String> list = new ArrayList<String>(); String[] elements = StringUtils.split(s, ','); if (elements != null && elements.length > 0) { for (String element : elements) { if (StringUtils.hasText(element)) { list.add(element.trim()); } } } return list; } private void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo,List<String> roles) { for (String role : roles) { simpleAuthorizationInfo.addRole(role); } } }
获取CAS返回的用户信息,具体代码如下:
Subject subject = SecurityUtils.getSubject(); List list = subject.getPrincipals().asList(); String name = (String) list.get(0); Map<String, Object> info = (Map<String, Object>)list.get(1);
通过CAS3.5.2 Server登录后返回更多用户信息,参考资料:http://blog.yoodb.com/yoodb/article/detail/1223,List集合其中0元素是当前登录账号,1元素是一个map集合,存放的是cas Server返回的用户信息。
阿星 (2019/09/17 15:04:36)回复
麻烦楼主分享一份代码。感谢感谢!
路人甲 (2019/09/28 07:07:39)回复
源码我现在没有了,其实我分享的代码就是源码差不多少。