CAS学习(二)不使用Cookie实现前后端分离情况下的CAS单点登录

上一篇已经介绍了将CAS6.2编译为服务,放在Tomcat中运行
地址:https://blog.csdn.net/u011943534/article/details/118437235

按照CAS官方的流程,需要借用Cookie实现TGC的传递,这里实验不使用Cookie实现了单点登录

第一个服务的单点登录流程如下:
在这里插入图片描述
第一个服务已登录,第二个服务无需输入用户名、密码单点登录流程:

在这里插入图片描述

1、将上一章的CAS服务添加配置
已经配置好的在百度云盘中:
链接: https://pan.baidu.com/s/13ynO1JuezmdrO-KF7-JmkQ 提取码: 425c
主要改动如下:
application.properties的配置文件添加配置:

#自动扫描服务配置,默认开启
#cas.serviceRegistry.watcherEnabled=true

#120秒扫描一遍
cas.serviceRegistry.schedule.repeatInterval=120000

#延迟15秒开启
# cas.serviceRegistry.schedule.startDelay=15000

# Ticket过期设置
cas.ticket.st.numberOfUses=1
cas.ticket.st.timeToKillInSeconds=60

services下添加几个服务
在这里插入图片描述
这里只有HTTPSXXXX、NEWFRAME-10000006和NEWFRAME2-10000004有用,其他的都是测试的。
文件名要和内容里的name和id对上,serviceId对应的是后台服务的IP端口

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "^(http)://127.0.0.1:7903.*",
  "name" : "NEWFRAME2",
  "id" : 10000004,
  "description" : "This service definition authorizes all application urls that support HTTP protocols.",
  "evaluationOrder" : 10000004
}

2、修改CAS rest接口跨域设置
经过测试org.apereo.cas:cas-server-support-rest中的接口都不能跨域,需要改下源码,源码已经改好了,将云盘内的ServiceTicketResource.classTicketGrantingTicketResource.class替换cas\WEB-INF\lib\cas-server-support-rest-6.3.2.jar中的,使用winrar打开jar包就可以替换。
当然还一种方式自己编译也可以,下面是对应的改造后的两个java文件

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apereo.cas.support.rest.resources;

import java.util.List;
import javax.servlet.http.HttpServletRequest;
import lombok.Generated;
import org.apache.commons.lang3.BooleanUtils;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.AuthenticationCredentialsThreadLocalBinder;
import org.apereo.cas.authentication.AuthenticationException;
import org.apereo.cas.authentication.AuthenticationResult;
import org.apereo.cas.authentication.AuthenticationSystemSupport;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.DefaultAuthenticationResultBuilder;
import org.apereo.cas.authentication.principal.WebApplicationService;
import org.apereo.cas.rest.BadRestRequestException;
import org.apereo.cas.rest.factory.RestHttpRequestCredentialFactory;
import org.apereo.cas.rest.factory.ServiceTicketResourceEntityResponseFactory;
import org.apereo.cas.ticket.InvalidTicketException;
import org.apereo.cas.ticket.registry.TicketRegistrySupport;
import org.apereo.cas.util.LoggingUtils;
import org.apereo.cas.web.support.ArgumentExtractor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;

@RestController("serviceTicketResourceRestController")
@CrossOrigin(allowCredentials="true")
public class ServiceTicketResource {
    @Generated
    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceTicketResource.class);
    private final AuthenticationSystemSupport authenticationSystemSupport;
    private final TicketRegistrySupport ticketRegistrySupport;
    private final ArgumentExtractor argumentExtractor;
    private final ServiceTicketResourceEntityResponseFactory serviceTicketResourceEntityResponseFactory;
    private final RestHttpRequestCredentialFactory credentialFactory;
    private final ApplicationContext applicationContext;

    @PostMapping(
            value = {"/v1/tickets/{tgtId:.+}"},
            consumes = {"application/x-www-form-urlencoded"}
    )
    public ResponseEntity<String> createServiceTicket(final HttpServletRequest httpServletRequest, @RequestBody(required = false) final MultiValueMap<String, String> requestBody, @PathVariable("tgtId") final String tgtId) {
        ResponseEntity var5;
        try {
            Authentication authn = this.ticketRegistrySupport.getAuthenticationFrom(tgtId);
            AuthenticationCredentialsThreadLocalBinder.bindCurrent(authn);
            if (authn == null) {
                throw new InvalidTicketException(tgtId);
            }

            WebApplicationService service = this.argumentExtractor.extractService(httpServletRequest);
            if (service == null) {
                throw new IllegalArgumentException("Target service/application is unspecified or unrecognized in the request");
            }

            AuthenticationResult authenticationResult;
            ResponseEntity var8;
            if (!BooleanUtils.toBoolean(httpServletRequest.getParameter("renew"))) {
                DefaultAuthenticationResultBuilder builder = new DefaultAuthenticationResultBuilder();
                authenticationResult = builder.collect(authn).build(this.authenticationSystemSupport.getPrincipalElectionStrategy(), service);
                var8 = this.serviceTicketResourceEntityResponseFactory.build(tgtId, service, authenticationResult);
                return var8;
            }

            List<Credential> credential = this.credentialFactory.fromRequest(httpServletRequest, requestBody);
            if (credential == null || credential.isEmpty()) {
                throw new BadRestRequestException("No credentials are provided or extracted to authenticate the REST request");
            }

            authenticationResult = this.authenticationSystemSupport.handleAndFinalizeSingleAuthenticationTransaction(service, credential);
            var8 = this.serviceTicketResourceEntityResponseFactory.build(tgtId, service, authenticationResult);
            return var8;
        } catch (InvalidTicketException var15) {
            var5 = new ResponseEntity(tgtId + " could not be found or is considered invalid", HttpStatus.NOT_FOUND);
            return var5;
        } catch (AuthenticationException var16) {
            var5 = RestResourceUtils.createResponseEntityForAuthnFailure(var16, httpServletRequest, this.applicationContext);
        } catch (BadRestRequestException var17) {
            LoggingUtils.error(LOGGER, var17);
            var5 = new ResponseEntity(var17.getMessage(), HttpStatus.BAD_REQUEST);
            return var5;
        } catch (Exception var18) {
            LoggingUtils.error(LOGGER, var18);
            var5 = new ResponseEntity(var18.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
            return var5;
        } finally {
            AuthenticationCredentialsThreadLocalBinder.clear();
        }

        return var5;
    }

    @Generated
    public ServiceTicketResource(final AuthenticationSystemSupport authenticationSystemSupport, final TicketRegistrySupport ticketRegistrySupport, final ArgumentExtractor argumentExtractor, final ServiceTicketResourceEntityResponseFactory serviceTicketResourceEntityResponseFactory, final RestHttpRequestCredentialFactory credentialFactory, final ApplicationContext applicationContext) {
        this.authenticationSystemSupport = authenticationSystemSupport;
        this.ticketRegistrySupport = ticketRegistrySupport;
        this.argumentExtractor = argumentExtractor;
        this.serviceTicketResourceEntityResponseFactory = serviceTicketResourceEntityResponseFactory;
        this.credentialFactory = credentialFactory;
        this.applicationContext = applicationContext;
    }
}

package org.apereo.cas.support.rest.resources;

import org.apereo.cas.CentralAuthenticationService;
import org.apereo.cas.authentication.AuthenticationException;
import org.apereo.cas.authentication.AuthenticationSystemSupport;
import org.apereo.cas.authentication.principal.ServiceFactory;
import org.apereo.cas.rest.BadRestRequestException;
import org.apereo.cas.rest.factory.RestHttpRequestCredentialFactory;
import org.apereo.cas.rest.factory.TicketGrantingTicketResourceEntityResponseFactory;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.util.LoggingUtils;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 * {@link RestController} implementation of CAS' REST API.
 * <p>
 * This class implements main CAS RESTful resource for vending/deleting TGTs and vending STs:
 * </p>
 * <ul>
 * <li>{@code POST /v1/tickets}</li>
 * <li>{@code POST /v1/tickets/{TGT-id}}</li>
 * <li>{@code GET /v1/tickets/{TGT-id}}</li>
 * <li>{@code DELETE /v1/tickets/{TGT-id}}</li>
 * </ul>
 *
 * @author Dmitriy Kopylenko
 * @since 4.1.0
 */
@RestController("ticketGrantingTicketResource")
@Slf4j
@RequiredArgsConstructor
@CrossOrigin(allowCredentials="true")
public class TicketGrantingTicketResource {

    private final AuthenticationSystemSupport authenticationSystemSupport;

    private final RestHttpRequestCredentialFactory credentialFactory;

    private final CentralAuthenticationService centralAuthenticationService;

    private final ServiceFactory serviceFactory;

    private final TicketGrantingTicketResourceEntityResponseFactory ticketGrantingTicketResourceEntityResponseFactory;

    private final ApplicationContext applicationContext;

    /**
     * Reject get response.
     *
     * @return the response entity
     */
    @GetMapping(RestProtocolConstants.ENDPOINT_TICKETS)
    public ResponseEntity<String> rejectGetResponse() {
        return new ResponseEntity<>(HttpStatus.METHOD_NOT_ALLOWED);
    }

    /**
     * Create new ticket granting ticket.
     *
     * @param requestBody username and password application/x-www-form-urlencoded values
     * @param request     raw HttpServletRequest used to call this method
     * @return ResponseEntity representing RESTful response
     */
    @PostMapping(value = RestProtocolConstants.ENDPOINT_TICKETS, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ResponseEntity<String> createTicketGrantingTicket(@RequestBody(required = false) final MultiValueMap<String, String> requestBody,
                                                             final HttpServletRequest request) {
        try {
            val tgtId = createTicketGrantingTicketForRequest(requestBody, request);
            return createResponseEntityForTicket(request, tgtId);
        } catch (final AuthenticationException e) {
            return RestResourceUtils.createResponseEntityForAuthnFailure(e, request, applicationContext);
        } catch (final BadRestRequestException e) {
//            LoggingUtils.error(LOGGER, e);
            return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
        } catch (final Exception e) {
//            LoggingUtils.error(LOGGER, e);
            return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * Destroy ticket granting ticket.
     *
     * @param tgtId ticket granting ticket id URI path param
     * @return {@link ResponseEntity} representing RESTful response. Signals
     * {@link HttpStatus#OK} when successful.
     */
    @DeleteMapping(value = RestProtocolConstants.ENDPOINT_TICKETS + "/{tgtId:.+}")
    public ResponseEntity<String> deleteTicketGrantingTicket(@PathVariable("tgtId") final String tgtId) {
        this.centralAuthenticationService.deleteTicket(tgtId);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    /**
     * Create response entity for ticket response entity.
     *
     * @param request the request
     * @param tgtId   the tgt id
     * @return the response entity
     * @throws Exception the exception
     */
    protected ResponseEntity<String> createResponseEntityForTicket(final HttpServletRequest request,
                                                                   final TicketGrantingTicket tgtId) throws Exception {
        return this.ticketGrantingTicketResourceEntityResponseFactory.build(tgtId, request);
    }

    /**
     * Create ticket granting ticket for request ticket granting ticket.
     *
     * @param requestBody the request body
     * @param request     the request
     * @return the ticket granting ticket
     */
    protected TicketGrantingTicket createTicketGrantingTicketForRequest(final MultiValueMap<String, String> requestBody,
                                                                        final HttpServletRequest request) {
        val credential = this.credentialFactory.fromRequest(request, requestBody);
        if (credential == null || credential.isEmpty()) {
            throw new BadRestRequestException("No credentials are provided or extracted to authenticate the REST request");
        }
        val service = this.serviceFactory.createService(request);
        val authenticationResult = authenticationSystemSupport.handleAndFinalizeSingleAuthenticationTransaction(service, credential);
        return centralAuthenticationService.createTicketGrantingTicket(authenticationResult);
    }
}

3、后台配置
以springboot为例

有一个LoginFilter耦合了业务代码,没有贴,让这个过滤器启动顺序早于Cas过滤器,先判定登录,如果为登录,返回401Http状态码。	


(1) build.gradle中添加依赖(maven的自行改过去就是):
 compile group: 'org.jasig.cas.client', name: 'cas-client-core', version: '3.6.2'

(2) 修改application.properties:

#########cas-client##############
# 监听退出的接口,即所有接口都会进行监听
spring.cas.sign-out-filters=/*
# 需要拦截的认证的接口
spring.cas.auth-filters=/*
spring.cas.validate-filters=/*
spring.cas.request-wrapper-filters=/*
spring.cas.assertion-filters=/*
# 表示忽略拦截的接口,也就是不用进行拦截
spring.cas.ignore-filters=/test
spring.cas.cas-server-login-url==http://192.168.1.11:8080/cas/login
spring.cas.cas-server-url-prefix=http://192.168.1.11:8080/cas/
spring.cas.redirect-after-validation=false
spring.cas.use-session=true
spring.cas.server-name=http://127.0.0.1:7901

(3) 读取配置文件类

package com.iscas.biz.config.cas;

import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.Arrays;
import java.util.List;

/**
 *
 * @author zhuquanwen
 * @vesion 1.0
 * @date 2021/7/3 17:17
 * @since jdk1.8
 */
@ConfigurationProperties(prefix ="spring.cas")
public class CasProps {
    static final String separator = ",";

    private String validateFilters;
    private String signOutFilters;
    private String authFilters;
    private String assertionFilters;
    private String requestWrapperFilters;
    private String ignoreFilters; //需要放行的url,多个可以使用|分隔,遵循正则

    private String casServerUrlPrefix;
    private String casServerLoginUrl;
    private String serverName;
    private boolean useSession = true;
    private boolean redirectAfterValidation = true;

    public String getIgnoreFilters() {
        return ignoreFilters;
    }
    public void setIgnoreFilters(String ignoreFilters) {
        this.ignoreFilters = ignoreFilters;
    }
    public List<String> getValidateFilters() {
        return Arrays.asList(validateFilters.split(separator));
    }
    public void setValidateFilters(String validateFilters) {
        this.validateFilters = validateFilters;
    }
    public List<String> getSignOutFilters() {
        return Arrays.asList(signOutFilters.split(separator));
    }
    public void setSignOutFilters(String signOutFilters) {
        this.signOutFilters = signOutFilters;
    }
    public List<String> getAuthFilters() {
        return Arrays.asList(authFilters.split(separator));
    }
    public void setAuthFilters(String authFilters) {
        this.authFilters = authFilters;
    }
    public List<String> getAssertionFilters() {
        return Arrays.asList(assertionFilters.split(separator));
    }
    public void setAssertionFilters(String assertionFilters) {
        this.assertionFilters = assertionFilters;
    }
    public List<String> getRequestWrapperFilters() {
        return Arrays.asList(requestWrapperFilters.split(separator));
    }
    public void setRequestWrapperFilters(String requestWrapperFilters) {
        this.requestWrapperFilters = requestWrapperFilters;
    }
    public String getCasServerUrlPrefix() {
        return casServerUrlPrefix;
    }
    public void setCasServerUrlPrefix(String casServerUrlPrefix) {
        this.casServerUrlPrefix = casServerUrlPrefix;
    }
    public String getCasServerLoginUrl() {
        return casServerLoginUrl;
    }
    public void setCasServerLoginUrl(String casServerLoginUrl) {
        this.casServerLoginUrl = casServerLoginUrl;
    }
    public String getServerName() {
        return serverName;
    }
    public void setServerName(String serverName) {
        this.serverName = serverName;
    }
    public boolean isRedirectAfterValidation() {
        return redirectAfterValidation;
    }
    public void setRedirectAfterValidation(boolean redirectAfterValidation) {
        this.redirectAfterValidation = redirectAfterValidation;
    }
    public boolean isUseSession() {
        return useSession;
    }
    public void setUseSession(boolean useSession) {
        this.useSession = useSession;
    }
}

(4)自定义CAS过滤器,将重定向去掉

/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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.
 */
package com.iscas.biz.config.cas;

import org.jasig.cas.client.Protocol;
import org.jasig.cas.client.authentication.*;
import org.jasig.cas.client.configuration.ConfigurationKeys;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.ReflectUtils;
import org.jasig.cas.client.validation.Assertion;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 将{@link AuthenticationFilter}类的内容拷贝过来,
 * 做一些改动,因为使用了rest的接口,不进行重定向
 */
public class CustomCasAuthenticationFilter extends AbstractCasFilter {
    private boolean isRedirectAfterValidation;

    /**
     * The URL to the CAS Server login.
     */
    private String casServerLoginUrl;

    /**
     * Whether to send the renew request or not.
     */
    private boolean renew = false;

    /**
     * Whether to send the gateway request or not.
     */
    private boolean gateway = false;

    /**
     * The method used by the CAS server to send the user back to the application.
     */
    private String method;

    private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();

    private AuthenticationRedirectStrategy authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy();

    private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = null;

    private static final Map<String, Class<? extends UrlPatternMatcherStrategy>> PATTERN_MATCHER_TYPES =
            new HashMap<String, Class<? extends UrlPatternMatcherStrategy>>();

    static {
        PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class);
        PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class);
        PATTERN_MATCHER_TYPES.put("FULL_REGEX", EntireRegionRegexUrlPatternMatcherStrategy.class);
        PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class);
    }

    public CustomCasAuthenticationFilter(boolean isRedirectAfterValidation) {
        this();
        this.isRedirectAfterValidation = isRedirectAfterValidation;
    }

    public CustomCasAuthenticationFilter() {
        this(Protocol.CAS2);
    }

    protected CustomCasAuthenticationFilter(final Protocol protocol) {
        super(protocol);
    }

    @Override
    protected void initInternal(final FilterConfig filterConfig) throws ServletException {
        if (!isIgnoreInitConfiguration()) {
            super.initInternal(filterConfig);

            final String loginUrl = getString(ConfigurationKeys.CAS_SERVER_LOGIN_URL);
            if (loginUrl != null) {
                setCasServerLoginUrl(loginUrl);
            } else {
                setCasServerUrlPrefix(getString(ConfigurationKeys.CAS_SERVER_URL_PREFIX));
            }

            setRenew(getBoolean(ConfigurationKeys.RENEW));
            setGateway(getBoolean(ConfigurationKeys.GATEWAY));
            setMethod(getString(ConfigurationKeys.METHOD));

            final String ignorePattern = getString(ConfigurationKeys.IGNORE_PATTERN);
            final String ignoreUrlPatternType = getString(ConfigurationKeys.IGNORE_URL_PATTERN_TYPE);

            if (ignorePattern != null) {
                final Class<? extends UrlPatternMatcherStrategy> ignoreUrlMatcherClass = PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType);
                if (ignoreUrlMatcherClass != null) {
                    this.ignoreUrlPatternMatcherStrategyClass = ReflectUtils.newInstance(ignoreUrlMatcherClass.getName());
                } else {
                    try {
                        logger.trace("Assuming {} is a qualified class name...", ignoreUrlPatternType);
                        this.ignoreUrlPatternMatcherStrategyClass = ReflectUtils.newInstance(ignoreUrlPatternType);
                    } catch (final IllegalArgumentException e) {
                        logger.error("Could not instantiate class [{}]", ignoreUrlPatternType, e);
                    }
                }
                if (this.ignoreUrlPatternMatcherStrategyClass != null) {
                    this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern);
                }
            }

            final Class<? extends GatewayResolver> gatewayStorageClass = getClass(ConfigurationKeys.GATEWAY_STORAGE_CLASS);

            if (gatewayStorageClass != null) {
                setGatewayStorage(ReflectUtils.newInstance(gatewayStorageClass));
            }

            final Class<? extends AuthenticationRedirectStrategy> authenticationRedirectStrategyClass = getClass(ConfigurationKeys.AUTHENTICATION_REDIRECT_STRATEGY_CLASS);

            if (authenticationRedirectStrategyClass != null) {
                this.authenticationRedirectStrategy = ReflectUtils.newInstance(authenticationRedirectStrategyClass);
            }
        }
    }

    @Override
    public void init() {
        super.init();

        final String message = String.format(
                "one of %s and %s must not be null.",
                ConfigurationKeys.CAS_SERVER_LOGIN_URL.getName(),
                ConfigurationKeys.CAS_SERVER_URL_PREFIX.getName());

        CommonUtils.assertNotNull(this.casServerLoginUrl, message);
    }

    @Override
    public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
                               final FilterChain filterChain) throws IOException, ServletException {

        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;

        if (isRequestUrlExcluded(request)) {
            logger.debug("Request is ignored.");
            filterChain.doFilter(request, response);
            return;
        }

        final HttpSession session = request.getSession(false);
        final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;

        if (assertion != null) {
            filterChain.doFilter(request, response);
            return;
        }

        final String serviceUrl = constructServiceUrl(request, response);
        final String ticket = retrieveTicketFromRequest(request);
        final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);

        if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
            filterChain.doFilter(request, response);
            return;
        }

        //修改CAS的源码 add by zqw
        //因为使用的是REST接口,也不使用cookie,并且LoginFilter的优先级比
        //当前的Filter优先级高,需要登录与否已经判断完了,这里无需重定向,不然无需权限控制的URL会陷入无限重定向中
        if (isRedirectAfterValidation) {
            final String modifiedServiceUrl;

            logger.debug("no ticket and no assertion found");
            if (this.gateway) {
                logger.debug("setting gateway attribute in session");
                modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
            } else {
                modifiedServiceUrl = serviceUrl;
            }

            logger.debug("Constructed service url: {}", modifiedServiceUrl);

            final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
                    getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway, this.method);

            logger.debug("redirecting to \"{}\"", urlToRedirectTo);
            this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
        } else {
            //进入下一个过滤器
            filterChain.doFilter(request, response);
        }
    }

    public final void setRenew(final boolean renew) {
        this.renew = renew;
    }

    public final void setGateway(final boolean gateway) {
        this.gateway = gateway;
    }

    public void setMethod(final String method) {
        this.method = method;
    }

    public final void setCasServerUrlPrefix(final String casServerUrlPrefix) {
        setCasServerLoginUrl(CommonUtils.addTrailingSlash(casServerUrlPrefix) + "login");
    }

    public final void setCasServerLoginUrl(final String casServerLoginUrl) {
        this.casServerLoginUrl = casServerLoginUrl;
    }

    public final void setGatewayStorage(final GatewayResolver gatewayStorage) {
        this.gatewayStorage = gatewayStorage;
    }

    private boolean isRequestUrlExcluded(final HttpServletRequest request) {
        if (this.ignoreUrlPatternMatcherStrategyClass == null) {
            return false;
        }

        final StringBuffer urlBuffer = request.getRequestURL();
        if (request.getQueryString() != null) {
            urlBuffer.append("?").append(request.getQueryString());
        }
        final String requestUri = urlBuffer.toString();
        return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);
    }

    public final void setIgnoreUrlPatternMatcherStrategyClass(
            final UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass) {
        this.ignoreUrlPatternMatcherStrategyClass = ignoreUrlPatternMatcherStrategyClass;
    }

}

(5)CAS配置类

package com.iscas.biz.config.cas;

import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.util.AssertionThreadLocalFilter;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 *
 * @author zhuquanwen
 * @vesion 1.0
 * @date 2021/7/3 17:19
 * @since jdk1.8
 */
@Configuration
@EnableConfigurationProperties(CasProps.class)
public class CasCustomConfig {
    @Autowired
    private CasProps casProps;

    private static boolean casEnabled = true;

    public CasCustomConfig() {
    }


    @Bean
    public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener() {
        ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> listener = new ServletListenerRegistrationBean<SingleSignOutHttpSessionListener>();
        listener.setEnabled(casEnabled);
        listener.setListener(new SingleSignOutHttpSessionListener());
        listener.setOrder(1);
        return listener;
    }

    /**
     * 该过滤器用于实现单点登出功能,单点退出配置,一定要放在其他filter之前
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean singleSignOutFilter() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new SingleSignOutFilter());
        filterRegistration.setEnabled(casEnabled);
        if (casProps.getSignOutFilters().size() > 0) {
            filterRegistration.setUrlPatterns(casProps.getSignOutFilters());
        } else {
            filterRegistration.addUrlPatterns("/*");
        }
        filterRegistration.addInitParameter("casServerUrlPrefix", casProps.getCasServerUrlPrefix());
        filterRegistration.setOrder(3);
        return filterRegistration;
    }

    /**
     * 该过滤器负责用户的认证工作
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean authenticationFilter() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new CustomCasAuthenticationFilter(casProps.isRedirectAfterValidation()));
        filterRegistration.setEnabled(casEnabled);
        if (casProps.getAuthFilters().size() > 0) {
            filterRegistration.setUrlPatterns(casProps.getAuthFilters());
        } else {
            filterRegistration.addUrlPatterns("/*");
        }
        if (casProps.getIgnoreFilters() != null) {
            filterRegistration.addInitParameter("ignorePattern", casProps.getIgnoreFilters());
        }
        filterRegistration.addInitParameter("casServerLoginUrl", casProps.getCasServerLoginUrl());
        filterRegistration.addInitParameter("serverName", casProps.getServerName());
        filterRegistration.addInitParameter("useSession", casProps.isUseSession() ? "true" : "false");
        filterRegistration.addInitParameter("redirectAfterValidation", casProps.isRedirectAfterValidation() ? "true" : "false");
        filterRegistration.setOrder(4);
        return filterRegistration;
    }

    /**
     * 该过滤器负责对Ticket的校验工作,使用CAS 3.0协议
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean cas30ProxyReceivingTicketValidationFilter() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
        filterRegistration.setEnabled(casEnabled);
        if (casProps.getValidateFilters().size() > 0) {
            filterRegistration.setUrlPatterns(casProps.getValidateFilters());
        } else {
            filterRegistration.addUrlPatterns("/*");
        }
        filterRegistration.addInitParameter("casServerUrlPrefix", casProps.getCasServerUrlPrefix());
        filterRegistration.addInitParameter("serverName", casProps.getServerName());
        filterRegistration.setOrder(5);
        return filterRegistration;
    }

    @Bean
    public FilterRegistrationBean httpServletRequestWrapperFilter() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new HttpServletRequestWrapperFilter());
        filterRegistration.setEnabled(true);
        if (casProps.getRequestWrapperFilters().size() > 0) {
            filterRegistration.setUrlPatterns(casProps.getRequestWrapperFilters());
        } else {
            filterRegistration.addUrlPatterns("/*");
        }
        filterRegistration.setOrder(6);
        return filterRegistration;
    }

    /**
     * 该过滤器使得可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。
     * 比如AssertionHolder.getAssertion().getPrincipal().getName()。
     * 这个类把Assertion信息放在ThreadLocal变量中,这样应用程序不在web层也能够获取到当前登录信息
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean assertionThreadLocalFilter() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new AssertionThreadLocalFilter());
        filterRegistration.setEnabled(true);
        if (casProps.getAssertionFilters().size() > 0) {
            filterRegistration.setUrlPatterns(casProps.getAssertionFilters());
        } else {
            filterRegistration.addUrlPatterns("/*");
        }
        filterRegistration.setOrder(7);
        return filterRegistration;
    }
}

(6)测试接口(为了测试认证的,没有实际意义)

package com.iscas.biz.test.cas;

import com.iscas.templet.common.BaseController;
import com.iscas.templet.common.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 *
 * @author zhuquanwen
 * @vesion 1.0
 * @date 2021/7/3 15:03
 * @since jdk1.8
 */
@RestController
@RequestMapping("/test/cas")
public class CasTestController extends BaseController {

    @PostMapping("/post")
    public ResponseEntity post() {
        return getResponse();
    }
}

(7)定义cas认证ST接口
这里的AuthServiceImpl 不用纠结,意思就是通过认证后的用户名,生成一个短期的Token,并返回,之后的请求前端将这个token放入请求头就可以了。后端解析Token,如果token有效就认为有权限,不必再去访问CAS。

package com.iscas.biz.controller.common;

import com.iscas.base.biz.config.auth.TokenProps;
import com.iscas.base.biz.service.AbstractAuthService;
import com.iscas.base.biz.util.SpringUtils;
import com.iscas.biz.service.common.AuthServiceImpl;
import com.iscas.templet.common.BaseController;
import com.iscas.templet.common.ResponseEntity;
import com.iscas.templet.exception.LoginException;
import org.jasig.cas.client.util.AssertionHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 *
 * @author zhuquanwen
 * @vesion 1.0
 * @date 2021/7/3 17:41
 * @since jdk1.8
 */
@RestController
@RequestMapping("/cas")
public class CasClientController extends BaseController {
    @Autowired
    private TokenProps tokenProps;

    @Autowired
    private AuthServiceImpl authService;
    @GetMapping("/valid")
    public ResponseEntity valid() throws LoginException {
        //获取CAS登录的用户名
        String username = AssertionHolder.getAssertion().getPrincipal().getName();
        ResponseEntity response = getResponse();
        authService.createToken(SpringUtils.getResponse(), username, response, ((Long) tokenProps.getExpire().toMinutes()).intValue(), tokenProps.getCookieExpire(), "", "");
        return response;
    }
}

4、前端编写一个支持跨域的JS(从网上找的)

研究了一下,这个东西的原理就是需要一个中间服务做中转,可以将corss-middle.js和corss-corss.html单独部署为一个服务,或用其中一个服务的前端也可以(要保证这个服务一直可用)。corss-client.js是客户端用的,放在每个服务的前端中。

corss-middle.js

class Middle {
    constructor() {
        this.iframeWin = window.parent;
        this.map = {
            /**
             * 设置数据
             * @param {Object} key
             * @param {Object} value
             */
            setStore(key, value) {
                if (!key) return;
                if (!key instanceof Object) return localStorage.setItem(key, value);
                Object.keys(key).forEach(dataKey => {
                    let dataValue = typeof key[dataKey] === 'object' ? JSON.stringify(key[dataKey]) : key[dataKey];
                    localStorage.setItem(dataKey, dataValue);
                });
            },

            /**
             * 获取数据
             * @param {Object} key
             */
            getStore(key) {
                if (typeof key === 'string') return localStorage.getItem(key);
                let dataRes = {};
                key.forEach(dataKey => {
                    dataRes[dataKey] = localStorage.getItem(dataKey) || null;
                });
                return dataRes;
            },

            /**
             * 删除数据
             * @param {Object} key
             */
            deleteStore(key) {
                let removeKeys = [...key];
                removeKeys.forEach(dataKey => {
                    localStorage.removeItem(dataKey);
                });
            },

            /**
             * 清空
             */
            clearStore() {
                localStorage.clear();
            }
        };

        this._initListener(); //监听消息
    }

    /**
     * 监听
     */
    _initListener() {
        window.addEventListener('message', (e) => {
            let {
                method,
                key,
                value,
                id = "default",
                ...res
            } = e.data;

            //取出本地的数据
            let mapFun = this.map[`${method}Store`];

            if (!mapFun) {
                return this.iframeWin.postMessage({
                    id,
                    request: e.data,
                    reponse: 'Request mode error!'
                }, '*');
            }

            //取出本地的数据
            let storeData = mapFun(key, value);

            //发送给父亲
            this.iframeWin.postMessage({
                id,
                request: e.data,
                reponse: storeData
            }, '*');
        })
    }
}

corss.middle.html

<!-- corss-middle 页面代码 -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <script src="corss-middle.js"></script>
</head>
<body>
<h1>This is Middle page!</h1>
<script>
    const corssMiddle = new Middle();
</script>
</body>
</html>

corss-client.js

<!-- corss-middle 页面代码 -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <script src="corss-middle.js"></script>
</head>
<body>
<h1>This is Middle page!</h1>
<script>
    const corssMiddle = new Middle();
</script>
</body>
</html>

5、前端代码(这里用原生JS写的,只是为了测试流程)

大概流程是这样:
1)test.html模拟前端一个页面,corssStorage初始化的地址就是第4步的中间服务的地址;
2)“点我测试”按钮对应test() 函数,向后台test/cas/post测试接口发送ajax请求;
3)如果返回401错误,代表认证没通过,调用toSSO(corssStorage)尝试单点登录;
4)toSSO中从corssStorage跨域共享服务中获取location;
5)第4)步如果获取到location,调用getST(location)函数获取ST;
6)第5)步如果取到ST,调用validST(st)函数,访问后台接口校验ST;
7)校验成功就可以执行成功的逻辑了,校验失败给与提示;
8)第5)步如果没有获取到ST,提示出错,跳转到登录页面;
9)第4)步如果没有获取到location,跳转到登录页面。

common.js

//后台服务地址
let backendUrlPrefix='http://127.0.0.1:7901/demo/';
//cas服务地址
let casServerPrefix='http://192.168.1.11:8080/cas/';
//后台校验st的url
let backendValidUrl='cas/valid';

/**通过TGT获取ST*/
function getST(location) {
    $.ajax({
        url: location,
        data: {
            service: backendUrlPrefix + backendValidUrl
        },
        type: "POST",
        dataType: "json",
        contentType:'application/x-www-form-urlencoded',
        xhrFields: {
            withCredentials: true //允许跨域带Cookie
        },
        crossDomain: true,
        error: function (res, error) {
            console.log(res);
            if (res.status == 200) {
                //成功获取到ST
                //调用后台服务校验ST
                let st = res.responseText;
                validST(st);
            } else {
                alert("获取ST出错,TGT可能已过期或不可用了,请重新登录");
                window.location.href = "ticket.html";
            }
        }
    });
}

/**通过后台校验ST*/
function validST(st) {
    $.ajax({
        url: backendUrlPrefix + backendValidUrl + "?ticket=" + st,
        type: "GET",
        dataType: "json",
        xhrFields: {
            withCredentials: true //允许跨域带Cookie
        },
        crossDomain: true,
        success: function (data) {
            alert("单点登录成功,前端做其他操作吧");
        },
        error: function (res, error) {
            console.log(res);
            if (res.status == 200) {
                //校验成功
                alert(res.responseText);

            } else {
                alert(res.responseText);
            }
        }
    });
}

/**尝试单点登录*/
function toSSO(corssStorage) {
    corssStorage.getItem(['location'], (result, data) => {
        console.log('client-1 getItem result: ', result);
        if (result['location'] != null) {
            //使用location获取ST
            getST(result['location']);
        } else {
            alert("未登陆,将跳转至登陆页面");
            window.location.href = "ticket.html";
        }
    })


}

// function getCorss() {
//     if (crossStorage == null) {
//         crossStorage = new CorssClient('http://127.0.0.1:8080/testCas/corss-middle.html');
//     }
//     return crossStorage;
// }

test.html

<html>
<head>
    <meta charset="UTF-8">
    <title>test-cas</title>
    <script src="jquery-3.4.1.min.js" charset="utf-8"></script>
    <script src="common.js" charset="utf-8"></script>



</head>

<body>
    <script src="corss-client.js" charset="utf-8"></script>
    <script>
        const corssStorage = new CorssClient('http://127.0.0.1:8080/testCas/corss-middle.html')

        // let backendUrlPrefix='http://localhost:7901/demo/';
        // let casServerPrefix='http://localhost:8080/cas';

        /**测试*/
        function test() {
            //发送一个请求,给后端
            $.ajax({
                url: backendUrlPrefix + "test/cas/post",
                data: {name: 'jenny'},
                type: "POST",
                dataType: "json",
                xhrFields: {
                    withCredentials: true //允许跨域带Cookie
                },
                crossDomain: true,
                success: function(data) {
                    // data = jQuery.parseJSON(data);  //dataType指明了返回数据为json类型,故不需要再反序列化
                },
                error: function (res, error) {
                    if (res.status == 401) {
                        //尝试单点登录
                        toSSO(corssStorage);

                        //如果未登陆,登陆
                        // window.location.href = "ticket.html";
                    }
                    if (res.status == 403) {
                        alert("鉴权失败");
                    }
                }
            });
        }

        /**登出*/
        function logout() {
            corssStorage.getItem(['location'], (result, data) => {
                console.log('client-1 getItem result: ', result);
                if (result['location'] != null) {
                    //使用location获取ST
                    $.ajax({
                        url: result['location'],
                        type: "DELETE",
                        dataType: "json",
                        xhrFields: {
                            withCredentials: true //允许跨域带Cookie
                        },
                        crossDomain: true,
                        success: function(data) {
                            // data = jQuery.parseJSON(data);  //dataType指明了返回数据为json类型,故不需要再反序列化
                        },
                        error: function (res, error) {
                            console.log(res);
                            if (res.status == 200) {
                                alert("退出成功");
                            } else {
                                alert("退出失败");
                            }
                        }
                    });
                } else {
                    alert("无需登出");
                }
            })
        }

    </script>
<button onclick="test();">点我测试</button><br/>
<button onclick="logout();">退出登录</button><br/>
</body>

</html>

ticket.html

<html>
<head>
    <meta charset="UTF-8">
    <title>test-cas</title>
    <script src="jquery-3.4.1.min.js" charset="utf-8"></script>
    <script src="common.js" charset="utf-8"></script>


</head>

<body>
    <script src="corss-client.js" charset="utf-8"></script>
    <script>

        // let backendUrlPrefix='http://localhost:7901/demo/';
        // let casServerPrefix='http://localhost:8080/cas/';
        // //后台校验st的url
        // let backendValidUrl='cas/valid';
        const crossStorage = new CorssClient('http://127.0.0.1:8080/testCas/corss-middle.html')
        /**登陆*/
        function getTicket() {
            //使用用户名、密码获取ticket
            $.ajax({
                url: casServerPrefix + "v1/tickets",
                data: $('#loginForm').serialize(),
                type: "POST",
                dataType: "json",
                contentType:'application/x-www-form-urlencoded',
                xhrFields: {
                    withCredentials: true //允许跨域带Cookie
                },
                crossDomain: true,
                // beforeSend: function(request) {
                //     request.setRequestHeader("Access-Control-Allow-Origin","http://127.0.0.1:8080");
                //     request.setRequestHeader("access-control-allow-origin","http://127.0.0.1:8080");
                // },
                success: function(data) {
                    console.log(data);
                },
                error: function (res) {
                    if (res.status == 401) {
                        //登陆失败,给与用户提示
                        let error = res.responseJSON.authentication_exceptions;
                        console.log(error);
                        alert(error[1][0]);
                    } else if (res.status == 201) {
                        //登陆成功
                        //获取TGT
                        let tgt = res.responseText;

                        //获取location,存入跨域存储corss-middle中
                        let location = res.getResponseHeader('Location');
                        console.log(location);
                        crossStorage.setItem({
                            location: location
                        }, null, (result) => {
                            console.log('完成跨域存储location')
                            //通过TGT获取ST
                            let location = casServerPrefix + "v1/tickets/" + (tgt == null ? "" : tgt);
                            getST(location);
                        })

                    }
                }
            });
        }
    </script>
    <form id = "loginForm", type="application/x-www-form-urlencoded">
        用户名:<input type="text" name="username"/><br/>
        密码:<input type="password" name="password"/>
        <input type="button" value="登陆" onclick="getTicket();"/>
    </form>
</body>

</html>
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值