上一篇已经介绍了将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.class
和TicketGrantingTicketResource.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>