CAS项目登录流程介绍(二)
上一篇介绍了cas的登陆流程,因为cas属于第三方登陆系统,用户在经过cas认证后会跳会原来用户访问的资源。
所以就会存在外部系统会和cas系统进行一系列的授权确认,保证用户是有权限访问现有资源的。
在应用端配置一些cas相关的filter
<bean id="casSingleSignOutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter" />
<bean id="casAuthenticationFilter" class="org.jasig.cas.client.authentication.AuthenticationFilter">
<dynamic:property-config name="casServerLoginUrl" param-key="${config.cas.login.url}" default-value="http://${domain}/cas/login" />
<dynamic:property-config name="serverName" param-key="${config.server.url}" default-value="http://${domain}" />
</bean>
<bean id="casValidationFilter" class="org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter">
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator" p:encoding="UTF-8">
<dynamic:constructor-config name="casServerUrlPrefix"
param-key="${config.cas.url}" default-value="http://${domain}/cas" />
</bean>
</property>
<dynamic:property-config name="serverName" param-key="${config.server.url}" default-value="http://${domain}" />
<dynamic:property-config name="casServerLoginUrl" param-key="${config.cas.login.url}" default-value="http://${domain}/cas/login" />
</bean>
<bean id="casAssertionThreadLocalFilter" class="org.jasig.cas.client.util.AssertionThreadLocalFilter" />
下面我来一个一个解释下这些filter的工作内容。
1.casSingleSignOutFilter
这个是单点登出功能,当用户在他处登出的时候就会在cas内部注销一切票据(tgt,st),而st和每个外部应用有关。所以登出时,cas会向所有外部应用发出一段saml,告诉这些应用该用户的票据失效了。
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
if (handler.isTokenRequest(request)) {
handler.recordSession(request);
} else if (handler.isLogoutRequest(request)) {
handler.destroySession(request);
// Do not continue up filter chain
return;
} else {
log.trace("Ignoring URI " + request.getRequestURI());
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void recordSession(final HttpServletRequest request) {
final HttpSession session = request.getSession(true);
final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName);
if (log.isDebugEnabled()) {
log.debug("Recording session for token " + token);
}
try {
this.sessionMappingStorage.removeBySessionById(session.getId());
} catch (final Exception e) {
// ignore if the session is already marked as invalid. Nothing we can do!
}
sessionMappingStorage.addSessionById(token, session);
}
public void destroySession(final HttpServletRequest request) {
final String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName);
if (log.isTraceEnabled()) {
log.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) {
String sessionID = session.getId();
if (log.isDebugEnabled()) {
log.debug ("Invalidating session [" + sessionID + "] for token [" + token + "]");
}
try {
session.invalidate();
} catch (final IllegalStateException e) {
log.debug("Error invalidating session.", e);
}
}
}
}
当用户的token不是登出请求时,调用recordSession,登出调用destroySession。我们可以看到其内部使用一个SessionMappingStorage对象维护session和st的关系。
而无论登出还是其他行为,cas等会带上st参数与外部应用发生交互。当登出是,这个filter做两件式意识移除SessionMappingStorage相应的session,而是将这个session失效。
2.casAuthenticationFilter
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;
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 = CommonUtils.safeGetParameter(request,getArtifactParameterName());
final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
filterChain.doFilter(request, response);
return;
}
final String modifiedServiceUrl;
log.debug("no ticket and no assertion found");
if (this.gateway) {
log.debug("setting gateway attribute in session");
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
if (log.isDebugEnabled()) {
log.debug("Constructed service url: " + modifiedServiceUrl);
}
final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
if (log.isDebugEnabled()) {
log.debug("redirecting to \"" + urlToRedirectTo + "\"");
}
response.sendRedirect(urlToRedirectTo);
}
这个流程还是分成两部分。如果有st则进入下一个流程。如果无则重定向到cas/login服务,前面对这个服务介绍过是个weblfow,在拥有tgt情况下会生成st跳回,如果没有tgt那就必须输入用户名密码再跳回。
在跳回后url一定会用st参数所以,直接进入下一个filter
3.casValidationFilter
这是验证st的过程,客户端拿到st需要确保这个st是否是cas正确签发的。
这里直接贴核心方法dofilter,在他的父类中
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;
final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());
if (CommonUtils.isNotBlank(ticket)) {
if (log.isDebugEnabled()) {
log.debug("Attempting to validate ticket: " + ticket);
}
try {
final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));
if (log.isDebugEnabled()) {
log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName());
}
request.setAttribute(CONST_CAS_ASSERTION, assertion);
if (this.useSession) {
request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
}
onSuccessfulValidation(request, response, assertion);
if (this.redirectAfterValidation) {
log. debug("Redirecting after successful ticket validation.");
response.sendRedirect(constructServiceUrl(request, response));
return;
}....................................
这里客户端会将st作为参数,访问cas的cas/serviceValidate服务,返回assertion。如果不正确会抛出异常,如果正确则在request放入CONST_CAS_ASSERTION参数。只要参数存在,用户一次的访问行为将不再需要认证。
4.casAssertionThreadLocalFilter
这个filter做的事情很简单,将request中的assertion拿出来,放入一个threadlocal变量中,供其他的filter使用,最后再将filter清楚。
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpSession session = request.getSession(false);
final Assertion assertion = (Assertion) (session == null ? request.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
try {
AssertionHolder.setAssertion(assertion);
filterChain.doFilter(servletRequest, servletResponse);
} finally {
AssertionHolder.clear();
}
}