cas3.5.2集群化部署及定制开发

集群化方案:

1.tomcat集群共享session
2.持久化票根st及tgt
3.持久化service
4.修改ServiceManager,从内存共享改为redis共享

tomcat集群共享session

之所以要共享session,是因为cas使用了spring-webflow,而webflow使用session存储中间变量,如果不共享session,会直接导致登录流程因为缺少中间变量而失败,具体表现为输入正确用户名密码后,界面刷新重新进入登录界面。

session共享在tomcat中有三种方案可供选择,分别是:1.tomcat原始集群共享。2.redis session持久化共享。3.memcache session持久化共享。

这里我选用了tomcat原始的集群共享,原因是redis session持久化实验失败,session是成功持久化到redis中了,但是登录流程还是失败,memcache由于没有环境,没有试验。

配制tomcat集群

1.在server.xml的Engine元素下加入以下配制

    <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
      <Manager className="org.apache.catalina.ha.session.DeltaManager"
             expireSessionsOnShutdown="false"
             notifyListenersOnReplication="true"/>
      <Channel className="org.apache.catalina.tribes.group.GroupChannel">
        <Membership
            className="org.apache.catalina.tribes.membership.McastService"
            address="228.0.0.4"
            port="45564"
            frequency="500"
            dropTime="3000"
            mcastTTL="1"/>
        <Receiver
            className="org.apache.catalina.tribes.transport.nio.NioReceiver"
            address="xxx"
            port="4001"
            autoBind="0"
            selectorTimeout="100"
            maxThreads="6"/>
        <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
          <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
        </Sender>
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
      </Channel>
      <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
           filter=".*\.gif;.*\.js;.*\.jpg;.*\.htm;.*\.html;.*\.txt;"/>
      <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
    </Cluster>

上面的address=”xxx”,替换成你自己的服务器ip地址

2.在工程web.xml的开头加入配置项

<distributable />

持久化票根

票根的持久化,cas默认就提供了支持,我们所要做的就是把相应的持久化类使用起来,在配置文件中替换掉原来的内存存储类。
1.pom中增加以下依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.18</version>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.6.2</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.5.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>4.1.0.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.1.0.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <scope>runtime</scope>
    <version>4.2.0.Final</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.1</version>
</dependency>

以上依赖包括mysql驱动,hibernate相关包,druid数据连接池及redis驱动(redis用于后面service持久化)

2.applicationContext.xml文件修改
增加以下配置项:

<tx:annotation-driven transaction-manager="transactionManager" />

<context:component-scan base-package="com" />

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
    <property name="driverClassName" value="${jdbc.driver}" />
    <!-- 基本属性 url、user、password -->
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="${jdbc.pool.minIdle}" />
    <property name="minIdle" value="${jdbc.pool.minIdle}" />
    <property name="maxActive" value="${jdbc.pool.maxActive}" />
    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="30000" />
    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="30000" />
    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="90000" />
    <property name="validationQuery" value="SELECT 'x'" />
    <property name="testWhileIdle" value="true" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"    p:dataSource-ref="dataSource" p:jpaVendorAdapter-ref="jpaVendorAdapter">
    <property name="jpaProperties">
        <props>
        <prop key="hibernate.dialect">${database.dialect}</prop>
        <prop key="hibernate.hbm2ddl.auto">update</prop>
        <prop key="hibernate.jdbc.batch_size">${database.batchSize}</prop>
    </props>
    </property>
</bean>

<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" p:generateDdl="true" p:showSql="true" />

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" />

<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

2.ticketRegistry.xml文件修改
查找ticketRegistry,修改原bean声明如下:

<bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.JpaTicketRegistry" />

持久化service

1.deployerConfigContext.xml文件修改
查找serviceRegistryDao,修改bean声明

<bean id="serviceRegistryDao" class="org.jasig.cas.services.JpaServiceRegistryDaoImpl"      p:entityManagerFactory-ref="entityManagerFactory" />

修改ServiceManger

cas自带的DefaultServicesManagerImpl的集群模式,是通过直接将所有的service存在内存的一个set中,然后通过quartz,每两分钟reload一把全量service,这种2分钟同步一次service的集群模式,显然不能正式上线使用,这里我们通过自己实现ServiceManager,采用redis,对所有service进行统一管理。

1.增加RedisServicesManagerImpl类:

public class RedisServicesManagerImpl implements ServicesManager {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final static String SPLIT = ",";

    private final static String REDIS_KEY = "registedService";

    @NotNull
    private ServiceRegistryDao serviceRegistryDao;

    private RegisteredService disabledRegisteredService;

    private RedisTemplate<String, RegisteredService> redisTemplate;

    private RegexRegisteredService defaultService = new RegexRegisteredService();

    public RedisServicesManagerImpl(final ServiceRegistryDao serviceRegistryDao,            final RedisTemplate<String, RegisteredService> redisTemplate) {
        this.serviceRegistryDao = serviceRegistryDao;
        this.disabledRegisteredService = constructDefaultRegisteredService(new ArrayList<String>());
        this.redisTemplate = redisTemplate;

        constructDefaultService();
    }

    @Transactional(readOnly = false)
    @Audit(action = "DELETE_SERVICE", actionResolverName = "DELETE_SERVICE_ACTION_RESOLVER", resourceResolverName = "DELETE_SERVICE_RESOURCE_RESOLVER")
    public synchronized RegisteredService delete(final long id) {
        final RegisteredService r = findServiceBy(id);
        if (r == null) {
            return null;
        }
        log.info("delete service by id..." + r.getServiceId() + "..."
                + r.getId());
        this.serviceRegistryDao.delete(r);
        String key = r.getId() + SPLIT + r.getServiceId();
        // redisTemplate.opsForValue().getOperations().delete(key);
        redisTemplate.opsForHash().delete(REDIS_KEY, key);
        return r;
    }

    public RegisteredService findServiceBy(final Service service) {
        if (service != null) {
            log.info("find service by service..." + service.getId() + "..."
                    + service.getClass());
        }
        Collection<RegisteredService> c = getAllServices();
        if (c.isEmpty()) {
            log.info("find service by service...service is blank");
            return this.disabledRegisteredService;
        }

        for (final RegisteredService r : c) {
            if (r.matches(service)) {
                log.info("find service by service...service is a match...in service..."
                        + service.getId()
                        + "...with redis..."
                        + r.getServiceId());
                return r;
            }
        }
        log.info("find service by service...service not match");
        return null;
    }

    public RegisteredService findServiceBy(final long id) {
        log.info("find service by id..." + id);
        Map<Object, Object> map = redisTemplate.opsForHash().entries(REDIS_KEY);
        Set<Entry<Object, Object>> set = map.entrySet();
        Iterator<Entry<Object, Object>> it = set.iterator();
        while (it.hasNext()) {
            Entry<Object, Object> entry = it.next();
            String key = entry.getKey().toString();
            RegisteredService value = (RegisteredService) entry.getKey();
            log.info("find service by id...service in redis..." + key);
            if (String.valueOf(id).equals(key.split(SPLIT)[0])) {
                log.info("find service by id...match..." + key);
                try {
                    return (RegisteredService) value.clone();
                } catch (final CloneNotSupportedException e) {
                    return value;
                }
            }
        }
        return null;
    }

    public Collection<RegisteredService> getAllServices() {
        log.info("get all services...");
        Set<RegisteredService> services = new TreeSet<RegisteredService>();
        Map<Object, Object> map = redisTemplate.opsForHash().entries(REDIS_KEY);
        Set<Entry<Object, Object>> set = map.entrySet();
        Iterator<Entry<Object, Object>> it = set.iterator();
        while (it.hasNext()) {
            Entry<Object, Object> entry = it.next();

            log.info("get all services...service in redis..." + entry.getKey()
                    + "..." + entry.getValue().getClass());

            String key = entry.getKey().toString();
            if (entry.getValue() instanceof RegisteredService) {
                RegisteredService value = (RegisteredService) entry.getValue();
                log.info("get all services...service in redis..." + key);
                services.add(value);
            }
        }
        if (!services.contains(defaultService)) {
            services.add(defaultService);
        }
        return services;
    }

    public boolean matchesExistingService(final Service service) {
        return findServiceBy(service) != null;
    }

    @Transactional(readOnly = false)
    @Audit(action = "SAVE_SERVICE", actionResolverName = "SAVE_SERVICE_ACTION_RESOLVER", resourceResolverName = "SAVE_SERVICE_RESOURCE_RESOLVER")
    public synchronized RegisteredService save(
            final RegisteredService registeredService) {
        log.info("save service..." + registeredService.getServiceId());
        final RegisteredService r = this.serviceRegistryDao
                .save(registeredService);

        String key = registeredService.getId() + SPLIT
                + registeredService.getServiceId();
        log.info("save service in redis..." + key);
        // redisTemplate.opsForValue().set(key, registeredService);
        redisTemplate.opsForHash().put(REDIS_KEY, key, registeredService);
        return r;
    }

    private RegisteredService constructDefaultRegisteredService(
            final List<String> attributes) {
        final RegisteredServiceImpl r = new RegisteredServiceImpl();
        r.setAllowedToProxy(true);
        r.setAnonymousAccess(false);
        r.setEnabled(true);
        r.setSsoEnabled(true);
        r.setAllowedAttributes(attributes);

        if (attributes == null || attributes.isEmpty()) {
            r.setIgnoreAttributes(true);
        }

        return r;
    }

    private void constructDefaultService() {
        defaultService.setId(0);
        defaultService.setName("HTTP and IMAP");
        defaultService.setDescription("Allows HTTP(S) and IMAP(S) protocols");
        defaultService.setServiceId("^(https?|imaps?)://.*");
        defaultService.setEvaluationOrder(10000001);
    }

}

2.applicationContext.xml文件修改
增加以下配置项:

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxTotal" value="${redis.pool.maxActive}" />
    <property name="maxIdle" value="${redis.pool.maxIdle}" />
    <property name="maxWaitMillis" value="${redis.pool.maxWait}" />
    <property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
</bean>

<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <property name="hostName" value="${redis.hostname}" />
    <property name="port" value="${redis.port}" />
    <property name="password" value="${redis.password}" />
    <property name="poolConfig" ref="jedisPoolConfig" />
</bean>

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    <property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>

修改以下配置项:

<bean id="servicesManager" class="com.wondersgroup.cas.services.RedisServicesManagerImpl">
    <constructor-arg index="0" ref="serviceRegistryDao" />
    <constructor-arg index="1" ref="redisTemplate" />
</bean>

spring-oauth集群负载的cas单点登出问题

背景:

前端有N台由spring-oauth,spring-cas搭建的提供oauth2服务的服务器,后端有单台cas搭建的sso单点登录服务器,通过nginx的iphash保证用户在同一会话工程中始终登录在固定的一台oauth2服务器上。

现象:

cas3.5默认不支持集群环境下的单点登出,导致当用户使用oauth服务时,出现单点故障,具体表现为:
用户A在浏览器上完成整个oauth流程后,此时
1.用户A在单点登录服务器上点击登出按钮
2.系统提示用户登出成功
3.用户B在同一个浏览器上访问oauth服务器,此时没有要求用户B登录,还是用户A的登录信息,并且后续oauth流程报错

原因:

假设oauth服务部署在A,B两台机器上,提供负载访问。SSO单点服务部署在C机器上。
1.用户在C机上登出时,C机器上的SSO服务删除C服务器中的session,并且清空存在用户浏览器中的cookies
2.C服务器中的sso服务通知A,B中部署的oauth服务,用户已经退出,请求oauth服务清空自己的session缓存。
3.此时,由于A,B是负载设置,CAS通知的oauth登出服务,其实只是通知到了A或B中的一台。
4.假设通知到的是A服务器,此时A服务器删除oauth中的session缓存,而B服务器中的oauth session缓存依旧存在
5.用户再次使用oauth服务,此时,由于集群原因,用户可能正好使用到的是B服务器上的oauth服务,由于B服务器中session依旧存在,结果出现单点登出故障。

解决方案:

翻遍了google中所有的讨论,结果毫无进展。
尝试了使用jedis来存储session,结果发现session中存储的AuthorizationRequest类,没有实现序列化接口,无法实体化到redis中,无奈之下,使用了一种监听广播的方式

1.重写SingleSignOutFilter类中的doFilter方法

if (handler.isTokenRequest(request)) {
    handler.recordSession(request);
} else if (handler.isLogoutRequest(request)) {
    String from = request.getParameter("from");
    if (StringUtils.isEmpty(from)) {
            multiCastToDestroy(request);
    }
    handler.destroySession(request);
    // Do not continue up filter chain
    return;
} else {
    log.trace("Ignoring URI " + request.getRequestURI());
}

增加multiCastToDestroy方法

private void multiCastToDestroy(HttpServletRequest request) {
    String logoutMessage = CommonUtils.safeGetParameter(request,"logoutRequest");
    ExecutorService executors = Executors.newFixedThreadPool(100);
    String[] tmps = multicastUrls.split(",");
    for (String tmp : tmps) {
        String[] urls = tmp.split("=");
        String key = urls[0];
        String url = urls[1];
        executors.submit(new MessageSender(url, logoutMessage, ownKey,5000, 5000, true));
    }
}

增加MessageSender内部类

private static final class MessageSender implements Callable<Boolean> {
    private String url;
    private String message;
    private String from;
    private int readTimeout;
    private int connectionTimeout;
    private boolean followRedirects;

    public MessageSender(final String url, final String message,final String from, final int readTimeout,final int connectionTimeout, final boolean followRedirects) {
        this.url = url;
        this.message = message;
        this.from = from;
        this.readTimeout = readTimeout;
        this.connectionTimeout = connectionTimeout;
        this.followRedirects = followRedirects;
    }

    public Boolean call() throws Exception {
        HttpURLConnection connection = null;
        BufferedReader in = null;
        try {
            System.out.println("Attempting to access " + url);
            final URL logoutUrl = new URL(url);
            final String output = "from=" + from + "&logoutRequest="+ URLEncoder.encode(message, "UTF-8");
            connection = (HttpURLConnection) logoutUrl.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.setReadTimeout(this.readTimeout);
            connection.setConnectTimeout(this.connectionTimeout);
            connection.setInstanceFollowRedirects(this.followRedirects);
            connection.setRequestProperty("Content-Length",Integer.toString(output.getBytes().length));
            connection.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            final DataOutputStream printout = new DataOutputStream(connection.getOutputStream());
            printout.writeBytes(output);
            printout.flush();
            printout.close();
            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                while (in.readLine() != null) {
                // nothing to do
            }

            System.out.println("Finished sending message to" + url);
            return true;
        } catch (final SocketTimeoutException e) {
            e.printStackTrace();
            return false;
        } catch (final Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (final IOException e) {
                    // can't do anything
                }
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
}

修改oauth配置文件:
增加以下配置:

<bean id="singleLogoutFilter" class="com.xxx.xxx.cas.filter.SingleSignOutFilter">
    <property name="multicastUrls"      value="127.0.0.1=http://127.0.0.1/api/j_spring_cas_security_check,localhost=http://localhost/api/j_spring_cas_security_check" />
    <property name="ownKey" value="localhost" />
</bean>

这样,当CAS通知到A服务器去做登出操作时,A服务器会广播给其他几台服务器同步去做登出操作,通过广播的方式解决单点登出的故障

spring oauth重复点击授权后报错Cannot approve uninitialized authorization request

现象:

在网络环境特别差等环境下,用户如果重复点击授权按钮,会导致spring报错:
Cannot approve uninitialized authorization request

原因:

AuthorizationEndpoint类中,有一行代码:

 finally {
    sessionStatus.setComplete();
}

sessionStatus标记了spring-security的session生命周期状态,当标记为session生命周期结束后,spring会调用sessionAttributesHandler.cleanupAttributes(request)方法,去清除spring-security上下文请求。对应到oauth的话,这个请求就是放在上下文请求map中的authorizationRequest对象,这个对象封装了authorize接口所用到的请求参数,如appid,redirect_uri,scope等,如果这个对象被从spring-security上下文请求中清掉了,那么整个oauth的流程自然也就断了

解决:

1.限制授权按钮,只能点击一次,这是治本的方法,从源头限制用户多次点击的可能性
2.修改后台,将authorizationRequest手动传到session用户请求上下文中(spring-security请求上下文和用户的session上下文是不同的),再从session中获取authorizationRequest,这是治标的方法,即使用户重复点击了,oauth流程还是能继续执行

model.put("authorizationRequest", authorizationRequest);
HttpSession session = request.getSession();
session.setAttribute("authRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest);
if (authorizationRequest == null) {
    HttpSession session = request.getSession();
    Object obj = session.getAttribute("authRequest");
    if (obj != null) {
        authorizationRequest = (AuthorizationRequest) obj;
       }
}

spring-oauth集成cas单点登录,登陆完成进入授权页面后,按回退按钮进入404页面的问题

背景:

1.项目中使用耶鲁的cas做单点登录。
2.使用spring-oauth包实现oauth2服务
3.使用spring-cas做spring-security及cas的集成

现象:

开发报了个bug,大致流程就是
系统调用/oauth/authorize接口,被spring-security拦截进入cas登录界面后,用户输入用户名密码做登录,登录成功后,浏览器跳转到授权界面,这时候用户点了回退按钮(这个用户好无聊),进入了404的错误界面

原因:

撸了一把spring的源码后发现,spring-security的默认拦截器堆栈中,有个拦截器叫RequestCacheAwareFilter
官方文档解释说这个拦截器是用来还原被登录操作打断的用户界面跳转用的,什么意思呢,就是说:
1.你的系统用spring-security提供的登录方法
2.用户点了你系统的某个链接xxx,这个xxx在spring-security中是个需要被安全控制的连接
3.spring-security拦截这个xxx链接,并且跳到了登录界面
4.用户登录完了后,RequestCacheAwareFilter这个拦截器,再把xxx返回给浏览器

但是,我们这里是集成了cas做登录的,那么,悲剧发生了。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest wrappedSavedRequest =
                requestCache.getMatchingRequest((HttpServletRequest) request, (HttpServletResponse) response);
        chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest, response);
    }

这个filter会去调用requestCache的一个方法,requestCache是spring中用来缓存浏览器request请求的缓存存储。这个方法如下所示:

public HttpServletRequest getMatchingRequest(HttpServletRequest request,
            HttpServletResponse response) {
        DefaultSavedRequest saved = (DefaultSavedRequest) getRequest(request,
                response);
        if (saved == null) {
            return null;
        }
        if (!saved.doesRequestMatch(request, portResolver)) {
            logger.debug("saved request doesn't match");
            return null;
        }
        removeRequest(request, response);
        return new SavedRequestAwareWrapper(saved, request);
    }

有一行叫remveRequest,这行代码会把存在requestCache中的请求路径给清除。而这个请求路径就是用户在按回退按钮时,浏览器要跳转的路径。具体看以下逻辑:

当用户按回退按钮时,其实是回到了spring-cas提供的j_spring_cas_security_check这个url里,最终会到

SavedRequestAwareAuthenticationSuccessHandler

中判断该跳转到哪个页面:

SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }
        String targetUrlParameter = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
            requestCache.removeRequest(request, response);
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }
        clearAuthenticationAttributes(request);
        // Use the DefaultSavedRequest URL
        String targetUrl = savedRequest.getRedirectUrl();
        logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);

可以看到,如果requestCache的请求路径是空,那么就会跳转到默认路径,这个路径如果你没有在spring中配置的话,默认就是你应用访问上下文的跟路径,如果跟路径下没有首页的话,那就自然报404错误了。

解决:

没什么好的解决方式,目前我们只是粗暴的将RequestCache的实现类HttpSessionRequestCache重写了一把,加入一段判断逻辑:

public void removeRequest(HttpServletRequest currentRequest,
            HttpServletResponse response) {
        HttpSession session = currentRequest.getSession(false);
        if (session != null) {
            logger.debug("Removing DefaultSavedRequest from session if present");
            Object obj = session.getAttribute(SAVED_REQUEST);
            if (obj instanceof DefaultSavedRequest) {
                DefaultSavedRequest savedRequest = (DefaultSavedRequest) obj;
                String requestURI = savedRequest.getRequestURI();
                System.out.println("requestCache....requestURI...."
                        + savedRequest.getRequestURI());
                if (requestURI.contains("/api/oauth/")) {
                    return;
                }
            }
            session.removeAttribute(SAVED_REQUEST);
        }
    }