CAS 单用户登录(基于CAS + Shiro整合)

    上一篇博文提到的是,单独使用CAS实现单用户操作功能,但是CAS 和Shiro集成在一起的时候,原来的方法就会失效;所以该博文基于CAS + Shiro 实现单用户操作功能;

    该博文配置环境为:

CAS = 4.1.2;  
shiro = 1.2.2;  

经过跟踪调查,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<String> usernames){  
          
    }  
      
    /** 
     * Maps the ID from the CAS server to the Session ID. 
     */  
    private final Map<String, Serializable> MANAGED_SESSIONS_ID = new HashMap<String, Serializable>();  
  
    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<String, Serializable> USERNAME_SESSIONS_ID = new HashMap<String, Serializable>();  
  
    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<String,String> USERNAME_TOKEN = new HashMap<String, String>();  
    /** 
     * 令牌和用户名对应关系 
     */  
    private final Map<String,String> TOKEN_USERNAME = new HashMap<String, String>();  
      
    /** 
     * 为令牌、用户名、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<br> 
     * 如果索引值为令牌,则为令牌对应sessionID<br> 
     * 否则,则为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. 
 * <p/> 
 * If the accessor is not a known user, then they will be redirected to the {@link #setLoginUrl(String) loginUrl}</p> 
 * 
 * @since 0.9 
 */  
public class UserFilter extends AccessControlFilter {  
  
    /** 
     * Returns <code>true</code> 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 <code>null</code>, <code>false</code> otherwise. 
     * 
     * @return <code>true</code> 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 <code>null</code>, <code>false</code> 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 <code>false</code>, 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);  
    }  
}  
到这里,基于CAS+ Shiro集成 实现单用户操作就完成了,该博文转自https://blog.csdn.net/tian3559060/article/details/80262958

猜你喜欢

转载自blog.csdn.net/qq_34125349/article/details/80274046
Cas
今日推荐