注:cas4.0.x+Tomcat7+Jdk7+redis3.0java
CAS中的票據默認是存儲在TicketRegistry中的,如果想要實現CAS服務端的集羣,首先要作的是將票據共享到緩存中。git
1.實現AbstractDistributedTicketRegistry抽象類github
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Collection; import javax.validation.constraints.Min; import org.jasig.cas.ticket.ServiceTicket; import org.jasig.cas.ticket.Ticket; import org.jasig.cas.ticket.TicketGrantingTicket; import org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry; import org.springframework.beans.factory.DisposableBean; import redis.clients.jedis.Jedis; import com.sdzn.cas.util.RedisClient; public class RedisTicketRegistry extends AbstractDistributedTicketRegistry implements DisposableBean { /** * TGT cache entry timeout in seconds. */ @Min(0) private final int tgtTimeout; /** * ST cache entry timeout in seconds. */ @Min(0) private final int stTimeout; /** * Creates a new instance that stores tickets * * @param ticketGrantingTicketTimeOut * TGT timeout in seconds. * @param serviceTicketTimeOut * ST timeout in seconds. */ public RedisTicketRegistry(final int ticketGrantingTicketTimeOut, final int serviceTicketTimeOut) { this.tgtTimeout = ticketGrantingTicketTimeOut; this.stTimeout = serviceTicketTimeOut; } protected void updateTicket(final Ticket ticket) { logger.debug("Updating ticket {}", ticket); try { addTicket(ticket); } catch (final Exception e) { logger.error("Failed updating {}", ticket, e); } } public void addTicket(final Ticket ticket) { logger.debug("Adding ticket {}", ticket); try { Jedis jedis = RedisClient.getJedis(); int seconds = 0; String key = ticket.getId(); if (ticket instanceof TicketGrantingTicket) { // key = ((TicketGrantingTicket) ticket).getAuthentication() // .getPrincipal().getId(); seconds = tgtTimeout; } else { seconds = stTimeout; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(bos); oos.writeObject(ticket); } catch (Exception e) { logger.error("adding ticket to redis error."); } finally { try { if (null != oos) oos.close(); } catch (Exception e) { logger.error("oos closing error when adding ticket to redis."); } } jedis.set(key.getBytes(), bos.toByteArray()); jedis.expire(key.getBytes(), seconds); RedisClient.closeJedis(jedis); } catch (final Exception e) { logger.error("Failed adding {}", ticket, e); } } public boolean deleteTicket(final String ticketId) { logger.debug("Deleting ticket {}", ticketId); try { if (ticketId == null) { return false; } Jedis jedis = RedisClient.getJedis(); jedis.del(ticketId.getBytes()); RedisClient.closeJedis(jedis); return true; } catch (final Exception e) { logger.error("Failed deleting {}", ticketId, e); } return false; } public Ticket getTicket(final String ticketId) { try { final Ticket t = getRawTicket(ticketId); if (t != null) { return getProxiedTicketInstance(t); } } catch (final Exception e) { logger.error("Failed fetching {} ", ticketId, e); } return null; } private Ticket getRawTicket(final String ticketId) { if (null == ticketId) return null; Jedis jedis = RedisClient.getJedis(); Ticket ticket = null; if (jedis.exists(ticketId.getBytes())) { ByteArrayInputStream bais = new ByteArrayInputStream( jedis.get(ticketId.getBytes())); ObjectInputStream ois = null; try { ois = new ObjectInputStream(bais); ticket = (Ticket) ois.readObject(); } catch (Exception e) { logger.error("getting ticket to redis error."); } finally { try { if (null != ois) ois.close(); } catch (Exception e) { logger.error("ois closing error when getting ticket to redis."); } } } RedisClient.closeJedis(jedis); return ticket; } /** * {@inheritDoc} This operation is not supported. * * @throws UnsupportedOperationException * if you try and call this operation. */ @Override public Collection<Ticket> getTickets() { throw new UnsupportedOperationException("GetTickets not supported."); } public void destroy() throws Exception { /** * TODO */ } /** * @param sync * set to true, if updates to registry are to be synchronized * @deprecated As of version 3.5, this operation has no effect since async * writes can cause registry consistency issues. */ @Deprecated public void setSynchronizeUpdatesToRegistry(final boolean sync) { } @Override protected boolean needsCallback() { return true; } private int getTimeout(final Ticket t) { if (t instanceof TicketGrantingTicket) { return this.tgtTimeout; } else if (t instanceof ServiceTicket) { return this.stTimeout; } throw new IllegalArgumentException("Invalid ticket type"); } }
2.修改ticketRegistry.xmlweb
<?xml version="1.0" encoding="UTF-8"?> <!-- Licensed to Jasig under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. Jasig licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at the following location: http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <description> Configuration for the default TicketRegistry which stores the tickets in-memory and cleans them out as specified intervals. </description> <!-- Ticket Registry <bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.DefaultTicketRegistry" />--> <bean id="ticketRegistry" class="com.sdzn.cas.support.RedisTicketRegistry"> <constructor-arg index="0" value="36000" /> <constructor-arg index="1" value="2" /> </bean> <!--Quartz --> <!-- TICKET REGISTRY CLEANER <bean id="ticketRegistryCleaner" class="org.jasig.cas.ticket.registry.support.DefaultTicketRegistryCleaner" p:ticketRegistry-ref="ticketRegistry" p:logoutManager-ref="logoutManager" /> <bean id="jobDetailTicketRegistryCleaner" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean" p:targetObject-ref="ticketRegistryCleaner" p:targetMethod="clean" /> <bean id="triggerJobDetailTicketRegistryCleaner" class="org.springframework.scheduling.quartz.SimpleTriggerBean" p:jobDetail-ref="jobDetailTicketRegistryCleaner" p:startDelay="20000" p:repeatInterval="5000000" />--> </beans>
3.共享tomcat中的session。redis
拷貝編譯打包以後的tomcat-redis-session-manager-VERSION.jar,jedis-2.5.2.jar,commons-pool2-2.2.jar到tomcat/lib目錄下;修改Tomcat context.xml:spring
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve"/> <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" host="redis_server_name" port="6379" database="0" maxInactiveInterval="1800"/>
CAS使用redis集羣后的問題:express
1.1問題描述apache
第一次登錄成功,退出後沒法再次登錄瀏覽器
日誌信息 DEBUG [org.jasig.cas.web.support.CasArgumentExtractor] - <Extractor did not generate service.> DEBUG [org.jasig.cas.web.flow.GenerateLoginTicketAction] - <Generated login ticket LT-16-0WUz0ySwzIrQ1QhfMbFm6Lyl0wK6uP-sso1.***.com>
1.2問題排查緩存
跟蹤訪問流程發現是因爲第一次登出後,會在瀏覽器殘留上一次所使用CASTGC以及SESSIONID,所以致使在登出後訪問時發生重定向不能正確登錄。
1.3解決辦法
修改退出流程,增長對於cookie的清除
import java.util.List; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotNull; import org.jasig.cas.authentication.principal.SimpleWebApplicationServiceImpl; import org.jasig.cas.logout.LogoutRequest; import org.jasig.cas.logout.LogoutRequestStatus; import org.jasig.cas.services.RegisteredService; import org.jasig.cas.services.ServicesManager; import org.jasig.cas.web.flow.AbstractLogoutAction; import org.jasig.cas.web.support.WebUtils; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; public class MyLogoutAction extends AbstractLogoutAction { @NotNull private ServicesManager servicesManager; private boolean followServiceRedirects; protected Event doInternalExecute(HttpServletRequest request, HttpServletResponse response, RequestContext context) throws Exception { boolean needFrontSlo = false; putLogoutIndex(context, 0); List<LogoutRequest> logoutRequests = WebUtils .getLogoutRequests(context); if (logoutRequests != null) { for (LogoutRequest logoutRequest : logoutRequests) { if (logoutRequest.getStatus() == LogoutRequestStatus.NOT_ATTEMPTED) { needFrontSlo = true; break; } } } String service = request.getParameter("service"); if ((this.followServiceRedirects) && (service != null)) { RegisteredService rService = this.servicesManager .findServiceBy(new SimpleWebApplicationServiceImpl(service)); if ((rService != null) && (rService.isEnabled())) { context.getFlowScope().put("logoutRedirectUrl", service); } } /** * delete cookie */ Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { cookie.setMaxAge(0); response.addCookie(cookie); } if (needFrontSlo) { return new Event(this, "front"); } return new Event(this, "finish"); } public void setFollowServiceRedirects(boolean followServiceRedirects) { this.followServiceRedirects = followServiceRedirects; } public void setServicesManager(ServicesManager servicesManager) { this.servicesManager = servicesManager; } }
2.1問題描述
第一次輸入密碼失敗後沒法再次登錄的問題。
2.2問題緣由
webflow中的信息是保存在session中的,可是tomcat-redis-session-manager中對於對象中屬性發生改變或集合增刪內容的狀況是不會同步。github上關於該特性的描述以下:
As noted in the "Overview" section above, in order to prevent colliding writes, the Redis Session Manager only serializes the session object into Redis if the session object has changed (it always updates the expiration separately however.) This dirty tracking marks the session as needing serialization according to the following rules: Calling session.removeAttribute(key) always marks the session as dirty (needing serialization.) Calling session.setAttribute(key, newAttributeValue) marks the session as dirty if any of the following are true: previousAttributeValue == null && newAttributeValue != null previousAttributeValue != null && newAttributeValue == null !newAttributeValue.getClass().isInstance(previousAttributeValue) !newAttributeValue.equals(previousAttributeValue) This feature can have the unintended consequence of hiding writes if you implicitly change a key in the session or if the object's equality does not change even though the key is updated. For example, assuming the session already contains the key "myArray" with an Array instance as its corresponding value, and has been previously serialized, the following code would not cause the session to be serialized again: List myArray = session.getAttribute("myArray"); myArray.add(additionalArrayValue); If your code makes these kind of changes, then the RedisSession provides a mechanism by which you can mark the session as dirty in order to guarantee serialization at the end of the request. For example: List myArray = session.getAttribute("myArray"); myArray.add(additionalArrayValue); session.setAttribute("__changed__"); In order to not cause issues with an application that may already use the key "__changed__", this feature is disabled by default. To enable this feature, simple call the following code in your application's initialization: RedisSession.setManualDirtyTrackingSupportEnabled(true);
大意是若是要實現對象內容的更改,須要同時在session中出發給定的條件。因此這裏修改了Spring webflow源代碼:
package org.springframework.webflow.context.servlet; import java.util.Iterator; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.binding.collection.SharedMap; import org.springframework.binding.collection.StringKeyedMapAdapter; import org.springframework.web.util.WebUtils; import org.springframework.webflow.context.web.HttpSessionMapBindingListener; import org.springframework.webflow.core.collection.AttributeMapBindingListener; import org.springframework.webflow.core.collection.CollectionUtils; public class HttpSessionMap extends StringKeyedMapAdapter implements SharedMap { private static final String REDIS_CHANGE_KEY = "__changed__"; private HttpServletRequest request; public HttpSessionMap(HttpServletRequest request) { this.request = request; } private HttpSession getSession() { return this.request.getSession(false); } protected Object getAttribute(String key) { HttpSession session = getSession(); if (session == null) { return null; } Object value = session.getAttribute(key); if ((value instanceof HttpSessionMapBindingListener)) { return ((HttpSessionMapBindingListener) value).getListener(); } return value; } protected void setAttribute(String key, Object value) { HttpSession session = this.request.getSession(true); if ((value instanceof AttributeMapBindingListener)) { session.setAttribute(key, new HttpSessionMapBindingListener( (AttributeMapBindingListener) value, this)); } else session.setAttribute(key, value); session.setAttribute(REDIS_CHANGE_KEY, ""); } protected void removeAttribute(String key) { HttpSession session = getSession(); if (session != null) session.removeAttribute(key); } protected Iterator getAttributeNames() { HttpSession session = getSession(); return session == null ? CollectionUtils.EMPTY_ITERATOR : CollectionUtils.toIterator(session.getAttributeNames()); } public Object getMutex() { HttpSession session = this.request.getSession(true); Object mutex = session.getAttribute(WebUtils.SESSION_MUTEX_ATTRIBUTE); return mutex != null ? mutex : session; } }
增長了REDIS_CHANGE_KEY ,在每次setAttribute時將其做爲key放入session中。問題得以解決。