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