cas+shiro+spring 单点登录

原创
2016/04/14 17:31
阅读数 3.1K

最近公司在搞单点登录,之前也做过,用的是58同城的wf框架,基于cas 的原理用拦截器自己写的一套。目前用cas+shiro+springmvc的框架,在网上参照张开涛的跟我学shiro 

http://jinnianshilongnian.iteye.com/blog/2036730  、http://jinnianshilongnian.iteye.com/blog/2047168

cas 原理流程图:如下

当用户第一次访问应用系统的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--ticket;用户再访问别的应用的时候,就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行校验,检查ticket的合法性。如果通过校验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。

下面将一些具体的配置:

cas 的  

deployerConfigContext.xml

配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Licensed to Jasig under one or more contributor license agreements. 
	See the NOTICE file distributed with this work for additional information 
	regarding copyright ownership. Jasig 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 the 
	following location: 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. -->
<!-- | deployerConfigContext.xml centralizes into one file some of the declarative 
	configuration that | all CAS deployers will need to modify. | | This file 
	declares some of the Spring-managed JavaBeans that make up a CAS deployment. 
	| The beans declared in this file are instantiated at context initialization 
	time by the Spring | ContextLoaderListener declared in web.xml. It finds 
	this file because this | file is among those declared in the context parameter 
	"contextConfigLocation". | | By far the most common change you will need 
	to make in this file is to change the last bean | declaration to replace 
	the default authentication handler with | one implementing your approach 
	for authenticating usernames and passwords. + -->

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:c="http://www.springframework.org/schema/c" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:util="http://www.springframework.org/schema/util" xmlns:sec="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd
       http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

	<!-- | The authentication manager defines security policy for authentication 
		by specifying at a minimum | the authentication handlers that will be used 
		to authenticate credential. While the AuthenticationManager | interface supports 
		plugging in another implementation, the default PolicyBasedAuthenticationManager 
		should | be sufficient in most cases. + -->
	<bean id="authenticationManager"
		class="org.jasig.cas.authentication.PolicyBasedAuthenticationManager">
		<constructor-arg>
			<map>
				<!-- | IMPORTANT | Every handler requires a unique name. | If more than 
					one instance of the same handler class is configured, you must explicitly 
					| set its name to something other than its default name (typically the simple 
					class name). -->
				<entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
				<!-- <entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" 
					/> -->
				<entry key-ref="dbAuthHandler" value-ref="primaryPrincipalResolver" />
			</map>
		</constructor-arg>

		<!-- Uncomment the metadata populator to allow clearpass to capture and 
			cache the password This switch effectively will turn on clearpass. <property 
			name="authenticationMetaDataPopulators"> <util:list> <bean class="org.jasig.cas.extension.clearpass.CacheCredentialsMetaDataPopulator" 
			c:credentialCache-ref="encryptedMap" /> </util:list> </property> -->

		<!-- | Defines the security policy around authentication. Some alternative 
			policies that ship with CAS: | | * NotPreventedAuthenticationPolicy - all 
			credential must either pass or fail authentication | * AllAuthenticationPolicy 
			- all presented credential must be authenticated successfully | * RequiredHandlerAuthenticationPolicy 
			- specifies a handler that must authenticate its credential to pass -->
		<property name="authenticationPolicy">
			<bean class="org.jasig.cas.authentication.AnyAuthenticationPolicy" />
		</property>
	</bean>

	<!-- Required for proxy ticket mechanism. -->
	<bean id="proxyAuthenticationHandler"
		class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
		p:httpClient-ref="httpClient" p:requireSecure="false" />

	<!-- | TODO: Replace this component with one suitable for your enviroment. 
		| | This component provides authentication for the kind of credential used 
		in your environment. In most cases | credential is a username/password pair 
		that lives in a system of record like an LDAP directory. | The most common 
		authentication handler beans: | | * org.jasig.cas.authentication.LdapAuthenticationHandler 
		| * org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler | * org.jasig.cas.adaptors.x509.authentication.handler.support.X509CredentialsAuthenticationHandler 
		| * org.jasig.cas.support.spnego.authentication.handler.support.JCIFSSpnegoAuthenticationHandler -->
	<!-- <bean id="primaryAuthenticationHandler" class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler"> 
		<property name="users"> <map> <entry key="casuser" value="Mellon"/> </map> 
		</property> </bean> -->
        <!-- 这里配置验证密码的数据源 -->
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		p:driverClass="com.mysql.jdbc.Driver"
		p:jdbcUrl="jdbc:mysql://192.168.1.230:3306/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull"
		p:user="root" p:password="root" />


	<!-- 密码加密方式 SHA1/MD5 -->
	<bean id="passwordEncoder"
		class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder"
		c:encodingAlgorithm="MD5" p:characterEncoding="UTF-8" />

	<!-- <bean id="dbAuthHandler"
		class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"
		p:dataSource-ref="dataSource"
		p:sql="SELECT passport.password as password FROM passport  where passport.mobile=? and passport.is_del=0"
		p:passwordEncoder-ref="passwordEncoder" /> --><!-- 暂时不使用密码加密 -->
        <!-- 到数据库查询用户密码 还有特定的加密字符串 -->
	<bean id="dbAuthHandler" class="org.jasig.cas.adaptors.jdbc.MultiCriteriaQueryDatabaseAuthenticationHandler" 
		p:dataSource-ref="dataSource"
		p:getPwdSql="select a.`password` as pwd from ss_user a where a.login_name = ? "
		p:getUsernameSql="select a.login_name as loginName from ss_user a where a.login_name = ? "
		p:getSaltSql="select a.salt as salt from ss_user a where a.login_name = ? "
		p:passwordEncoder-ref="passwordEncoder" />

	<!-- Required for proxy ticket mechanism -->
	<bean id="proxyPrincipalResolver"
		class="org.jasig.cas.authentication.principal.BasicPrincipalResolver" />

	<!-- | Resolves a principal from a credential using an attribute repository 
		that is configured to resolve | against a deployer-specific store (e.g. LDAP). -->
	<bean id="primaryPrincipalResolver"
		class="org.jasig.cas.authentication.principal.PersonDirectoryPrincipalResolver">
		<property name="attributeRepository" ref="attributeRepository" />
	</bean>

	<!-- Bean that defines the attributes that a service may return. This example 
		uses the Stub/Mock version. A real implementation may go against a database 
		or LDAP server. The id should remain "attributeRepository" though. + -->
	<bean id="attributeRepository"
		class="org.jasig.services.persondir.support.StubPersonAttributeDao"
		p:backingMap-ref="attrRepoBackingMap" />

	<util:map id="attrRepoBackingMap">
		<entry key="uid" value="uid" />
		<entry key="eduPersonAffiliation" value="eduPersonAffiliation" />
		<entry key="groupMembership" value="groupMembership" />
	</util:map>

	<!-- Sample, in-memory data store for the ServiceRegistry. A real implementation 
		would probably want to replace this with the JPA-backed ServiceRegistry DAO 
		The name of this bean should remain "serviceRegistryDao". + -->
	<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl"
		p:registeredServices-ref="registeredServicesList" />

	<util:list id="registeredServicesList">
		<bean class="org.jasig.cas.services.RegexRegisteredService" p:id="0"
			p:name="HTTP and IMAP" p:description="Allows HTTP(S) and IMAP(S) protocols"
			p:serviceId="^(https?|imaps?)://.*" p:evaluationOrder="10000001" />
		<!-- Use the following definition instead of the above to further restrict 
			access to services within your domain (including sub domains). Note that 
			example.com must be replaced with the domain you wish to permit. This example 
			also demonstrates the configuration of an attribute filter that only allows 
			for attributes whose length is 3. -->
		<!-- <bean class="org.jasig.cas.services.RegexRegisteredService"> <property 
			name="id" value="1" /> <property name="name" value="HTTP and IMAP on example.com" 
			/> <property name="description" value="Allows HTTP(S) and IMAP(S) protocols 
			on example.com" /> <property name="serviceId" value="^(https?|imaps?)://([A-Za-z0-9_-]+\.)*example\.com/.*" 
			/> <property name="evaluationOrder" value="0" /> <property name="attributeFilter"> 
			<bean class="org.jasig.cas.services.support.RegisteredServiceRegexAttributeFilter" 
			c:regex="^\w{3}$" /> </property> </bean> -->
	</util:list>

	<bean id="auditTrailManager"
		class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager" />

	<bean id="healthCheckMonitor" class="org.jasig.cas.monitor.HealthCheckMonitor"
		p:monitors-ref="monitorsList" />

	<util:list id="monitorsList">
		<bean class="org.jasig.cas.monitor.MemoryMonitor"
			p:freeMemoryWarnThreshold="10" />
		<!-- NOTE The following ticket registries support SessionMonitor: * DefaultTicketRegistry 
			* JpaTicketRegistry Remove this monitor if you use an unsupported registry. -->
		<bean class="org.jasig.cas.monitor.SessionMonitor"
			p:ticketRegistry-ref="ticketRegistry"
			p:serviceTicketCountWarnThreshold="5000" p:sessionCountWarnThreshold="100000" />
	</util:list>
</beans>

对应的代码如下:

/**
 * @文件名: MultiCriteriaQueryDatabaseAuthenticationHandler.java
 * @包 org.jasig.cas.adaptors.jdbc
 * @描述: 扩展基于数据库的身份验证
 * @作者:qpxboy@163.com
 * @创建时间 2016年4月1日 上午9:56:19
 * @版本 V1.0
 */
package org.jasig.cas.adaptors.jdbc;

import java.security.GeneralSecurityException;
import java.security.MessageDigest;

import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import javax.validation.constraints.NotNull;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.jasig.cas.authentication.HandlerResult;
import org.jasig.cas.authentication.PreventedException;
import org.jasig.cas.authentication.UsernamePasswordCredential;
import org.jasig.cas.authentication.principal.SimplePrincipal;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;


public class MultiCriteriaQueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {

    @NotNull
    private String getPwdSql;

    @NotNull
    private String getUsernameSql;
    
    @NotNull
    private String getSaltSql;



	@Override
    protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credentials) throws GeneralSecurityException, PreventedException {
        final String id = getPrincipalNameTransformer().transform(credentials.getUsername());
        final String password = credentials.getPassword();

        try {
            String dbPassword = getJdbcTemplate().queryForObject(this.getPwdSql, String.class, new Object[] { id});

            String username = getJdbcTemplate().queryForObject(this.getUsernameSql, String.class, new Object[] { id});
            
            
            String salt = getJdbcTemplate().queryForObject(this.getSaltSql, String.class, new Object[] { id});

            credentials.setUsername(username);
           // credentials.setPassword(dbPassword);
            if (!dbPassword.equals(entryptPassword(password, salt))) {
                throw new FailedLoginException("Password does not match value on record.");
            }
            //            return dbPassword.equals(encryptedPassword);
            //        } catch (final IncorrectResultSizeDataAccessException e) {
            //            // this means the username was not found.
            //            return false;
            //        }
        } catch (final IncorrectResultSizeDataAccessException e) {
            if (e.getActualSize() == 0) {
                throw new AccountNotFoundException(id + " not found with SQL query");
            } else {
                throw new FailedLoginException("Multiple records found for " + id);
            }
        } catch (final DataAccessException e) {
            throw new PreventedException("SQL exception while executing query for " + id, e);
        }
        return createHandlerResult(credentials, new SimplePrincipal(id), null);
    }

    public void setGetPwdSql(final String getPwdSql) {
        this.getPwdSql = getPwdSql;
    }

    public void setGetUsernameSql(final String getUsernameSql) {
        this.getUsernameSql = getUsernameSql;
    }
    
    public void setGetSaltSql(String getSaltSql) {
		this.getSaltSql = getSaltSql;
	}
    
    
    
    
    
	/**
	 * 密码加密,使用salt经过1024次 sha-1 hash
	 * 
	 * @param password
	 * @param salt
	 * @return
	 */
	private static String entryptPassword(final String password, final byte[] salt) {
		byte[] hashPassword = sha1(password.getBytes(), salt, 1024);
		return encodeHex(hashPassword);
	}
    
	private static byte[] sha1(byte[] input, byte[] salt, int iterations) {
		return digest(input, "SHA-1", salt, iterations);
	}
	
	
	/**
	 * 对字符串进行散列, 支持md5与sha1算法.
	 */
	private static byte[] digest(byte[] input, String algorithm, byte[] salt, int iterations) {
		try {
			MessageDigest digest = MessageDigest.getInstance(algorithm);

			if (salt != null) {
				digest.update(salt);
			}

			byte[] result = digest.digest(input);

			for (int i = 1; i < iterations; i++) {
				digest.reset();
				result = digest.digest(result);
			}
			return result;
		} catch (GeneralSecurityException e) {
			e.printStackTrace();
			return null;
		}
	}
	/**
	 * 密码加密,使用salt经过1024次 sha-1 hash
	 * 
	 * @param password
	 * @param salt
	 * @return
	 */
	private  static String entryptPassword(final String password, final String salt) {
		byte[] salts = decodeHex(salt);
		return entryptPassword(password, salts);
	}
	
	/**
	 * Hex编码.
	 */
	private static String encodeHex(byte[] input) {
		return Hex.encodeHexString(input);
	}

	/**
	 * Hex解码.
	 */
	private static byte[] decodeHex(String input) {
		try {
			return Hex.decodeHex(input.toCharArray());
		} catch (DecoderException e) {
			e.printStackTrace();
			return null;
		}
	}
}

另外还有一点需要注意的是

ticketExpirationPolicies.xml 这个文件中 

 <util:constant id="SECONDS" static-field="java.util.concurrent.TimeUnit.SECONDS"/>

    <bean id="serviceTicketExpirationPolicy" class="org.jasig.cas.ticket.support.MultiTimeUseOrTimeoutExpirationPolicy"

          c:numberOfUses="1" c:timeToKill="${st.timeToKillInSeconds:1800}" c:timeUnit-ref="SECONDS"/>

红色表明的部分要改大一点默认是2秒,这个是ticket的过期时间。

在 deployerConfigContext.xml 中:

<util:list id="monitorsList">
<bean class="org.jasig.cas.monitor.MemoryMonitor"
p:freeMemoryWarnThreshold="10" />
<!-- NOTE The following ticket registries support SessionMonitor: * DefaultTicketRegistry 
* JpaTicketRegistry Remove this monitor if you use an unsupported registry. -->
<bean class="org.jasig.cas.monitor.SessionMonitor"
p:ticketRegistry-ref="ticketRegistry"
p:serviceTicketCountWarnThreshold="5000" p:sessionCountWarnThreshold="100000" />
</util:list>


freeMemoryWarnThreshold="10"

是session 的实效时间,之前查询数据库一直报错,原因是设置的太小了,导致数据库没有查询出来结果。session就关闭了。客户端的shiro.xml 配置如下:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
     http://www.springframework.org/schema/beans 
     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://www.springframework.org/schema/context 
     http://www.springframework.org/schema/context/spring-context-3.0.xsd
     http://www.springframework.org/schema/util
     http://www.springframework.org/schema/util/spring-util-3.0.xsd"
default-lazy-init="true">
<!-- 缓存管理器 ehcache 的配置 -->
<!-- <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
</bean> -->
<!-- Realm实现 -->
    <bean id="statelessRealm" class="com.techstar.shiro.realm.StatelessRealm">
        <property name="cachingEnabled" value="false"/>
    </bean>
 <bean id="statelessAuthcFilter" class="com.techstar.shiro.filter.StatelessAuthcFilter"/>
<!--  <bean id="statelessAuthcFilter" class="org.apache.shiro.web.filter.AccessControlFilter"/> -->
<bean id="roleAuthorizationFilter" class="com.techstar.shiro.RoleAuthorizationFilter"/>
<bean id="casRealm" class="com.techstar.security.service.ShiroDbRealm">
<property name="cachingEnabled" value="true" />
<property name="authenticationCachingEnabled" value="true" />
<property name="authenticationCacheName" value="authenticationCache" />
<property name="authorizationCachingEnabled" value="true" />
<property name="authorizationCacheName" value="authorizationCache" />
<!--该地址为cas server地址-->
<property name="casServerUrlPrefix" value="${casServerUrl}:${casServerPort}" />
<!-- 该地址为demo 的访问地址 + 下面配置的cas  filter -->
<property name="casService" value="${casClientUrl}:${casClientPort}/oauth" />
</bean>
<bean id="logoutFilter" class="com.techstar.shiro.filter.LogoutFilter">
<!--该地址为cas server地址-->
<property name="casServerLogoutUrl" value="${casServerUrl}:${casServerPort}/logout" />
        <!-- 返回登录地址,类似http://192.168.1.191:8090/boss/yw-base-consumer/logout.shtml -->
        <property name="redirectUrl" value="${casServerUrl}:${casServerPort}/login?service=${casClientUrl}:${casClientPort}/oauth" />
    </bean>
    <bean id="kickoutSessionControlFilter" class="com.techstar.shiro.filter.KickoutSessionControlFilter">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="kickoutAfter" value="false"/>
        <property name="maxSession" value="2"/>
        <!--该地址为cas server地址-->
<property name="casServerLogoutUrl" value="${casServerUrl}:${casServerPort}/logout" />
        <property name="kickoutUrl" value=""/>
    </bean>
<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!--<property name="loginUrl" value="http://192.168.1.248:8080/offen-cas/login?service=http://192.168.1.191:8090/oauth"/>--> <!--访问demo 时,如果未通过cas认证将会跳转到认证中心,通过得跳转到下面配置的successUrl 里的地址 -->
<property name="loginUrl" value="${casServerUrl}:${casServerPort}/login?service=${casClientUrl}:${casClientPort}/oauth"/><!--访问demo 时,如果未通过cas认证将会跳转到认证中心,通过得跳转到下面配置的successUrl 里的地址 -->
<property name="successUrl" value="${authorityUrl}:${authorityPort}/security/system/getMenuList" />
<property name="filters">
<util:map>
<entry key="authc" value-ref="formAuthenticationFilter" />
<entry key="cas" value-ref="casFilter" />
<entry key="role" value-ref="roleAuthorizationFilter"/>
                <entry key="kickout" value-ref="kickoutSessionControlFilter"/>
<entry key="logout" value-ref="logoutFilter"/>
<entry key="user" value-ref="userFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/casFailure.jsp = anon
/api* = anon
/oauth* = cas
/images/** = anon
/css/** = anon
/js/** = anon
/editor/** = anon
/static/** = anon
/logout** = logout
/favicon.ico = anon
/favicon* = anon
/** = user
<!-- /** = authc,kickout -->
</value>
</property>
</bean>
<!-- 会话ID生成器 -->
<bean id="sessionIdGenerator"
class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" />
<!-- 会话Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid" />
<property name="httpOnly" value="true" />
<property name="maxAge" value="-1" />
</bean>
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe" />
<property name="httpOnly" value="true" />
<property name="maxAge" value="2592000" /><!-- 30天 -->
</bean>
<!-- rememberMe管理器  如需要记住功能 可删掉相关配置-->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)-->
<property name="cipherKey"
value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}" />
<property name="cookie" ref="rememberMeCookie" />
</bean>
<!-- 会话DAO -->
<bean id="sessionDAO"
class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
<property name="activeSessionsCacheName" value="shiro-activeSessionCache" />
<property name="sessionIdGenerator" ref="sessionIdGenerator" />
</bean>
<!-- 会话验证调度器 -->
<!-- <bean id="sessionValidationScheduler" -->
<!-- class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler"> -->
<!-- <property name="sessionValidationInterval" value="900000" /> -->
<!-- <property name="sessionManager" ref="sessionManager" /> -->
<!-- </bean> -->
<!-- 会话管理器 -->
<!-- <bean id="sessionManager"
class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="900000" />
<property name="deleteInvalidSessions" value="true" />
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionValidationScheduler" ref="sessionValidationScheduler" />
<property name="sessionDAO" ref="sessionDAO" />
<property name="sessionIdCookieEnabled" value="true" />
<property name="sessionIdCookie" ref="sessionIdCookie" />
</bean> -->
<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory" />
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
           <list>
             <ref local="casRealm"/>
           </list>
        </property>
<!-- <property name="sessionManager" ref="sessionManager" /> -->
<property name="cacheManager" ref="cacheManager" />
<property name="rememberMeManager" ref="rememberMeManager" />
<property name="subjectFactory" ref="casSubjectFactory" />
</bean>
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<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="casFilter" class="com.techstar.shiro.filter.ShiroCasFilter">
<property name="failureUrl" value="/casFailure.jsp" />
</bean>
 
<bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter" />
 
 <bean id="userFilter" class="com.techstar.modules.shiro.web.filter.authc.UserFilter"></bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
 <!-- shiro redisManager -->
<!-- <bean id="redisManager" class="org.crazycake.shiro.RedisManager"> -->
<!--     <property name="host" value="127.0.0.1"/> -->
<!--     <property name="port" value="6379"/> -->
<!--     <property name="expire" value="1800"/> -->
<!--     optional properties: -->
<!--     <property name="timeout" value="1800"/> -->
<!--     <property name="password" value="L$RGE7NOuDTZ"/> -->
    
<!-- </bean> -->
<!-- redisSessionDAO -->
<!-- <bean id="redisSessionDAO" class="org.crazycake.shiro.RedisSessionDAO">
    <property name="redisManager" ref="redisManager" />
</bean> -->
<!-- sessionManager -->
<!-- <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <property name="sessionDAO" ref="redisSessionDAO" />
</bean> -->
<!-- cacheManager -->
<!-- <bean id="cacheManager" class="org.crazycake.shiro.RedisCacheManager">
    <property name="redisManager" ref="redisManager" />
</bean> -->
<!-- 用户授权信息Cache, 采用EhCache -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:security/ehcache-shiro.xml"/>
</bean>
</beans>

配置文件里面主要的java 代码如下:

/*
 * 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 com.techstar.security.service;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
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.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;
import org.springframework.beans.factory.annotation.Autowired;

import com.techstar.security.entity.Organization;
import com.techstar.security.entity.Permission;
import com.techstar.security.entity.Role;
import com.techstar.security.entity.User;

public class ShiroDbRealm extends CasRealm{

	private static final Logger log = LoggerFactory.getLogger(ShiroDbRealm.class);

	@Autowired
	protected UserService userService;

	/**
	 * 认证回调函数,登录时调用.
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
		CasToken casToken = (CasToken) authcToken;
		if (authcToken == null) {
			return null;
		}
		String ticket = (String) casToken.getCredentials();
		if (StringUtils.isEmpty(ticket)) {
			return null;
		}
		TicketValidator ticketValidator = ensureTicketValidator();
		Assertion casAssertion = null;
		try {
			casAssertion = ticketValidator.validate(ticket, getCasService());

			AttributePrincipal casPrincipal = casAssertion.getPrincipal();
			String userName = casPrincipal.getName();

			User user = userService.findOne("loginName", userName);
			if (user != null) {
				if (user.getStatus().equals("disabled")) {
					throw new DisabledAccountException();
				}
				Map attributes = casPrincipal.getAttributes();
				casToken.setUserId(userName);
				String rememberMeAttributeName = getRememberMeAttributeName();
				String rememberMeStringValue = (String) attributes.get(rememberMeAttributeName);
				boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
				if (isRemembered){
					casToken.setRememberMe(true);
				}
				ShiroUser shiroUser = new ShiroUser(user.getLoginName(), user.getName(), user.getPassword(), user.getId(),user.getOrganizations() == null ? null : user.getOrganizations());
				// List<Object> principals = Arrays.asList(new Object[] { userName, shiroUser });
				
				PrincipalCollection principalCollection = new SimplePrincipalCollection(shiroUser, getName());
				return new SimpleAuthenticationInfo(principalCollection, ticket);
			}
		} catch (TicketValidationException e) {
			throw new CasAuthenticationException((new StringBuilder()).append("Unable to validate ticket [").append(ticket).append("]").toString(),e);
		}
		return null;
	}

	/**
	 * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用.
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
		User user = userService.findOne("loginName", shiroUser.getLoginName());
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		for (Role role : user.getRoles()) {
			// 基于Role的权限信息
			info.addRole(role.getName());
			for (Permission permission : role.getPermissions()) {
				// 基于Permission的权限信息
				info.addStringPermission(permission.getName());
			}
		}
		for (Permission permission : user.getPermissions()) {
			info.addStringPermission(permission.getName());
		}
		// 超级用户可以赋权超级权限
		if (userService.isSupervisor(user)) {
			info.addStringPermission("admin");
		}
		log.info("当前登录用户" + shiroUser.getLoginName() + "所拥有的权限:" + info.getStringPermissions());
		return info;
	}

	/**
	 * 自定义Authentication对象,使得Subject除了携带用户的登录名外还可以携带更多信息.
	 */
	public static class ShiroUser extends com.techstar.modules.shiro.domain.ShiroUser implements Serializable {
		private static final long serialVersionUID = -1373760761780840081L;
		public Set<Organization> organizations;
		private Map<String, Object> cacheMap;

		public ShiroUser(String loginName, String name, String password, String id, Set<Organization> organizations) {
			super(id, loginName, password, name);
			this.organizations = organizations;
		}

		public Map<String, Object> getCacheMap() {
			if (cacheMap == null) {
				cacheMap = new HashMap<String, Object>();
			}
			return cacheMap;
		}

		public Set<Organization> getOrganizations() {
			return this.organizations;
		}

		/**
		 * 本函数输出将作为默认的<shiro:principal/>输出.
		 */
		@Override
		public String toString() {
			return this.getLoginName();
		}

		/**
		 * 重载equals,只计算loginName;
		 */
		@Override
		public int hashCode() {
			return HashCodeBuilder.reflectionHashCode(this, "loginName");
		}

		/**
		 * 重载equals,只比较loginName
		 */
		@Override
		public boolean equals(Object obj) {
			return EqualsBuilder.reflectionEquals(this, obj, "loginName");
		}
	}
}
/*
 * 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 com.techstar.shiro.filter;

import java.io.Serializable;
import java.util.Deque;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.servlet.AdviceFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Simple Filter that, upon receiving a request, will immediately log-out the currently executing
 * {@link #getSubject(ServletRequest, ServletResponse) subject} and then redirect them to a
 * configured {@link #getRedirectUrl() redirectUrl}.
 * 
 * @since 1.2
 */
public class LogoutFilter extends AdviceFilter {

    private static final Logger log = LoggerFactory.getLogger(LogoutFilter.class);

    /**
     * The default redirect URL to where the user will be redirected after logout. The value is {@code "/"}, Shiro's
     * representation of the web application's context root.
     */
    public static final String DEFAULT_REDIRECT_URL = "/";

    /**
     * The URL is Logout address.
     */
    private String casServerLogoutUrl;

//    private Cache<String, Deque<Serializable>> cache;

    /**
     * The URL to where the user will be redirected after logout.
     */
    private String redirectUrl = DEFAULT_REDIRECT_URL;

    /**
     * Acquires the currently executing {@link #getSubject(ServletRequest, ServletResponse)
     * subject}, a potentially Subject or request-specific
     * {@link #getRedirectUrl(ServletRequest, ServletResponse, Subject)
     * redirectUrl}, and redirects the end-user to that redirect url.
     * 
     * @param request the incoming ServletRequest
     * @param response the outgoing ServletResponse
     * @return {@code false} always as typically no further interaction should be done after user logout.
     * @throws Exception if there is any error.
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        String redirectUrl = getRedirectUrl(request, response, subject);
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
//        String basePath = this.getWebBasePathNoPort(req);
        String casServerLogoutUrl = getCasServerLogoutUrl() + "?service=" + redirectUrl;
        //try/catch added for SHIRO-298:
        try {
//            Session session = subject.getSession();
//            String username = (String) subject.getPrincipal();
//            Serializable sessionId = session.getId();
            //TODO 同步控制
            //            Deque<Serializable> deque = cache.get(username);
            //            if (deque != null) {
            //                deque.remove(sessionId);
            //                cache.put(username, deque);
            //            }
            subject.logout();
        } catch (SessionException ise) {
            log.debug("Encountered session exception during logout.  This can generally safely be ignored.", ise);
        }
        //        issueRedirect(request, response, redirectUrl);
        resp.sendRedirect(casServerLogoutUrl);
        return false;
    }

    /**
     * Returns the currently executing {@link Subject}. This implementation merely defaults to calling
     * {@code SecurityUtils.}{@link SecurityUtils#getSubject() getSubject()}, but can be overridden by
     * subclasses for different retrieval strategies.
     * 
     * @param request the incoming Servlet request
     * @param response the outgoing Servlet response
     * @return the currently executing {@link Subject}.
     */
    protected Subject getSubject(ServletRequest request, ServletResponse response) {
        return SecurityUtils.getSubject();
    }

    /**
     * Issues an HTTP redirect to the specified URL after subject logout. This implementation simply calls
     * {@code WebUtils.}
     * {@link WebUtils#issueRedirect(ServletRequest, ServletResponse, String)
     * issueRedirect(request,response,redirectUrl)}.
     * 
     * @param request the incoming Servlet request
     * @param response the outgoing Servlet response
     * @param redirectUrl the URL to where the browser will be redirected immediately after Subject logout.
     * @throws Exception if there is any error.
     */
    protected void issueRedirect(ServletRequest request, ServletResponse response, String redirectUrl) throws Exception {
        WebUtils.issueRedirect(request, response, redirectUrl);
    }

    /**
     * Returns the redirect URL to send the user after logout. This default implementation ignores the arguments and
     * returns the static configured {@link #getRedirectUrl() redirectUrl} property, but this method may be overridden
     * by subclasses to dynamically construct the URL based on the request or subject if necessary.
     * <p/>
     * Note: the Subject is <em>not</em> yet logged out at the time this method is invoked. You may access the Subject's
     * session if one is available and if necessary.
     * <p/>
     * Tip: if you need to access the Subject's session, consider using the {@code Subject.}
     * {@link Subject#getSession(boolean) getSession(false)} method to ensure a new session isn't created unnecessarily.
     * If a session would be created, it will be immediately stopped after logout, not providing any value and
     * unnecessarily taxing session infrastructure/resources.
     * 
     * @param request the incoming Servlet request
     * @param response the outgoing ServletResponse
     * @param subject the not-yet-logged-out currently executing Subject
     * @return the redirect URL to send the user after logout.
     */
    protected String getRedirectUrl(ServletRequest request, ServletResponse response, Subject subject) {
        return getRedirectUrl();
    }

    public String getCasServerLogoutUrl() {
        return casServerLogoutUrl;
    }

    public void setCasServerLogoutUrl(String casServerLogoutUrl) {
        this.casServerLogoutUrl = casServerLogoutUrl;
    }

    /**
     * Returns the URL to where the user will be redirected after logout. Default is the web application's context root,
     * i.e. {@code "/"}
     * 
     * @return the URL to where the user will be redirected after logout.
     */
    public String getRedirectUrl() {
        return redirectUrl;
    }

    /**
     * Sets the URL to where the user will be redirected after logout. Default is the web application's context root,
     * i.e. {@code "/"}
     * 
     * @param redirectUrl the url to where the user will be redirected after logout
     */
    public void setRedirectUrl(String redirectUrl) {
        this.redirectUrl = redirectUrl;
    }

//    public void setCacheManager(CacheManager cacheManager) {
//        this.cache = cacheManager.getCache("shiro-kickout-session");
//    }

    /**
     * 返回不带"/"带端口的网站根路径
     * 
     * @return 不带"/"带端口的网站根路径
     * @author fushihua
     */
    private String getWebBasePath(HttpServletRequest request) {
        String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
        return basePath;
    }

    /**
     * 返回不带"/"不带端口的网站根路径
     * 
     * @return 不带"/"不带端口的网站根路径
     * @author fushihua
     */
    private String getWebBasePathNoPort(HttpServletRequest request) {
        String basePath = request.getScheme() + "://" + request.getServerName() + request.getContextPath();
        return basePath;
    }

}
package com.techstar.shiro.filter;

import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;

import com.techstar.shiro.BaseDomain;

/**
 * <p>
 * User: Zhang Kaitao
 * <p>
 * Date: 14-2-18
 * <p>
 * Version: 1.0
 */
public class KickoutSessionControlFilter extends AccessControlFilter {

    private String kickoutUrl; //踢出后到的地址
    private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; //同一个帐号最大会话数 默认1

    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;
    /**
     * The URL is Logout address.
     */
    private String casServerLogoutUrl;

    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }

    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro-kickout-session");
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            //如果没有登录,直接进行之后的流程
            return true;
        }
        Session session = subject.getSession();
        String username = (String) subject.getPrincipal();
        Serializable sessionId = session.getId();

        //TODO 同步控制
        Deque<Serializable> deque = cache.get(username);
        if (deque == null) {
            deque = new LinkedList<Serializable>();
            cache.put(username, deque);
        }

        //如果队列里没有此sessionId,且用户没有被踢出;放入队列
        if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            deque.push(sessionId);
        }

        //如果队列里的sessionId数超出最大会话数,开始踢人
        while (deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if (kickoutAfter) { //如果踢出后者
                kickoutSessionId = deque.removeFirst();
            } else { //否则踢出前者
                kickoutSessionId = deque.removeLast();
            }
            try {
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
            }
        }

        //如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute("kickout") != null) {
            //会话被踢出了
            try {
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);
            //            WebUtils.issueRedirect(request, response, kickoutUrl);
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;
            String basePath = BaseDomain.getWebBasePathNoPort(req);
            String casServerLogoutUrl = getCasServerLogoutUrl() + "?service=" + basePath + "/" + kickoutUrl;
            resp.sendRedirect(casServerLogoutUrl);
            return false;
        }

        return true;
    }

    public String getCasServerLogoutUrl() {
        return casServerLogoutUrl;
    }

    public void setCasServerLogoutUrl(String casServerLogoutUrl) {
        this.casServerLogoutUrl = casServerLogoutUrl;
    }
}


展开阅读全文
加载中

作者的其它热门文章

打赏
1
24 收藏
分享
打赏
3 评论
24 收藏
1
分享
返回顶部
顶部