最近公司在搞單點登陸,以前也作過,用的是58同城的wf框架,基於cas 的原理用攔截器本身寫的一套。目前用cas+shiro+springmvc的框架,在網上參照張開濤的跟我學shiro css
http://jinnianshilongnian.iteye.com/blog/2036730 、http://jinnianshilongnian.iteye.com/blog/2047168 html
cas 原理流程圖:以下java
當用戶第一次訪問應用系統的時候,由於尚未登陸,會被引導到認證系統中進行登陸;根據用戶提供的登陸信息,認證系統進行身份校驗,若是經過校驗,應該返回給用戶一個認證的憑據--ticket;用戶再訪問別的應用的時候,就會將這個ticket帶上,做爲本身認證的憑據,應用系統接受到請求以後會把ticket送到認證系統進行校驗,檢查ticket的合法性。若是經過校驗,用戶就能夠在不用再次登陸的狀況下訪問應用系統2和應用系統3了。mysql
下面將一些具體的配置:git
cas 的 github
deployerConfigContext.xml
配置以下:
web
<?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&characterEncoding=UTF-8&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>
對應的代碼以下:redis
/** * @文件名: 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 這個文件中 spring
<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; } }