cas和shiro集成,很好的解决了登录及权限问题。本人最近第一次使用,框架使用的是jeesite开源框架,本身已经集成了shiro,现在将cas集成到项目中。
折腾了三天,终于把cas集成到jeesite中。现将集成过程写下,供朋友参考。
本项目集成cas的同时还留有登录入口,此时需要多种认证方式,步骤6、7的设置就是针对这个功能的,如不需要可直接跳过。
不做技术好多年了,项目时间紧只能亲自上阵,写的不周全的请多包涵。有问题望指教。
1、添加cas的maven依赖。
<!-- CAS -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.2.1</version>
</dependency>
2、web.xml添加内容。
<!-- 该过滤器对HttpServletRequest请求包装, 可通过HttpServletRequest的getRemoteUser()方法获得登录用户的登录名,可选 -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器使得可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。
比如AssertionHolder.getAssertion().getPrincipal().getName()。
这个类把Assertion信息放在ThreadLocal变量中,这样应用程序不在web层也能够获取到当前登录信息 -->
<filter>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3、配置spring-content-shiro.xml。
整体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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd"
default-lazy-init="true">
<description>Shiro Configuration</description>
<!-- 加载配置属性文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:project.properties" />
<!-- Shiro权限过滤过滤器定义 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/static/** = anon
/userfiles/** = anon
${adminPath}/login = authc
${adminPath}/logout = logout
${adminPath}/cas = cas
${adminPath}/** = user
</value>
</constructor-arg>
</bean>
<bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl"
value="${cas.logout.url}"/>
</bean>
<!-- 安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="${cas.server.url}/login?service=${cas.project.url}${adminPath}/cas" />
<!--<property name="loginUrl" value="${adminPath}/login" /> -->
<property name="successUrl" value="${adminPath}" />
<property name="filters">
<map>
<entry key="cas" value-ref="casFilter"/>
<entry key="authc" value-ref="formAuthenticationFilter"/>
<entry key="logout" value-ref="logout" />
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean>
<!-- cas和shiro结合 -->
<bean id="casRealm" class="com.sinosoft.modules.sys.security.CasLoginRealm">
<property name="casServerUrlPrefix" value="${cas.server.url}"></property>
<property name="casService" value="${cas.project.url}${adminPath}/cas"></property>
</bean>
<!-- CAS认证过滤器 -->
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<property name="failureUrl" value="${adminPath}/login"/>
</bean>
<!-- 定义Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="casRealm"/>
<ref bean="systemAuthorizingRealm"/>
</list>
</property>
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>
<!-- 配置使用自定义认证器,可以实现多Realm认证,并且可以指定特定Realm处理特定类型的验证 -->
<bean id="authenticator" class="com.sinosoft.modules.sys.security.CustomizedModularRealmAuthenticator">
<!-- 配置认证策略,只要有一个Realm认证成功即可,并且返回所有认证成功信息 -->
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>
<!-- 自定义会话管理配置 -->
<bean id="sessionManager" class="com.sinosoft.common.security.shiro.session.SessionManager">
<property name="sessionDAO" ref="sessionDAO"/>
<!-- 会话超时时间,单位:毫秒 -->
<property name="globalSessionTimeout" value="${session.sessionTimeout}"/>
<!-- 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话 -->
<property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
<!-- <property name="sessionValidationSchedulerEnabled" value="false"/> -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
<property name="sessionIdCookieEnabled" value="true"/>
</bean>
<!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="jeesite.session.id"/>
</bean>
<!-- 自定义Session存储容器 -->
<!-- <bean id="sessionDAO" class="com.sinosoft.common.security.shiro.session.JedisSessionDAO"> -->
<!-- <property name="sessionIdGenerator" ref="idGen" /> -->
<!-- <property name="sessionKeyPrefix" value="${redis.keyPrefix}_session_" /> -->
<!-- </bean> -->
<bean id="sessionDAO" class="com.sinosoft.common.security.shiro.session.CacheSessionDAO">
<property name="sessionIdGenerator" ref="idGen" />
<property name="activeSessionsCacheName" value="activeSessionsCache" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>
<!-- 自定义系统缓存管理器-->
<!-- <bean id="shiroCacheManager" class="com.sinosoft.common.security.shiro.cache.JedisCacheManager"> -->
<!-- <property name="cacheKeyPrefix" value="${redis.keyPrefix}_cache_" /> -->
<!-- </bean> -->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- AOP式方法级权限检查 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
4、project.properties中增加属性url。
cas.server.url=http://localhost:8080/cas
cas.project.url=http://1ocalhost:8080/infoService
cas.logout.url=http://localhost:8080/cas/logout?server=http://localhost:8080/infoService
5、创建CasLoginRealm类,继承CasRealm。
package com.sinosoft.modules.sys.security;
import java.util.Collection;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasAuthenticationException;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.session.Session;
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 com.sinosoft.common.config.Global;
import com.sinosoft.common.utils.SpringContextHolder;
import com.sinosoft.common.web.Servlets;
import com.sinosoft.modules.sys.entity.Menu;
import com.sinosoft.modules.sys.entity.Role;
import com.sinosoft.modules.sys.entity.User;
import com.sinosoft.modules.sys.security.SystemAuthorizingRealm.Principal;
import com.sinosoft.modules.sys.service.SystemService;
import com.sinosoft.modules.sys.utils.LogUtils;
import com.sinosoft.modules.sys.utils.UserUtils;
/**
* <p>Title: cas认证类</p>
* <p>Description: cas认证操作类</p>
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: </p>
* <p>Date: 2017-7-5</p>
* @author holyspirit
* @version 1.0
*/
public class CasLoginRealm extends CasRealm {
private Logger logger = LoggerFactory.getLogger(getClass());
private SystemService systemService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// return super.doGetAuthenticationInfo(token);
CasToken casToken = (CasToken) token;
if (token == null) {
return null;
}
// 获取ticket
String ticket = (String) casToken.getCredentials();
if (!org.apache.shiro.util.StringUtils.hasText(ticket)) {
return null;
}
TicketValidator ticketValidator = ensureTicketValidator();
try {
// 回传ticket到服务端验证,验证通过就进入下一行,可以获取登录后的相关信息,否则直接抛异常,即验证不通过
Assertion casAssertion = ticketValidator.validate(ticket,
getCasService());
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String userId = casPrincipal.getName();
User user = getSystemService().getUserByLoginName(userId);
if (user != null) {
Principal p = new Principal(user, false);
PrincipalCollection principalCollection = new SimplePrincipalCollection(
p, getName());
return new SimpleAuthenticationInfo(principalCollection, ticket);
} else {
return null;
}
} catch (TicketValidationException e) {
logger.error("票据认证失败", e);
throw new CasAuthenticationException("Unable to validate ticket ["
+ ticket + "]", e);
}
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
Principal principal = (Principal) getAvailablePrincipal(principals);
// 获取当前已登录的用户
if (!Global.TRUE.equals(Global.getConfig("user.multiAccountLogin"))) {
Collection<Session> sessions = getSystemService().getSessionDao()
.getActiveSessions(true, principal, UserUtils.getSession());
if (sessions.size() > 0) {
// 如果是登录进来的,则踢出已在线用户
if (UserUtils.getSubject().isAuthenticated()) {
for (Session session : sessions) {
getSystemService().getSessionDao().delete(session);
}
}
// 记住我进来的,并且当前用户已登录,则退出当前用户提示信息。
else {
UserUtils.getSubject().logout();
throw new AuthenticationException("msg:账号已在其它地方登录,请重新登录。");
}
}
}
User user = getSystemService().getUserByLoginName(
principal.getLoginName());
if (user != null) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Menu> list = UserUtils.getMenuList();
for (Menu menu : list) {
if (StringUtils.isNotBlank(menu.getPermission())) {
// 添加基于Permission的权限信息
for (String permission : StringUtils.split(
menu.getPermission(), ",")) {
info.addStringPermission(permission);
}
}
}
// 添加用户权限
info.addStringPermission("user");
// 添加用户角色信息
for (Role role : user.getRoleList()) {
info.addRole(role.getEnname());
}
// 更新登录IP和时间
getSystemService().updateUserLoginInfo(user);
// 记录登录日志
LogUtils.saveLog(Servlets.getRequest(), "系统登录");
return info;
} else {
return null;
}
}
/**
*
* Title:获取系统业务对象
* Description:获取系统业务对象
* @param
* @return
* @Author:holyspirit
* @Create Date: 2017-7-6
* @Modifier:
* @Modify Date:
*/
public SystemService getSystemService() {
if (systemService == null) {
systemService = SpringContextHolder.getBean(SystemService.class);
}
return systemService;
}
}
6、FormAuthenticationFilter类增加代码片段,重写redirectToLogin方法。
@Override
protected void redirectToLogin(ServletRequest request,
ServletResponse response) throws IOException {
setLoginUrl("/info/login");
WebUtils.issueRedirect(request, response, getLoginUrl());
}
7、新增CustomizedModularRealmAuthenticator类。
package com.sinosoft.modules.sys.security;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import java.util.Collection;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.realm.Realm;
/**
* <p>Title: 自定义Authenticator</p>
* <p>Description:分别定义处理不同登录方式验证的Realm</p>
* <p>Copyright: Copyright (c) 2017</p>
* <p>Company: </p>
* <p>Date: 2017-7-6</p>
* @author holyspirit
* @version 1.0
*/
public class CustomizedModularRealmAuthenticator extends
ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(
AuthenticationToken authenticationToken)
throws AuthenticationException {
//获取所有Realm
Collection<Realm> realms = getRealms();
Realm realm = null;
//判断当前token是castoken还是UsernamePasswordToken
//强转对应类型token
//根据token类型获得对应的Realm
if(authenticationToken instanceof CasToken){
authenticationToken = (CasToken)authenticationToken;
realm = getRealm(realms, authenticationToken);
}
if(authenticationToken instanceof UsernamePasswordToken){
authenticationToken = (UsernamePasswordToken)authenticationToken;
realm = getRealm(realms, authenticationToken);
}
//执行响应Realm
return doSingleRealmAuthentication(realm, authenticationToken);
}
//判断当前token对应的Realm
public Realm getRealm(Collection<Realm> realms, AuthenticationToken token){
for (Realm realm : realms) {
if(realm.supports(token)){
return realm;
}
}
return null;
}
}
8、设置注销事件,退出cas认证同时清除系统session
<a href="${ctx}/logout" title="退出登录">退出</a>
问题集合:
1。中文乱码问题,所有提交的请求中文变成乱码,这是过滤器顺序所致。cas过滤器和shiro过滤器要放在编码过滤器后面。