上一篇博文提到的是,单独使用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