上一章节中我们已经知道是通过http请求去到CAS服务端获取信息,根据CAS单点登录客户端的请求地址/serviceValidate,我们再CAS单点登录服务端上通过Springmvc根据url里的/serviceValidate,匹配到@RequestMapping(path="/serviceValidate")
/serviceValidate路径所对应的java类为ServiceValidateController.java,在cas-server-webapp-validation模块下的org.jasig.cas.web包中。
*ServiceValidateController.java*
@Component("serviceValidateController")
@Controller
public class ServiceValidateController extends AbstractServiceValidateController {
/**
* Handle model and view.
*
* @param request the request
* @param response the response
* @return the model and view
* @throws Exception the exception
*/
@RequestMapping(path="/serviceValidate", method = RequestMethod.GET)
@Override
protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
//交给父类去处理
return super.handleRequestInternal(request, response);
}
调用父类的handleRequestInternal()的方法。
AbstractServiceValidateController.java类在cas-server-webapp-validation模块下的org.jasig.cas.web包中。
*AbstractServiceValidateController.java*
@Component("serviceValidateController")
public abstract class AbstractServiceValidateController extends AbstractDelegateController {
@Override
protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
//CAS单点登录服务端根据请求获取到service
final WebApplicationService service = this.argumentExtractor.extractService(request);
//根据服务端获取的service取到serviceId
final String serviceTicketId = service != null ? service.getArtifactId() : null;
//如果service为空,或者serviceId为空,返回错误信息给CAS单点登录客户端
if (service == null || serviceTicketId == null) {
logger.debug("Could not identify service and/or service ticket for service: [{}]", service);
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_REQUEST,
CasProtocolConstants.ERROR_CODE_INVALID_REQUEST, null, request, service);
}
try {
TicketGrantingTicket proxyGrantingTicketId = null;
//获取pgturl所对应的认证信息(代理模式下),此时为空
final Credential serviceCredential = getServiceCredentialsFromRequest(service, request);
if (serviceCredential != null) {
proxyGrantingTicketId = handleProxyGrantingTicketDelivery(serviceTicketId, serviceCredential);
if (proxyGrantingTicketId == null) {
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
new Object[]{serviceCredential.getId()}, request, service);
}
}
//通过认证中心校验ticket,并返回认证信息
final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
//根据认证用户信息和service,如果校验失败返回ticket校验失效信息到CAS单点登录客户端
if (!validateAssertion(request, serviceTicketId, assertion)) {
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_TICKET,
CasProtocolConstants.ERROR_CODE_INVALID_TICKET, null, request, service);
}
//代理模式下的pgtIOU,此时非代理模式下获取为空
String proxyIou = null;
if (serviceCredential != null && this.proxyHandler.canHandle(serviceCredential)) {
proxyIou = this.proxyHandler.handle(serviceCredential, proxyGrantingTicketId);
if (StringUtils.isEmpty(proxyIou)) {
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
new Object[] {serviceCredential.getId()}, request, service);
}
}
onSuccessfulValidation(serviceTicketId, assertion);
logger.debug("Successfully validated service ticket {} for service [{}]", serviceTicketId, service.getId());
//返回到CAS服务端成功页面,同时把相关参数信息返回给CAS单点登录客户端
return generateSuccessView(assertion, proxyIou, service, proxyGrantingTicketId);
} catch (final AbstractTicketValidationException e) {
final String code = e.getCode();
return generateErrorView(code, code,
new Object[] {serviceTicketId, e.getOriginalService().getId(), service.getId()}, request, service);
} catch (final AbstractTicketException te) {
return generateErrorView(te.getCode(), te.getCode(),
new Object[] {serviceTicketId}, request, service);
} catch (final UnauthorizedProxyingException e) {
return generateErrorView(e.getMessage(), e.getMessage(), new Object[] {service.getId()}, request, service);
} catch (final UnauthorizedServiceException e) {
return generateErrorView(e.getMessage(), e.getMessage(), null, request, service);
}
}
…………
注意校验成功和失败时所构建的jsp页面是不一样的,当成功时请求跳转的页面为casServiceValidationSuccess.jsp;而当失败时,请求的跳转页面为casServiceValidationFailure.jsp。
调用中心认证服务器校验服务ticket票据:centralAuthenticationService.validateServiceTicket。
CentralAuthenticationServiceImpl.java此类在cas-server-core模块下的org.jasig.cas包中。
CentralAuthenticationServiceImpl.java
@Component("centralAuthenticationService")
public final class CentralAuthenticationServiceImpl extends AbstractCentralAuthenticationService {
@Audit(
action="SERVICE_TICKET_VALIDATE",
actionResolverName="VALIDATE_SERVICE_TICKET_RESOLVER",
resourceResolverName="VALIDATE_SERVICE_TICKET_RESOURCE_RESOLVER")
@Timed(name="VALIDATE_SERVICE_TICKET_TIMER")
@Metered(name="VALIDATE_SERVICE_TICKET_METER")
@Counted(name="VALIDATE_SERVICE_TICKET_COUNTER", monotonic=true)
@Override
public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws AbstractTicketException {
//根据service获取到cas服务端维护的service地址
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
//校验获取到的注册service是否满足CAS单点登录服务端的service规则
verifyRegisteredServiceProperties(registeredService, service);
//根据serviceId获取到serviceTicket
final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);
if (serviceTicket == null) {
logger.info("Service ticket [{}] does not exist.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
//校验ticket是否过期,是否有效
try {
synchronized (serviceTicket) {
//注意这里对ticket的判断
if (serviceTicket.isExpired()) {
logger.info("ServiceTicket [{}] has expired.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
if (!serviceTicket.isValidFor(service)) {
logger.error("Service ticket [{}] with service [{}] does not match supplied service [{}]",
serviceTicketId, serviceTicket.getService().getId(), service);
throw new UnrecognizableServiceForServiceTicketValidationException(serviceTicket.getService());
}
}
//通过ticket获取到tgt信息,通过tgt和service获取到当前的认证信息
final TicketGrantingTicket root = serviceTicket.getGrantingTicket().getRoot();
final Authentication authentication = getAuthenticationSatisfiedByPolicy(
root, new ServiceContext(serviceTicket.getService(), registeredService));
final Principal principal = authentication.getPrincipal();
//获取扩展属性信息
final RegisteredServiceAttributeReleasePolicy attributePolicy = registeredService.getAttributeReleasePolicy();
logger.debug("Attribute policy [{}] is associated with service [{}]", attributePolicy, registeredService);
@SuppressWarnings("unchecked")
final Map<String, Object> attributesToRelease = attributePolicy != null
? attributePolicy.getAttributes(principal) : Collections.EMPTY_MAP;
final String principalId = registeredService.getUsernameAttributeProvider().resolveUsername(principal, service);
final Principal modifiedPrincipal = this.principalFactory.createPrincipal(principalId, attributesToRelease);
final AuthenticationBuilder builder = DefaultAuthenticationBuilder.newInstance(authentication);
builder.setPrincipal(modifiedPrincipal);
//产生认证信息,用于返回给CAS单点登录客户端
final Assertion assertion = new ImmutableAssertion(
builder.build(),
serviceTicket.getGrantingTicket().getChainedAuthentications(),
serviceTicket.getService(),
serviceTicket.isFromNewLogin());
//触发ticket校验事件
doPublishEvent(new CasServiceTicketValidatedEvent(this, serviceTicket, assertion));
return assertion;
} finally {
if (serviceTicket.isExpired()) {
this.ticketRegistry.deleteTicket(serviceTicketId);
}
}
}
通过代码的分析主要过程如下:
1、校验service服务
2、根据serviceTicketId获取ServiceTicket
3、判断serviceTicket是否过期
4、判断serviceTicket是否有效
5、认证,得到用户信息(包括扩展属性)
6、生成断言用户认证信息
下面重点分析一下此过程中相关的代码。
serviceTicket.isExpired()调用。此类为AbstractTicket.java在cas-server-core-tickets模块下的org.jasig.cas.ticket包中。在校验中是父类ServiceTicketImpl.java中继承了AbstractTicket类。
*AbstractTicket.java*
@MappedSuperclass
public abstract class AbstractTicket implements Ticket, TicketState {
@Override
public final boolean isExpired() {
final TicketGrantingTicket tgt = getGrantingTicket();
return this.expirationPolicy.isExpired(this)
|| (tgt != null && tgt.isExpired())
|| isExpiredInternal();
}
…………
}
expirationPolicy.isExpired调用,通过调用MultiTimeUseOrTimeoutExpirationPolicy类来实现失效策略。在deployerConfigContext.xml中配置了别名。可以自行去文件中查看。
*MultiTimeUseOrTimeoutExpirationPolicy.java*
@Component("multiTimeUseOrTimeoutExpirationPolicy")
public final class MultiTimeUseOrTimeoutExpirationPolicy extends AbstractCasExpirationPolicy {
/** Serialization support. */
private static final long serialVersionUID = -5704993954986738308L;
/** The time to kill in milliseconds. */
@Value("#{${st.timeToKillInSeconds:10}*1000L}")
private final long timeToKillInMilliSeconds;
/** The maximum number of uses before expiration. */
@Value("${st.numberOfUses:1}")
private final int numberOfUses;
@Override
public boolean isExpired(final TicketState ticketState) {
if (ticketState == null) {
LOGGER.debug("Ticket state is null for {}", this.getClass().getSimpleName());
return true;
}
//获取当前在使用的ticket数量
final long countUses = ticketState.getCountOfUses();
//如果使用数量大于配置文件中配置的数量,直接返回失效
if (countUses >= this.numberOfUses) {
LOGGER.debug("Ticket usage count {} is greater than or equal to {}", countUses, this.numberOfUses);
return true;
}
final long systemTime = System.currentTimeMillis();
final long lastTimeUsed = ticketState.getLastTimeUsed();
final long difference = systemTime - lastTimeUsed;
//判断ticket是否已经失效
if (difference >= this.timeToKillInMilliSeconds) {
LOGGER.debug("Ticket has expired because the difference between current time [{}] "
+ "and ticket time [{}] is greater than or equal to [{}]", systemTime, lastTimeUsed,
this.timeToKillInMilliSeconds);
return true;
}
return false;
}
}
为调试方便或根据需要,可在cas.properties里重新配置过期时间和使用次数:
st.timeToKillInSeconds=10 过期时间(单位为秒)
st.numberOfUses=1 使用次数
ticket的实现类直接使用的是ServiceTicketImpl.java,此类在cas-server-core-tickets模块下的org.jasig.cas.ticket包中。
ServiceTicketImpl.java
@Entity
@Table(name="SERVICETICKET")
@DiscriminatorColumn(name="TYPE")
@DiscriminatorValue(ServiceTicket.PREFIX)
public class ServiceTicketImpl extends AbstractTicket implements ServiceTicket {
@Override
public boolean isValidFor(final Service serviceToValidate) {
//更新serviceTicket相关时间和使用次数
updateState();
return serviceToValidate.matches(this.service);
}
…………
}
先更新ticket的状态,再进行验证,通过service的match进行校验。
更新状态:
*AbstractTicket.java*
@MappedSuperclass
public abstract class AbstractTicket implements Ticket, TicketState {
protected final void updateState() {
this.previousLastTimeUsed = this.lastTimeUsed;
this.lastTimeUsed = System.currentTimeMillis();
this.countOfUses++;
}
更新前一次最后使用时间、最后使用时间和使用次数。
验证,具体的验证在抽象类AbstractWebApplicationService中实现:
AbstractWebApplicationService.java
public abstract class AbstractWebApplicationService implements SingleLogoutService {
@Override
public boolean matches(final Service service) {
try {
//当前的url路径
final String thisUrl = URLDecoder.decode(this.id, "UTF-8");
//CAS服务端保存的service中的路径
final String serviceUrl = URLDecoder.decode(service.getId(), "UTF-8");
logger.trace("Decoded urls and comparing [{}] with [{}]", thisUrl, serviceUrl);
return thisUrl.equalsIgnoreCase(serviceUrl);
} catch (final Exception e) {
logger.error(e.getMessage(), e);
}
return false;
}
比较本次客户端请求的url是否满足CAS服务端配置文件中对于service的要求。
获取到认证的用户信息通过authentication =getAuthenticationSatisfiedByPolicy()的调用:
*AbstractCentralAuthenticationService.java*
public abstract class AbstractCentralAuthenticationService implements CentralAuthenticationService, Serializable,
ApplicationEventPublisherAware {
protected final Authentication getAuthenticationSatisfiedByPolicy(
final TicketGrantingTicket ticket, final ServiceContext context) throws AbstractTicketException {
final ContextualAuthenticationPolicy<ServiceContext> policy =
serviceContextAuthenticationPolicyFactory.createPolicy(context);
//如果策略符合tgt中获取到的用户认证信息,就直接放回tgt中的认证信息
if (policy.isSatisfiedBy(ticket.getAuthentication())) {
return ticket.getAuthentication();
}
//不满足,就去获取tgt追加的相关认证信息(注意:getSupplementalAuthentications()方法在4.2.x版本中已经废弃)
for (final Authentication auth : ticket.getSupplementalAuthentications()) {
if (policy.isSatisfiedBy(auth)) {
return auth;
}
}
throw new UnsatisfiedAuthenticationPolicyException(policy);
}
…… ……
}
通过用户的根tgt,得到用户身份验证信息。
AbstractServiceValidateController.java
@Component("serviceValidateController")
public abstract class AbstractServiceValidateController extends AbstractDelegateController {
@Override
protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
......
final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service); //上一节执行到这里
if (!validateAssertion(request, serviceTicketId, assertion)) {
//校验失败返回
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_TICKET,
CasProtocolConstants.ERROR_CODE_INVALID_TICKET, null, request, service);
}
String proxyIou = null;
if (serviceCredential != null && this.proxyHandler.canHandle(serviceCredential)) {
proxyIou = this.proxyHandler.handle(serviceCredential, proxyGrantingTicketId);
if (StringUtils.isEmpty(proxyIou)) {
return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
new Object[] {serviceCredential.getId()}, request, service);
}
}
onSuccessfulValidation(serviceTicketId, assertion);
logger.debug("Successfully validated service ticket {} for service [{}]", serviceTicketId, service.getId());
return generateSuccessView(assertion, proxyIou, service, proxyGrantingTicketId); //此处校验成功返回
} catch (final AbstractTicketValidationException e) {
final String code = e.getCode();
return generateErrorView(code, code,
new Object[] {serviceTicketId, e.getOriginalService().getId(), service.getId()}, request, service);
} catch (final AbstractTicketException te) {
return generateErrorView(te.getCode(), te.getCode(),
new Object[] {serviceTicketId}, request, service);
} catch (final UnauthorizedProxyingException e) {
return generateErrorView(e.getMessage(), e.getMessage(), new Object[] {service.getId()}, request, service);
} catch (final UnauthorizedServiceException e) {
return generateErrorView(e.getMessage(), e.getMessage(), null, request, service);
}
}
generateErrorView 返回的ModelAndView名称为:cas2ServiceFailureView
generateSuccessView返回的ModelAndView名称为:cas2ServiceSuccessView
需要特别注意的是ticket校验成功后返回的cas2ServiceSuccessView这个视图并不能直接在配置文件中找到对应。这个时候我们可以看看Cas20ResponseView.java这个类是到底如何实现的。
cas2ServiceFailureView对于失败比较简单直接可以在spring配置文件protocolViewsConfiguration.xml中找到对应的配置。
*protocolViewsConfiguration.xml:*
<bean id="cas2ServiceFailureView" class="org.springframework.web.servlet.view.JstlView"
c:url="/WEB-INF/view/jsp/protocol/2.0/casServiceValidationFailure.jsp" />
*casServiceValidationFailure.jsp*
<%@ page session="false" contentType="application/xml; charset=UTF-8" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationFailure code='${code}'>
${fn:escapeXml(description)}
cas:authenticationFailure>
cas:serviceResponse>
把CAS单点登录服务的错误码和错误描述返回到客户端。
Cas20ResponseView.java此类在cas-server-webapp-validation模块下的org.jasig.cas.web.view包中,主要用于最终响应CAS单点登录客户端。
*Cas20ResponseView.java*
public class Cas20ResponseView extends AbstractDelegatingCasView {
//内部类
@Component("cas2ServiceSuccessView")
public static class Success extends Cas20ResponseView {
/**
* Instantiates a new Success.
* @param view the view
*/
@Autowired
public Success(@Qualifier("cas2JstlSuccessView")
final View view) {
super(view);
super.setSuccessResponse(true);
}
}
此时是发布了组件cas2ServiceSuccessView,在发布cas2ServiceSuccessView组件的时候,我们注入组件为cas2JstlSuccessView。
那么我们就要看看casJstlSuccessView具体是怎么配置的。它的配置文件为spring配置文件protocolViewsConfiguration.xml。
*protocolViewsConfiguration.xml:*
<bean id="cas2JstlSuccessView" class="org.springframework.web.servlet.view.JstlView"
c:url="/WEB-INF/view/jsp/protocol/2.0/casServiceValidationSuccess.jsp" />
从配置文件可知对应视图文件为casServiceValidationSuccess.jsp。
casServiceValidationSuccess.jsp
<%@ page session="false" contentType="application/xml; charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>${fn:escapeXml(principal.id)}cas:user>
<c:if test="${not empty pgtIou}">
<cas:proxyGrantingTicket>${pgtIou}cas:proxyGrantingTicket>
c:if>
<c:if test="${fn:length(chainedAuthentications) > 0}">
<cas:proxies>
<c:forEach var="proxy" items="${chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(chainedAuthentications)}" step="1">
<cas:proxy>${fn:escapeXml(proxy.principal.id)}cas:proxy>
c:forEach>
cas:proxies>
c:if>
cas:authenticationSuccess>
cas:serviceResponse>
由于我们现在测试的是非代理模式下的登录跳转因此文件执行后,返回给客户端的内容为:
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
//测试情况下返回的就为用户登录名,还可以有其他的用户扩展信息
<cas:user>casusercas:user>
cas:authenticationSuccess>
cas:serviceResponse>