• -------------------------------------------------------------
  • ====================================

CAS-shiro环境下的单点登出

技能 dewbay 5年前 (2019-04-12) 2766次浏览 已收录 0个评论 扫描二维码

本文使用的环境为:

CAS = 4.1.10;
shiro = 1.2.2;
公司产品线较多,需要使用单点登录来贯通各个产品项目,遂采用 CAS + shiro进行单点登录的实现。

完成配置后,发现一个问题:ABC 三个应用在单点登录环境下是可以一次登录各处使用,在 A 登录后 BC 均可直接使用。

但是,卧槽为啥我 A 登出之后 BC 没有跟着登出呢?这不坑么?难不成还要让客户挨个项目点退出去?经理会撕了我的……

查官方文档……发现并没有相关的说明。

好吧,目测shiro提供的shirocas支持包并不全面啊。

查百度,浪费了一天之后,得出结论:靠天靠地不如靠自己。

不要怂,就是刚,抄起键盘就是干!

经过跟踪调查,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());
    /** 请求识别关键字 用来标记请求中票据保存的 key */
    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;
      }
    protected SingleSignOutHandler(){
    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();
      final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName);
      if (log.isDebugEnabled()) {
      log.debug(“Recording session for token ” + token);
      } System.out.println(“记录 token:”+token+” “+”sessionId:”+session.getId());
      storage.addSessionById(token, session);
      }
    /**
    • 从 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) { } } }
      }
    private boolean isMultipartRequest(final HttpServletRequest request) {
    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();
      }
    public void clean(List usernames){ } /**
    • Maps the ID from the CAS server to the Session ID.
      */
      private final Map MANAGED_SESSIONS_ID = new HashMap();
    public synchronized void addSessionById(String mappingId, Session session) {
    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();
    public synchronized void addSessionIdByUserName(String username,
    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;
      }
    public synchronized void removeRelation(String key){
    // 将传入值当做 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 方法进行修改
    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 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); } }
    至此就完成了代码的修改。

之后只需要在 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
版权声明:本文为博主原创文章,转载请附上博文链接!


露水湾 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:CAS-shiro环境下的单点登出
喜欢 (0)
[]
分享 (0)
关于作者:
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址