本文使用的环境为:
CAS = 4.1.10;
shiro = 1.2.2;
公司产品线较多,需要使用单点登录来贯通各个产品项目,遂采用 CAS + shiro进行单点登录的实现。
完成配置后,发现一个问题:ABC 三个应用在单点登录环境下是可以一次登录各处使用,在 A 登录后 BC 均可直接使用。
但是,卧槽为啥我 A 登出之后 BC 没有跟着登出呢?这不坑么?难不成还要让客户挨个项目点退出去?经理会撕了我的……
查官方文档……发现并没有相关的说明。
好吧,目测shiro提供的shiro–cas支持包并不全面啊。
查百度,浪费了一天之后,得出结论:靠天靠地不如靠自己。
不要怂,就是刚,抄起键盘就是干!
经过跟踪调查,shiro并没有对单点登出进行支持。也就是说需要完全自己实现。
创建一个 package,在里面创建下列几个类:
单点登出执行类
SingleSignOutHandler
import java.io.Serializable;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.XmlUtils;
/**
- 单点登出执行类
*
*/
public final class SingleSignOutHandler { /**- 强制踢出用户标示符
/ public static final String SESSION_FORCE_BAN_KEY=”BAND”; /* - 用户登出标示符
/ public static final String SESSION_FORCE_LOGOUT_KEY=”LOGOUT”; /* 日志 */
private final Log log = LogFactory.getLog(getClass());
private String artifactParameterName = “ticket”; /** 请求识别关键字 用来标记请求中登出信息的 key */
private String logoutParameterName = “logoutRequest”; /** 强制登出指令名 */
private String banParameterName = “banRequest”; private static HashMapBackedSessionMappingStorage storage = new HashMapBackedSessionMappingStorage(); /**- 获取记录的 token 与 sessionID 对应信息
- @return storage
*/
public static HashMapBackedSessionMappingStorage getSessionMappingStorage(){
return storage;
}
init();
}
/**- @param name Name of the authentication token parameter.
*/
public void setArtifactParameterName(final String name) {
this.artifactParameterName = name;
}
- @param name Name of parameter containing CAS logout request message.
*/
public void setLogoutParameterName(final String name) {
this.logoutParameterName = name;
}
protected String getLogoutParameterName() {
return this.logoutParameterName;
}
- Initializes the component for use.
*/
public void init() {
CommonUtils.assertNotNull(this.artifactParameterName, “artifactParameterName cannot be null.”);
CommonUtils.assertNotNull(this.logoutParameterName, “logoutParameterName cannot be null.”);
}
- 检测是否是一个 token 验证请求
* - @param request HTTP reqest.
* - @return True if request contains authentication token, false otherwise.
*/
public boolean isTokenRequest(final HttpServletRequest request) {
return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName));
}
- 检测是否是一个 CAS 登出通知请求
* - @param request HTTP request.
* - @return True if request is logout request, false otherwise.
*/
public boolean isLogoutRequest(final HttpServletRequest request) {
return “POST”.equals(request.getMethod()) && !isMultipartRequest(request) &&
CommonUtils.isNotBlank(request.getParameter(this.logoutParameterName));
}
- 检测请求是否为强制踢出指令
* - @param request HTTP request.
* - @return True if request is ban request, false otherwise.
*/
public boolean isBanRequest(final HttpServletRequest request) {
return “POST”.equals(request.getMethod()) && !isMultipartRequest(request) &&
CommonUtils.isNotBlank(request.getParameter(this.banParameterName));
}
- 记录请求中的 token 和 sessionID 的映射对
- @param request HTTP request containing an authentication token.
*/
public void recordSession(final HttpServletRequest request) {
Session session = SecurityUtils.getSubject().getSession();
if (log.isDebugEnabled()) {
log.debug(“Recording session for token ” + token);
} System.out.println(“记录 token:”+token+” “+”sessionId:”+session.getId());
storage.addSessionById(token, session);
}- @param request HTTP request containing an authentication token.
- 从 logoutRequest 参数中解析出 token,根据 token 获取到 sessionID,再根据 sessionID 获取到 session,设置 logoutRequest 参数为 true
- 从而标记此 session 已经失效。
* - @param request HTTP request containing a CAS logout message.
*/
public void invalidateSession(final HttpServletRequest request, final SessionManager sessionManager) {
final String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName);
if (log.isTraceEnabled()) {
log.trace (“Logout request:\n” + logoutMessage);
} final String token = XmlUtils.getTextForElement(logoutMessage, “SessionIndex”);
if (CommonUtils.isNotBlank(token)) {
Serializable sessionId = storage.getSessionId(token);
storage.removeRelation(token);
if (sessionId!=null) {
try {
Session session = sessionManager.getSession(new DefaultSessionKey(sessionId));
if(session != null) {
//设置会话的 logoutParameterName 属性表示无效了,这里添加强制踢出用户标示符
session.setAttribute(SESSION_FORCE_LOGOUT_KEY, Boolean.TRUE);
if (log.isDebugEnabled()) {
log.debug (“Invalidating session [” + sessionId + “] for token [” + token + “]”);
}
}
} catch (Exception e) {} }
}
}
- 从 banRequest 参数中解析出 username,根据 username 获取到 sessionID,再根据 sessionID 获取到 session,设置 logoutRequest 参数为 true
- 从而标记此 session 已经失效。
* - @param request HTTP request containing a Ban message.
*/
public void invalidateSessionByBan(final HttpServletRequest request, final SessionManager sessionManager) {
final String banMessage = request.getParameter(this.banParameterName);
if (log.isTraceEnabled()) {
log.trace (“Ban request:\n” + banMessage);
} final String username = XmlUtils.getTextForElement(banMessage, “SessionIndex”);
if (CommonUtils.isNotBlank(username)) {
Serializable sessionId = storage.getSessionId(username);
storage.removeRelation(username);
if (sessionId!=null) {
try {
Session session = sessionManager.getSession(new DefaultSessionKey(sessionId));
if(session != null) {
//设置会话的 logoutParameterName 属性表示无效了,这里添加强制踢出用户标示符
session.setAttribute(SESSION_FORCE_BAN_KEY, Boolean.TRUE);
session.setAttribute(SESSION_FORCE_LOGOUT_KEY, Boolean.TRUE);
if (log.isDebugEnabled()) {
log.debug (“Invalidating session [” + sessionId + “] for user [” + username + “]”);
}
}
} catch (Exception e) {} }
}
}
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith(“multipart”);
}
}
存储 ticket 到 sessionID、用户名的映射
HashMapBackedSessionMappingStorage
import org.apache.shiro.session.Session;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map; - 强制踢出用户标示符
/**
- 存储 ticket 到 sessionID 的映射
*/
public final class HashMapBackedSessionMappingStorage { /**- 获取当前缓存的对应关系数量
- @return
*/
public int size(){
return MANAGED_SESSIONS_ID.size();
}
- Maps the ID from the CAS server to the Session ID.
*/
private final Map MANAGED_SESSIONS_ID = new HashMap();
MANAGED_SESSIONS_ID.put(mappingId, session.getId());
} public synchronized Serializable getSessionIDByMappingId(String mappingId) {
return MANAGED_SESSIONS_ID.get(mappingId);
} public synchronized void removeSession(String mappingId) {
MANAGED_SESSIONS_ID.remove(mappingId);
} /**- 记录用户名和 sessionID 的关系
*/
private final Map USERNAME_SESSIONS_ID = new HashMap();
Session session) {
USERNAME_SESSIONS_ID.put(username, session.getId());
} public synchronized Serializable getSessionIDByUserName(String username) {
return USERNAME_SESSIONS_ID.get(username);
} public synchronized void removeUser(String username) {
USERNAME_SESSIONS_ID.remove(username);
} /**- 用户名和令牌对应关系
/ private final Map USERNAME_TOKEN = new HashMap(); /* - 令牌和用户名对应关系
*/
private final Map TOKEN_USERNAME = new HashMap();
- 为令牌、用户名、sessionID 添加对应关系
- @param token 令牌
- @param username 用户名
- @param session session(获取 sessionId 用)
*/
public synchronized void addUserNameTokenSessionId(String token,String username,Session session){ removeRelation(username); addSessionById(token, session);
addSessionIdByUserName(username, session);
USERNAME_TOKEN.put(username, token);
TOKEN_USERNAME.put(token, username);
- 通过索引值获取 sessionID
- @param key 索引值
- @return 如果索引值为用户名,则为用户名对应 sessionID
- 如果索引值为令牌,则为令牌对应 sessionID
- 否则,则为 null
*/
public synchronized Serializable getSessionId(String key){
// 当传入索引为用户名时
if(USERNAME_SESSIONS_ID.containsKey(key)){
return USERNAME_SESSIONS_ID.get(key);
}else if(MANAGED_SESSIONS_ID.containsKey(key)){
return MANAGED_SESSIONS_ID.get(key);
}
return null;
}
// 将传入值当做 username 用于获取 Token
String token = USERNAME_TOKEN.get(key);
// 如果获取不到,则说明传入值为 Token
if(token == null){
token = key;
}
// 用 Token 获取 username
String username = TOKEN_USERNAME.get(token);
// 如果没能获取到 username 则可判定为异常情况:session 没有被存档
if(username == null){
// 退出
return;
}USERNAME_TOKEN.remove(username); USERNAME_SESSIONS_ID.remove(username); TOKEN_USERNAME.remove(token); MANAGED_SESSIONS_ID.remove(token);
}
}
单点登出过滤器
CasLogoutFilter
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.servlet.AdviceFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CasLogoutFilter extends AdviceFilter{
private static final Logger log = LoggerFactory.getLogger(CasLogoutFilter.class);
private static final SingleSignOutHandler HANDLER = new SingleSignOutHandler();
private SessionManager sessionManager;
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
/**
* 如果请求中包含了 ticket 参数,记录 ticket 和 sessionID 的映射
* 如果请求中包含 logoutRequest 参数,标记 session 为无效
* 如果 session 不为空,且被标记为无效,则登出
*
* @param request the incoming ServletRequest
* @param response the outgoing ServletResponse
* @return 是 logoutRequest 请求返回 false,否则返回 true
* @throws Exception if there is any error.
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest)request;
if (HANDLER.isTokenRequest((HttpServletRequest)req)) {
//通过浏览器发送的请求,链接中含有 token 参数,记录 token 和 sessionID
//由于还未登录完成,此时进行记录只能记录 token 和 sessionID,无法记录用户名,废弃,改至 CasRealm
// HANDLER.recordSession(req);
return true;
} else if (HANDLER.isLogoutRequest(req)) {
//cas服务器发送的请求,链接中含有 logoutRequest 参数,在之前记录的 session 中设置 logoutRequest 参数为 true
//因为 Subject 是和线程是绑定的,所以无法获取登录的 Subject 直接 logout
HANDLER.invalidateSession(req,sessionManager);
log.warn(“收到登出指令” + req.getRequestURI());
// 登出后认证链无需继续
return false;
} else if (HANDLER.isBanRequest(req)) {
//系统管理服务器发送的请求,链接中含有 banRequest 参数,在之前记录的 session 中设置 logoutRequest 参数为 true
//因为 Subject 是和线程是绑定的,所以无法获取登录的 Subject 直接 logout
HANDLER.invalidateSessionByBan(req,sessionManager);
// 踢出后认证链无需继续
return false;
} else {
log.trace(“Ignoring URI ” + req.getRequestURI());
}
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession(false);
if (session!=null&&session.getAttribute(HANDLER.getLogoutParameterName())!=null) {
try {
subject.logout();
} catch (SessionException ise) {
log.debug(“Encountered session exception during logout. This can generally safely be ignored.”, ise);
}
}
return true;
}
}
之后,根据需要决定是修改还是复写下面两个类:
用户登录状态检测过滤器
org.apache.shiro.web.filter.authc.UserFilter
import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
/**
- Filter that allows access to resources if the accessor is a known user, which is defined as
- having a known principal. This means that any user who is authenticated or remembered via a
- ‘remember me’ feature will be allowed access from this filter.
- If the accessor is not a known user, then they will be redirected to the {@link #setLoginUrl(String) loginUrl}
* - @since 0.9
*/
public class UserFilter extends AccessControlFilter { /**- Returns
true
if the request is a - {@link #isLoginRequest(javax.servlet.ServletRequest, javax.servlet.ServletResponse) loginRequest} or
- if the current {@link #getSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) subject}
- is not
null
,false
otherwise.
* - @return
true
if the request is a - {@link #isLoginRequest(javax.servlet.ServletRequest, javax.servlet.ServletResponse) loginRequest} or
- if the current {@link #getSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) subject}
- is not
null
,false
otherwise.
*/
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginRequest(request, response)) {
return true;
} else { Subject subject = getSubject(request, response); // 修改开始 // 如果令牌不存在,拒绝使用 if(subject.getPrincipal() == null){ return false; } // 确认 session 中是否有失效标记,有则使其立即失效,同时拒绝使用 Session session = subject.getSession(); Boolean isFLK=(Boolean)session.getAttribute(SingleSignOutHandler.SESSION_FORCE_LOGOUT_KEY); if(isFLK!=null&&isFLK){ // 重新获取登录信息 Boolean isBAN=(Boolean)session.getAttribute(SingleSignOutHandler.SESSION_FORCE_BAN_KEY); subject.logout();if(isBAN!=null&&isBAN){ try { // 强制登出 WebUtils.issueRedirect(request, response, “/logout”);return true; } catch (IOException e) { e.printStackTrace(); }
}else{ return false; } return false;} return true; // 修改结束
}
}
- This default implementation simply calls
- {@link #saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse) saveRequestAndRedirectToLogin}
- and then immediately returns
false
, thereby preventing the chain from continuing so the redirect may - execute.
*/
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 屏蔽登录超时后转回登录画面,重登录后画面显示不正确的问题
saveRequestAndRedirectToLogin(request, response);
// redirectToLogin(request, response);
return false;
}
}
shiro-cas的登录验证器
org.apache.shiro.cas.CasRealm
找到 doGetAuthenticationInfo 方法进行修改
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 userId = casPrincipal.getName(); log.debug(“Validate ticket : {} in CAS server : {} to retrieve user : {}”, new Object[]{ ticket, getCasServerUrlPrefix(), userId });SingleSignOutHandler.getSessionMappingStorage().addUserNameTokenSessionId(ticket, userId.trim(), session); 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); PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName()); return new SimpleAuthenticationInfo(principalCollection, ticket);
} catch (TicketValidationException e) { throw new CasAuthenticationException(“Unable to validate ticket [” + ticket + “]”, e); } }
至此就完成了代码的修改。 - Returns
之后只需要在 spring 配置文件中找到 bean shiroFilter
<!-- 添加 -->
<bean id="casLogoutFilter" class="你自己的 package.CasLogoutFilter">
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- Shiro 的 Web 过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="${casUrl}/login?service=${localUrl}/cas"/>
<property name="successUrl" value="/"/>
<property name="filters">
<util:map>
<entry key="cas" value-ref="casFilter"/>
<entry key="logout" value-ref="logoutFilter"/>
<entry key="casLogout" value-ref="casLogoutFilter" /><!-- 添加 -->
<entry key="user" value-ref="userFilter" /> <!-- 添加 -->
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/casFailure.jsp = anon
/User/getByIds = anon
/cas = casLogout,cas <!-- 修改 -->
/logout = logout
/** = user
</value>
</property>
</bean>
如此即可完成对 CAS 登出通知的响应,实现单点登出。
作者:tian3559060
来源:CSDN
原文:https://blog.csdn.net/tian3559060/article/details/80262958
版权声明:本文为博主原创文章,转载请附上博文链接!