CAS流程简析 客户端处理未携带Ticket访问请求
相关阅读
简介
用户访问客户端的请求,会先经过客户端配置的过滤器链,常用的过滤器如下:
- CAS Single Sign Out Filter——SingleSignOutFilter
- 实现单点登出,放在首个位置;
- CAS Validation Filter——Cas30ProxyReceivingTicketValidationFilter
- 负责对Ticket的校验
- 需要指定服务端地址:casServerUrlPrefix
- 需要指定客户端地址:serverName
- CAS Authentication Filter——AuthenticationFilter
- 负责用户的鉴权
- 需要指定服务端登录地址:casServerLoginUrl
- 需要指定客户端地址:serverName
- CAS HttpServletRequest Wrapper Filter——HttpServletRequestWrapperFilter
- 负责包装
HttpServletRequest
,从而可通过HttpServletRequest
的getRemoteUser()
方法获取登录用户的登录名
用户的访问请求会依次经过以上配置的过滤器的拦截和处理;
- 负责包装
SingleSignOutFilter
首先经过SingleSignOutFilter
处理,该过滤器实现单点登出功能;
本次是用户访问请求,不是登出请求,该过滤器会直接放行,核心代码如下:
private static final SingleSignOutHandler HANDLER = new SingleSignOutHandler();
public void init(final FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
if (!isIgnoreInitConfiguration()) {
// 设置属性
setArtifactParameterName(getString(ConfigurationKeys.ARTIFACT_PARAMETER_NAME));
setLogoutParameterName(getString(ConfigurationKeys.LOGOUT_PARAMETER_NAME));
setFrontLogoutParameterName(getString(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME));
setRelayStateParameterName(getString(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME));
setCasServerUrlPrefix(getString(ConfigurationKeys.CAS_SERVER_URL_PREFIX));
HANDLER.setArtifactParameterOverPost(getBoolean(ConfigurationKeys.ARTIFACT_PARAMETER_OVER_POST));
HANDLER.setEagerlyCreateSessions(getBoolean(ConfigurationKeys.EAGERLY_CREATE_SESSIONS));
}
// handler初始化
HANDLER.init();
// 设置handler初始化完成标识
handlerInitialized.set(true);
}
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
/**
* <p>Workaround for now for the fact that Spring Security will fail since it doesn't call {@link #init(javax.servlet.FilterConfig)}.</p>
* <p>Ultimately we need to allow deployers to actually inject their fully-initialized {@link org.jasig.cas.client.session.SingleSignOutHandler}.</p>
*/
// 校验handler是否初始化完成
if (!this.handlerInitialized.getAndSet(true)) {
HANDLER.init();
}
// 根据handler的处理选择是否继续过滤器链
if (HANDLER.process(request, response)) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
首先分析设置属性的操作,设置属性时用到了getString
方法,该方法继承自父类AbstractConfigurationFilter
,代码如下:
protected final String getString(final ConfigurationKey<String> configurationKey) {
return this.configurationStrategy.getString(configurationKey);
}
getString
使用了this.configurationStrategy
,继续看下该属性的赋值,代码如下:
private static final String CONFIGURATION_STRATEGY_KEY = "configurationStrategy";
public void init(FilterConfig filterConfig) throws ServletException {
// 从配置中获取configurationStrategy的配置信息
final String configurationStrategyName = filterConfig.getServletContext().getInitParameter(CONFIGURATION_STRATEGY_KEY);
// 根据configurationStrategy的配置信息获取ConfigurationStrategy实例
this.configurationStrategy = ReflectUtils.newInstance(ConfigurationStrategyName.resolveToConfigurationStrategy(configurationStrategyName));
// 执行初始化操作
this.configurationStrategy.init(filterConfig, getClass());
}
// ConfigurationStrategyName.java
DEFAULT(LegacyConfigurationStrategyImpl.class),
public static Class<? extends ConfigurationStrategy> resolveToConfigurationStrategy(final String value) {
if (CommonUtils.isBlank(value)) {
// 若为空,返回默认值LegacyConfigurationStrategyImpl.class
return DEFAULT.configurationStrategyClass;
}
for (final ConfigurationStrategyName csn : values()) {
if (csn.name().equalsIgnoreCase(value)) {
return csn.configurationStrategyClass;
}
}
try {
final Class<?> clazz = Class.forName(value);
if (ConfigurationStrategy.class.isAssignableFrom(clazz)) {
return (Class<? extends ConfigurationStrategy>) clazz;
}
} catch (final ClassNotFoundException e) {
LOGGER.error("Unable to locate strategy {} by name or class name. Using default strategy instead.", value, e);
}
return DEFAULT.configurationStrategyClass;
}
若配置文件中没有配置configurationStrategy
,则默认创建LegacyConfigurationStrategyImpl
实例,该类型是WebXmlConfigurationStrategyImpl
和JndiConfigurationStrategyImpl
的包装,优先使用WebXmlConfigurationStrategyImpl
,核心代码如下:
private final WebXmlConfigurationStrategyImpl webXmlConfigurationStrategy = new WebXmlConfigurationStrategyImpl();
private final JndiConfigurationStrategyImpl jndiConfigurationStrategy = new JndiConfigurationStrategyImpl();
public void init(FilterConfig filterConfig, Class<? extends Filter> filterClazz) {
this.webXmlConfigurationStrategy.init(filterConfig, filterClazz);
this.jndiConfigurationStrategy.init(filterConfig, filterClazz);
}
protected String get(final ConfigurationKey key) {
// 优先使用WebXmlConfigurationStrategyImpl
final String value1 = this.webXmlConfigurationStrategy.get(key);
if (CommonUtils.isNotBlank(value1)) {
return value1;
}
return this.jndiConfigurationStrategy.get(key);
}
接着分析SingleSignOutHandler
的实现,其核心代码如下:
public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
if (isTokenRequest(request)) {
// 若是携带token的请求
// 本次用户访问请求未携带Ticket信息,不满足
logger.trace("Received a token request");
// 记录session
recordSession(request);
// 返回true,继续执行过滤器链
return true;
} else if (isBackChannelLogoutRequest(request)) {
// 若是后置通道登出请求
// 本次用户访问请求不是登出请求,不满足
logger.trace("Received a back channel logout request");
// 销毁session
destroySession(request);
// 返回false,终止执行过滤器链
return false;
} else if (isFrontChannelLogoutRequest(request)) {
// 若是前置通道登出请求
// 本次用户访问请求不是登出请求,不满足
logger.trace("Received a front channel logout request");
// 销毁session
destroySession(request);
// redirection url to the CAS server
final String redirectionUrl = computeRedirectionToServer(request);
if (redirectionUrl != null) {
// 重定向到CAS Server
CommonUtils.sendRedirect(response, redirectionUrl);
}
// 返回false,终止执行过滤器链
return false;
} else {
logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
// 默认返回true,继续执行过滤器链
// 本次用户访问请求会由这里直接返回true
return true;
}
}
private boolean isTokenRequest(final HttpServletRequest request) {
return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName,
this.safeParameters));
}
private boolean isBackChannelLogoutRequest(final HttpServletRequest request) {
return "POST".equals(request.getMethod())
&& !isMultipartRequest(request)
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
this.safeParameters));
}
private boolean isFrontChannelLogoutRequest(final HttpServletRequest request) {
return "GET".equals(request.getMethod()) && CommonUtils.isNotBlank(this.casServerUrlPrefix)
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName));
}
private void recordSession(final HttpServletRequest request) {
final HttpSession session = request.getSession(this.eagerlyCreateSessions);
if (session == null) {
logger.debug("No session currently exists (and none created). Cannot record session information for single sign out.");
return;
}
final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);
logger.debug("Recording session for token {}", token);
try {
// 移除已缓存的session信息
this.sessionMappingStorage.removeBySessionById(session.getId());
} catch (final Exception e) {
// ignore if the session is already marked as invalid. Nothing we can do!
}
// 缓存当前token和session
sessionMappingStorage.addSessionById(token, session);
}
private void destroySession(final HttpServletRequest request) {
final String logoutMessage;
// front channel logout -> the message needs to be base64 decoded + decompressed
if (isFrontChannelLogoutRequest(request)) {
logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request,
this.frontLogoutParameterName));
} else {
logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
}
logger.trace("Logout request:\n{}", logoutMessage);
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
if (CommonUtils.isNotBlank(token)) {
final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
if (session != null) {
final String sessionID = session.getId();
logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);
try {
// 失效session
session.invalidate();
} catch (final IllegalStateException e) {
logger.debug("Error invalidating session.", e);
}
// 登出
this.logoutStrategy.logout(request);
}
}
}
Cas30ProxyReceivingTicketValidationFilter
根据过滤器配置顺序,接下来就是Cas30ProxyReceivingTicketValidationFilter
,该过滤器负责对Ticket
的校验,需要指定服务端地址casServerUrlPrefix
和客户端地址serverName
;
本次用户访问请求未携带Ticket
信息,该过滤器会直接放行,核心代码如下:
public Cas30ProxyReceivingTicketValidationFilter() {
// 支持协议为CAS3
super(Protocol.CAS3);
// ST校验器使用Cas30ServiceTicketValidator
this.defaultServiceTicketValidatorClass = Cas30ServiceTicketValidator.class;
// PT校验器使用Cas30ProxyTicketValidator
this.defaultProxyTicketValidatorClass = Cas30ProxyTicketValidator.class;
}
Cas30ProxyReceivingTicketValidationFilter
继承自Cas20ProxyReceivingTicketValidationFilter
,重写了this.protocol
、this.defaultServiceTicketValidatorClass
和this.defaultProxyTicketValidatorClass
的实现,其它处理都一样,接着分析Cas20ProxyReceivingTicketValidationFilter
;
Cas20ProxyReceivingTicketValidationFilter
的doFilter
方法继承自父类AbstractTicketValidationFilter
,AbstractTicketValidationFilter
实现了doFilter
方法的算法模板,并提供算法细节preFilter
、onSuccessfulValidation
、onFailedValidation
可供子类重写添加额外处理逻辑,核心代码如下:
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
// 过滤前预处理
if (!preFilter(servletRequest, servletResponse, filterChain)) {
return;
}
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
// 从请求中获取Ticket信息
final String ticket = retrieveTicketFromRequest(request);
// 是否存在Ticket信息
// 本次用户访问请求未携带Ticket信息,直接跳过,执行过滤器链
if (CommonUtils.isNotBlank(ticket)) {
logger.debug("Attempting to validate ticket: {}", ticket);
try {
// 存在Ticket信息就需要校验
final Assertion assertion = this.ticketValidator.validate(ticket,
constructServiceUrl(request, response));
logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
// 未抛出异常,则校验成功
// 将校验结果放入request请求中,方便下次获取
request.setAttribute(CONST_CAS_ASSERTION, assertion);
if (this.useSession) {
// 使用session情况下,将校验结果方法session属性中
request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
}
// 校验成功时处理
onSuccessfulValidation(request, response, assertion);
if (this.redirectAfterValidation) {
// 默认为true,校验后需要重定向
logger.debug("Redirecting after successful ticket validation.");
response.sendRedirect(constructServiceUrl(request, response));
return;
}
} catch (final TicketValidationException e) {
logger.debug(e.getMessage(), e);
// 校验失败时处理
onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(e);
}
// 返回HttpServletResponse.SC_FORBIDDEN响应
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
return;
}
}
// 执行过滤器链
filterChain.doFilter(request, response);
}
protected boolean preFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
return true;
}
protected void onSuccessfulValidation(final HttpServletRequest request, final HttpServletResponse response,
final Assertion assertion) {
// nothing to do here.
}
protected void onFailedValidation(final HttpServletRequest request, final HttpServletResponse response) {
// nothing to do here.
}
protected String retrieveTicketFromRequest(final HttpServletRequest request) {
// 从请求中获取"ticket"信息
return CommonUtils.safeGetParameter(request, this.protocol.getArtifactParameterName());
}
对于this.redirectAfterValidation
属性需要说明下,该值决定了Ticket
校验成功后是否需要重定向:
- 如果进行重定向,那么新请求就不会携带
Ticket
信息,新请求也就不需要再进行Ticket
校验处理;但由于AuthenticationFilter
是从Session
中获取鉴权结果,所以要进行重定向,就必须要支持使用Session
,即this.useSession
为true; - 如果不进行重定向,那么原请求还会携带
Ticket
信息,AuthenticationFilter
获取到Ticket
信息就会放行;
所以this.redirectAfterValidation
属性赋值的相关代码如下:
private boolean redirectAfterValidation = true;
protected void initInternal(final FilterConfig filterConfig) throws ServletException {
setExceptionOnValidationFailure(getBoolean(ConfigurationKeys.EXCEPTION_ON_VALIDATION_FAILURE));
setRedirectAfterValidation(getBoolean(ConfigurationKeys.REDIRECT_AFTER_VALIDATION));
setUseSession(getBoolean(ConfigurationKeys.USE_SESSION));
if (!this.useSession && this.redirectAfterValidation) {
// 如果不使用Session,那么就不支持Ticket校验后重定向到客户端
logger.warn("redirectAfterValidation parameter may not be true when useSession parameter is false. Resetting it to false in order to prevent infinite redirects.");
setRedirectAfterValidation(false);
}
setTicketValidator(getTicketValidator(filterConfig));
super.initInternal(filterConfig);
}
Cas20ProxyReceivingTicketValidationFilter
重写了preFilter
,主要针对代理模式下的请求做预处理,代码如下:
protected final boolean preFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String requestUri = request.getRequestURI();
// 是否是代理请求
// 本次用户访问请求不是代理请求
if (CommonUtils.isEmpty(this.proxyReceptorUrl) || !requestUri.endsWith(this.proxyReceptorUrl)) {
return true;
}
try {
CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage);
} catch (final RuntimeException e) {
logger.error(e.getMessage(), e);
throw e;
}
return false;
}
AbstractTicketValidationFilter.doFilter
中使用了this.ticketValidator
来校验Ticket
信息,Cas30ProxyReceivingTicketValidationFilter
中默认使用的是Cas30ServiceTicketValidator
,其validate
方法继承自父类AbstractUrlBasedTicketValidator
,AbstractUrlBasedTicketValidator
实现了validate
方法的算法模板,提供算法细节retrieveResponseFromServer
、parseResponseFromServer
由子类实现,代码如下:
public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
// 构建校验请求URL
final String validationUrl = constructValidationUrl(ticket, service);
logger.debug("Constructing validation url: {}", validationUrl);
try {
// 发送校验请求并获取服务端响应
logger.debug("Retrieving response from server.");
final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);
// 返回结果为空则校验失败
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
}
logger.debug("Server response: {}", serverResponse);
// 解析服务端响应
return parseResponseFromServer(serverResponse);
} catch (final MalformedURLException e) {
throw new TicketValidationException(e);
}
}
protected abstract String retrieveResponseFromServer(URL validationUrl, String ticket);
protected abstract Assertion parseResponseFromServer(final String response) throws TicketValidationException;
protected final String constructValidationUrl(final String ticket, final String serviceUrl) {
final Map<String, String> urlParameters = new HashMap<String, String>();
logger.debug("Placing URL parameters in map.");
// 放入Ticket信息
urlParameters.put("ticket", ticket);
// 放入Service信息
urlParameters.put("service", serviceUrl);
// 如果包含renew
if (this.renew) {
urlParameters.put("renew", "true");
}
logger.debug("Calling template URL attribute map.");
// 填充其他属性
populateUrlAttributeMap(urlParameters);
logger.debug("Loading custom parameters from configuration.");
if (this.customParameters != null) {
urlParameters.putAll(this.customParameters);
}
final String suffix = getUrlSuffix();
final StringBuilder buffer = new StringBuilder(urlParameters.size() * 10 + this.casServerUrlPrefix.length()
+ suffix.length() + 1);
int i = 0;
// 构造URL
buffer.append(this.casServerUrlPrefix);
if (!this.casServerUrlPrefix.endsWith("/")) {
buffer.append("/");
}
buffer.append(suffix);
for (Map.Entry<String, String> entry : urlParameters.entrySet()) {
final String key = entry.getKey();
final String value = entry.getValue();
if (value != null) {
buffer.append(i++ == 0 ? "?" : "&");
buffer.append(key);
buffer.append("=");
final String encodedValue = encodeUrl(value);
buffer.append(encodedValue);
}
}
return buffer.toString();
}
AbstractCasProtocolUrlBasedTicketValidator
实现了算法细节的retrieveResponseFromServer
的算法模板,代码如下:
protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
// 借助CommonUtils实现
return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
}
// CommonUtils.java
public static String getResponseFromServer(final URL constructedUrl, final HttpURLConnectionFactory factory,
final String encoding) {
HttpURLConnection conn = null;
InputStreamReader in = null;
try {
conn = factory.buildHttpURLConnection(constructedUrl.openConnection());
if (CommonUtils.isEmpty(encoding)) {
in = new InputStreamReader(conn.getInputStream());
} else {
in = new InputStreamReader(conn.getInputStream(), encoding);
}
// 获取响应
final StringBuilder builder = new StringBuilder(255);
int byteRead;
while ((byteRead = in.read()) != -1) {
builder.append((char) byteRead);
}
// 返回响应
return builder.toString();
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
throw new RuntimeException(e);
} finally {
closeQuietly(in);
if (conn != null) {
conn.disconnect();
}
}
}
Cas20ServiceTicketValidator
实现了算法细节parseResponseFromServer
的算法模板,并提供算法细节customParseResponse
供子类重写添加额外处理逻辑,代码如下:
protected final Assertion parseResponseFromServer(final String response) throws TicketValidationException {
final String error = XmlUtils.getTextForElement(response, "authenticationFailure");
if (CommonUtils.isNotBlank(error)) {
// 存在错误信息
throw new TicketValidationException(error);
}
final String principal = XmlUtils.getTextForElement(response, "user");
final String proxyGrantingTicketIou = XmlUtils.getTextForElement(response, "proxyGrantingTicket");
// 获取PGT信息
final String proxyGrantingTicket;
if (CommonUtils.isBlank(proxyGrantingTicketIou) || this.proxyGrantingTicketStorage == null) {
proxyGrantingTicket = null;
} else {
proxyGrantingTicket = this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou);
}
// 校验principal信息
if (CommonUtils.isEmpty(principal)) {
throw new TicketValidationException("No principal was found in the response from the CAS server.");
}
// 封装Assertion信息
final Assertion assertion;
final Map<String, Object> attributes = extractCustomAttributes(response);
if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
final AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes,
proxyGrantingTicket, this.proxyRetriever);
assertion = new AssertionImpl(attributePrincipal);
} else {
assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
}
// 支持子类定制化解析Assertion信息
customParseResponse(response, assertion);
return assertion;
}
protected Map<String, Object> extractCustomAttributes(final String xml) {
final SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
spf.setValidating(false);
try {
final SAXParser saxParser = spf.newSAXParser();
final XMLReader xmlReader = saxParser.getXMLReader();
final CustomAttributeHandler handler = new CustomAttributeHandler();
xmlReader.setContentHandler(handler);
xmlReader.parse(new InputSource(new StringReader(xml)));
return handler.getAttributes();
} catch (final Exception e) {
logger.error(e.getMessage(), e);
return Collections.emptyMap();
}
}
protected void customParseResponse(final String response, final Assertion assertion)
throws TicketValidationException {
// nothing to do
}
AuthenticationFilter
接下来就是AuthenticationFilter
,该过滤器负责用户的鉴权,需要指定服务端登录地址casServerLoginUrl
和客户端地址serverName
;
本次用户访问请求未携带Ticket
信息,也不存在Assertion
信息,所以会被该过滤器重定向到服务端进行鉴权处理,核心代码如下:
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;
}
// 尝试获取Assertion信息
// 本次用户访问请求不存在Assertion信息
final HttpSession session = request.getSession(false);
final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
if (assertion != null) {
// 若Assertion存在,则无需鉴权,直接执行过滤器链
filterChain.doFilter(request, response);
return;
}
// 构造本客户端URL
final String serviceUrl = constructServiceUrl(request, response);
// 从request请求中获取Ticket信息
final String ticket = retrieveTicketFromRequest(request);
final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
// 本次用户访问请求未携带Ticket信息
if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
// 若存在Ticket信息则无需鉴权
filterChain.doFilter(request, response);
return;
}
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);
// 重定向到服务端进行登录鉴权
// 本次用户访问请求到此结束
logger.debug("redirecting to \"{}\"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}
至此,本次未携带Ticket
的用户访问请求处理流程结束。