第三部分:shiro整合spring使用cas單點登入配置
第三部分 shiro整合spring使用cas單點登入配置
(一)shiro單點登入
配置的主要目的在於將登入頁面改為${cas.server}?service=${cas.client}/login
的形式,service後面為本地的回撥地址。在cas伺服器端登入成功後,會生成ticket返回給客戶端,客戶端的shiro使用ticket最為憑據儲存起來。
shiro配置單點登陸後,在登出時原始的cas-client只能刪除HttpSession,不能刪除shiro的Session,因此未使用shiro的session管理器。
如果想啟用shiro的Session管理器,可以參考
原有的CasRealm在AuthenticationInfo中只儲存了使用者名稱作為principal,MyCasRealm中重寫了此方法,改為儲存使用者資訊類。
需要在pom增加依賴,shiro-cas會通過依賴傳遞自動增加cas-client-core-3.2.1的依賴。
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId >
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.2.3</version>
</dependency>
1、spring-shiro.xml配置
<?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:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
" default-lazy-init="false">
<description>Shiro安全配置</description>
<!-- 快取管理器 -->
<bean id="cacheManager" class="com.whty.framework.base.common.cache.SpringCacheManagerWrapper">
<property name="cacheManager" ref="springCacheManager"/>
</bean>
<!-- Realm實現 -->
<bean id="casRealm" class="com.whty.oim.base.shiro.MyCasRealm">
<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="${cas.server}"/>
<!-- 客戶端的回撥地址設定,必須和下面的shiro-cas過濾器攔截的地址一致 -->
<property name="casService" value="${cas.client}/login"/>
</bean>
<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"/>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="casRealm"/>
<!-- <property name="sessionManager" ref="sessionManager"/> -->
<property name="cacheManager" ref="cacheManager"/>
<!-- sessionMode引數設定為native時,那麼shrio就將使用者的基本認證資訊儲存到預設名稱為shiro-activeSessionCache 的Cache中 -->
<!--<property name="sessionMode" value="native" />-->
<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="org.apache.shiro.cas.CasFilter">
<!-- 配置驗證錯誤時的失敗頁面 -->
<property name="failureUrl" value="${cas.client}"/>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- 設定角色的登入連結,這裡為cas登入頁面的連結可配置回撥地址 -->
<property name="loginUrl" value="${cas.server}?service=${cas.client}/login" />
<property name="successUrl" value="/index" />
<property name="unauthorizedUrl" value="/"/>
<property name="filters">
<util:map>
<!-- <entry key="authc" value-ref="authcFilter"/>
<entry key="captchaFilter" value-ref="captchaFilter"/> -->
<!-- 新增casFilter到shiroFilter -->
<entry key="cas" value-ref="casFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/logout = logout
/login = cas
/** = user
</value>
</property>
</bean>
<!--保證實現了Shiro內部lifecycle函式的bean執行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>
2、cas.properties
3、自定義Realm實現
主要修改了CasRealm的doGetAuthenticationInfo()
方法,CasRealm預設只儲存了username,本系統改為儲存ShiroUser物件。
public class MyCasRealm extends CasRealm {
Logger logger = LoggerFactory.getLogger(MyCasRealm.class);
@Autowired
private UcsUserService ucsUserService;
@Autowired
private UcsRoleService ucsRoleService;
@Autowired
private UcsPermissionService ucsPermissionService;
@Value("${domain}")
private String domain;
/**
* 授權查詢回撥函式, 進行鑑權但快取中無使用者的授權資訊時呼叫.
*
* @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
UcsUser ucsUser = ucsUserService.selectByUsername(shiroUser.loginName);
//把principals放session中 key=userId value=principals
SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(ucsUser.getId()),SecurityUtils.getSubject().getPrincipals());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//賦予角色
List<UcsRole> ucsRoles = ucsRoleService.selectByUser(ucsUser.getId());
if (!CheckEmptyUtil.isEmpty(ucsRoles)){
for(UcsRole ucsRole:ucsRoles){
info.addRole(ucsRole.getId());
}
}
//賦予許可權
// List<UcsPermission> ucsPermissions = ucsPermissionService.selectByUser(ucsUser.getId(), domain);
if (!CheckEmptyUtil.isEmpty(shiroUser.getMenus())){
for(EasyUIMenu permission:shiroUser.getMenus()){
if(UcsBaseConstant.PermissionType.BUTTON.equals(permission.getType()))
info.addStringPermission(permission.getUrl());
}
}
return info;
}
/**
* Authenticates a user and retrieves its information.
*
* @param token the authentication token
* @throws AuthenticationException if there is an error during authentication.
*/
@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 {
// contact CAS server to validate service ticket
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
// get principal, user id and attributes
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String username = casPrincipal.getName();
logger.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{
ticket, getCasServerUrlPrefix(), username
});
Map<String, Object> attributes = casPrincipal.getAttributes();
// refresh authentication token (user id + remember me)
casToken.setUserId(username);
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
// 根據使用者名稱獲取賬號資訊
UcsUser ucsUser = ucsUserService.selectByUsername(username);
List<UcsPermission> ucsPermissions = null;
if (ucsUser != null) {
ucsPermissions = ucsPermissionService.selectByUser(ucsUser.getId(), domain);
} else {
throw new UnknownAccountException();//登入失敗
}
//給選單排序
List<EasyUIMenu> menus = toEasyUIMenu(ucsPermissions);
if(!CheckEmptyUtil.isEmpty(menus)){
Collections.sort(menus);
}
ShiroUser shiroUser = new ShiroUser(ucsUser.getId(), ucsUser.getUsername(), ucsUser.getName(), menus);
return new SimpleAuthenticationInfo(shiroUser, ticket, getName());
} catch (TicketValidationException e) {
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
}
private List<EasyUIMenu> toEasyUIMenu(List<UcsPermission> ucsPermissions){
List<EasyUIMenu> menus = new ArrayList<EasyUIMenu>(0);
for (UcsPermission ucsPermission : ucsPermissions) {
menus.add(toEasyUIMenu(ucsPermission));
}
return menus;
}
private EasyUIMenu toEasyUIMenu(UcsPermission ucsPermission){
return new EasyUIMenu(ucsPermission.getId(), ucsPermission.getPid(), ucsPermission.getDomain(), ucsPermission.getName(), ucsPermission.getType(), ucsPermission.getSort(), ucsPermission.getIcon(), ucsPermission.getUrl(), ucsPermission.getDescription(), ucsPermission.getStatus());
}
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
clearAllCache();
}
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
(二)cas登出
在任意一個子系統登出後,cas伺服器會向所有發過登入請求的子系統傳送一個登出請求,使所有子系統的HttpSession失效。如果使用了shiro的session管理器,需要修改cas客戶端程式碼,在移除HttpSession的時候也移除shiro的Session。
1、web.xml
web.xml中需要加入cas的SingleSignOutFilter實現單點登出功能,該過濾器需要放在shiroFilter之前,spring字符集過濾器之後。在實際使用時發現,SingleSignOutFilter如果放在了spring字符集過濾器之前,資料在傳輸過程中就會出現亂碼。
<!-- 用於單點退出,該過濾器用於實現單點登出功能,可選配置。-->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 該過濾器用於實現單點登出功能,可選配置。 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2、登出請求
登出只需要將當前頁面跳轉到cas伺服器的logout頁面即可實現登出。如果需要登出後跳轉頁面,可以加入service引數,並參考第一部分中第四節進行配置。
子系統使用的方法是在後臺拼裝好登出連結,傳送給前臺頁面,前臺頁面接受使用el表示式接收。具體實現需要根據每個子系統的不同來自行實現。
@Value("${cas.server}")
private String cas_server;
@Value("${cas.client}")
private String cas_client;
@RequestMapping(value = "/index")
public String index(Model model){
StringBuilder logoutUrl = new StringBuilder();
logoutUrl.append(cas_server);
logoutUrl.append("/logout?service=");
logoutUrl.append(cas_client);
model.addAttribute("logoutUrl", logoutUrl.toString());
return "system/index";
}
前臺頁面:
<script>
var logoutUrl = '${logoutUrl}';
</script>
快取配置(可選)
我使用的ehcache作為shiro的快取,也可以使用其他的。
spring-cache.xml
<?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:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"
default-lazy-init="false">
<bean id="springCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcacheManager"/>
</bean>
<!--ehcache-->
<bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:properties/ehcache.xml"/>
</bean>
</beans>
ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="true" monitoring="autodetect"
dynamicConfig="true">
<diskStore path="C:\\ehcache\\cache"/>
<!--<diskStore path="/application/cache"/> -->
<!-- 登入記錄快取 鎖定10分鐘 -->
<cache name="passwordRetryEhcache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authorizationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authenticationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000"
diskSpoolBufferSizeMB="30"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
statistics="false"
/>
</ehcache>