公司最近搞了个单点登录的项目,公司用的是jeesite框架,里面自带集成了shiro 自己花了蛮多时间弄这个东西,总算是好了。分享出来希望大家碰到这个坑的时候可以少花点时间。
一、jesstie.properties中添加如下配置
#单点登录CAS设置
shiro.cas.service=http://127.0.0.1:8080/ems_cas/a/cas
shiro.cas.serverUrlPrefix=http\://www.cas.com
shiro.loginUrl=${shiro.cas.serverUrlPrefix}/login?service=${shiro.cas.service}
shiro.logoutUrl=${shiro.cas.serverUrlPrefix}/logout?service=${shiro.cas.service}
二、修改spring-context-shiro.xml
1、shiroFilter里面,修改loginUrl为配置中的登录地址。然后在filters里面添加
<entry key="cas" value-ref="casFilter"/>
2、casFilter中修改登录失败页面地址(我这里默认为登录页面)
<property name="failureUrl" value="${adminPath}/login"/>
3、logoutFilter修改登出页面地址(我这里默认为登录页面)
<property name="redirectUrl" value="${shiro.logoutUrl}"/>
4、securityManager中修改realm为casRealm
<!--应用登陆realm-->
<!--<property name="realm" ref="systemAuthorizingRealm"/>-->
<!--cas登陆realm-->
<property name="realm" ref="casRealm"/>
5、配置casRealm
<bean id="casRealm" class="com.court.modules.sys.security.CasAuthorizingRealm">
<!--该地址为cas server地址 -->
<property name="casServerUrlPrefix" value="${shiro.cas.serverUrlPrefix}"/>
<!-- 该地址为是当前应用 CAS 服务 URL,即用于接收并处理登录成功后的 Ticket 的, 必须和loginUrl中的service参数保持一致,否则服务器会判断service不匹配 -->
<property name="casService" value="${shiro.cas.service}"/>
</bean>
完整配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd"
default-lazy-init="true">
<description>Shiro Configuration</description>
<!-- 加载配置属性文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:courts.properties"/>
<!-- Shiro权限过滤过滤器定义 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/static/** = anon
/userfiles/** = anon
${adminPath}/cas = cas
${adminPath}/login = authc
${adminPath}/logout = logout
${adminPath}/** = user
</value>
</constructor-arg>
</bean>
<!--##需要修改部分 BEGIN #######################################################################-->
<!-- 安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--cas服务登陆-->
<!-- 设定用户的登录链接,这里为cas登录页面的链接地址可配置回调地址 -->
<property name="loginUrl" value="${shiro.loginUrl}"/>
<!--本应用登陆地址-->
<!--<property name="loginUrl" value="${adminPath}/login" />-->
<property name="successUrl" value="${adminPath}"/>
<property name="filters">
<!-- 自定义filters -->
<map>
<entry key="cas" value-ref="casFilter"/>
<entry key="logout" value-ref="logoutFilter"/>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean>
<!-- CAS认证过滤器 -->
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<!-- 配置验证错误时的失败页面 -->
<property name="failureUrl" value="${adminPath}/login"/>
</bean>
<!-- 登出监听 -->
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<!--cas登陆服务跳转urL-->
<property name="redirectUrl" value="${shiro.logoutUrl}"/>
<!--本应用登陆url-->
<!--<property name="redirectUrl" value="${adminPath}/login"></property>-->
</bean>
<!-- 定义Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--本应用登陆realm-->
<!--<property name="realm" ref="systemAuthorizingRealm"/>-->
<!--cas登陆realm-->
<property name="realm" ref="casRealm"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="shiroCacheManager"/>
</bean>
<bean id="casRealm" class="com.court.modules.sys.security.CasAuthorizingRealm">
<!--该地址为cas server地址 -->
<property name="casServerUrlPrefix" value="${shiro.cas.serverUrlPrefix}"/>
<!-- 该地址为是当前应用 CAS 服务 URL,即用于接收并处理登录成功后的 Ticket 的, 必须和loginUrl中的service参数保持一致,否则服务器会判断service不匹配 -->
<property name="casService" value="${shiro.cas.service}"/>
</bean>
<!--##需要修改部分 END #######################################################################-->
<!-- 自定义会话管理配置 -->
<bean id="sessionManager" class="com.court.common.security.shiro.session.SessionManager">
<property name="sessionDAO" ref="sessionDAO"/>
<!-- 会话超时时间,单位:毫秒 -->
<property name="globalSessionTimeout" value="${session.sessionTimeout}"/>
<!-- 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话 -->
<property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
<!--扫描session线程,负责清理超时会话 -->
<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="sid"/>
</bean>
<!-- 自定义Session存储容器 -->
<!-- <bean id="sessionDAO" class="com.court.common.security.shiro.session.JedisSessionDAO"> -->
<!-- <property name="sessionIdGenerator" ref="idGen" /> -->
<!-- <property name="sessionKeyPrefix" value="${redis.keyPrefix}_session_"/> -->
<!-- </bean> -->
<bean id="sessionDAO" class="com.court.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="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!-- 缓存配置 -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:${ehcache.configFile}"/>
</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>
CasAuthorizingRealm.java
package com.jeesite.thinkgem.modules.sys.security;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
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.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.util.CommonUtils;
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.stereotype.Service;
import com.court.common.config.Global;
import com.court.common.utils.SpringContextHolder;
import com.court.common.web.Servlets;
import com.court.modules.sys.entity.Menu;
import com.court.modules.sys.entity.Role;
import com.court.modules.sys.entity.User;
import com.court.modules.sys.security.SystemAuthorizingRealm.Principal;
import com.court.modules.sys.service.SystemService;
import com.court.modules.sys.utils.LogUtils;
import com.court.modules.sys.utils.UserUtils;
/**
* 系统安全认证实现类
* @author ThinkGem
* @version 2014-7-5
*/
@Service
//@DependsOn({"userDao","roleDao","menuDao"})
public class CasAuthorizingRealm extends CasRealm {
private Logger logger = LoggerFactory.getLogger(getClass());
private static final SingleSignOutHandler singleSignOutHandler = new SingleSignOutHandler();
private SystemService systemService;
/**
* 认证回调函数, 登录时调用
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
HttpServletRequest request = Servlets.getRequest();
System.out.println(request.getRemoteUser());
// 单点登录登出句柄(登出时注销session)
if (singleSignOutHandler.isLogoutRequest(request)) {
String logoutRequest = CommonUtils.safeGetParameter(request, "logoutRequest");
singleSignOutHandler.destroySession(request, logoutRequest);
return null;
}
CasToken casToken = (CasToken) token;
if (token == null) {
return null;
}
String ticket = (String)casToken.getCredentials();
if (!org.apache.shiro.util.StringUtils.hasText(ticket)) {
return null;
}
TicketValidator ticketValidator = ensureTicketValidator();
try {
// contact CAS server to validate service ticket
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
// get principal, user id and attributes
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String userId = casPrincipal.getName();
// log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{
// ticket, getCasServerUrlPrefix(), userId
// });
Map<String, Object> attributes = casPrincipal.getAttributes();
// refresh authentication token (user id + remember me)
casToken.setUserId(userId);
String rememberMeAttributeName = getRememberMeAttributeName();
String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
if (isRemembered) {
casToken.setRememberMe(true);
}
// create simple authentication info
// List<Object> principals = CollectionUtils.asList(userId, attributes);
User user = getSystemService().getUserByLoginName(userId);
// PrincipalCollection principalCollection = new SimplePrincipalCollection(new Principal(user, ticket), getName());
// return new SimpleAuthenticationInfo(principalCollection, ticket);
return new SimpleAuthenticationInfo(new Principal(user, ticket), ticket, getName());
// byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
// return new SimpleAuthenticationInfo(new Principal(user, false),
// ticket, ByteSource.Util.bytes(salt), getName());
} catch (TicketValidationException e) {
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
}
/**
* 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Principal principal = (Principal) getAvailablePrincipal(principals);
HttpServletRequest request = Servlets.getRequest();
// 单点登录登出句柄(登录时注入session)
String ticket = principal.getTicket();
singleSignOutHandler.recordSession(request, ticket);
// 获取当前已登录的用户
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;
}
}
/**
* 获取系统业务对象
*/
public SystemService getSystemService() {
if (systemService == null){
systemService = SpringContextHolder.getBean(SystemService.class);
}
return systemService;
}
public static SingleSignOutHandler getSingleSignOutHandler() {
return singleSignOutHandler;
}
/**
* 授权用户信息
*/
public static class Principal implements Serializable {
private static final long serialVersionUID = 1L;
private String id; // 编号
private String loginName; // 登录名
private String name; // 姓名
private boolean mobileLogin; // 是否手机登录
private String ticket;
// private Map<String, Object> cacheMap;
public Principal(User user, boolean mobileLogin) {
this.id = user.getId();
this.loginName = user.getLoginName();
this.name = user.getName();
this.mobileLogin = mobileLogin;
}
public Principal(User user, String ticket) {
this.id = user.getId();
this.loginName = user.getLoginName();
this.name = user.getName();
this.mobileLogin = mobileLogin;
this.ticket = ticket;
}
public String getId() {
return id;
}
public String getLoginName() {
return loginName;
}
public String getName() {
return name;
}
public boolean isMobileLogin() {
return mobileLogin;
}
// @JsonIgnore
// public Map<String, Object> getCacheMap() {
// if (cacheMap==null){
// cacheMap = new HashMap<String, Object>();
// }
// return cacheMap;
// }
public String getTicket() {
return ticket;
}
public void setTicket(String ticket) {
this.ticket = ticket;
}
/**
* 获取SESSIONID
*/
public String getSessionid() {
try{
return (String) UserUtils.getSession().getId();
}catch (Exception e) {
return "";
}
}
@Override
public String toString() {
return id;
}
}
}