实现原理:客户端提供一个登录界面,其中action为服务端登录地址,通过改写服务端的登录代码,生成ticket票据,写cookie,重定向到客户端的service地址(如果验证失败同样,并有错误码返回),过滤器判断是否带有ticket,验证通过获取远程用户名。
1、使用源码构建自己工程
版本:
cas-server 3.5.2 (https://github.com/apereo/cas/releases/tag/v3.5.2)
cas-client 3.4.1 (https://github.com/apereo/java-cas-client/releases)
cas-demo 为测试工程
其它的工程都是插件,可以不用(groupId,artifactId,version也可以自己定义)
改造登录页面分为服务器端和客户端,下面分别说明
2、服务器端改造
1)重载AuthenticationViaFormAction 的submit和validatorCode方法
submit 方法去掉了credentials 参数,该参数自己创建。用户名和密码从request 中获得。并且自定义errorCode 作为返回客户端的提示code。
this.centralAuthenticationService.createTicketGrantingTicket(credentials) 方法中实现了验证用户名和密码的方法。
public final String submit(final RequestContext context, final MessageContext messageContext) throws Exception {
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
final Service service = WebUtils.getService(context);
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
//用户名和密码从rquest 中获得
String username = request.getParameter("username");
if(!StringUtils.hasText(username)){
context.getFlowScope().put("errorCode", "required.username");//设置errorCode ,在跳转页面返回给demo 页面提示错误信息
return "error";
}
String password = request.getParameter("password");
if(!StringUtils.hasText(password)){
context.getFlowScope().put("errorCode", "required.password");
return "error";
}
//用户名和密码从rquest 中获得,自己构建对象
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials();
credentials.setUsername(username);
credentials.setPassword(password);
if (StringUtils.hasText(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null) {
try {
final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service, credentials);
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
putWarnCookieIfRequestParameterPresent(context);
return "warn";
} catch (final TicketException e) {
if (e.getCause() != null && AuthenticationException.class.isAssignableFrom(e.getCause().getClass())) {
//populateErrorsInstance(e, messageContext);
context.getFlowScope().put("errorCode", e.getCode());
return "error";
}
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
if (logger.isDebugEnabled()) {
logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e);
}
}
}
try {
//createTicketGrantingTicket 方法验证用户名和密码(实现类SimpleTestUsernamePasswordAuthenticationHandler)
WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
putWarnCookieIfRequestParameterPresent(context);
return "success";
} catch (final TicketException e) {
//populateErrorsInstance(e, messageContext);
context.getFlowScope().put("errorCode", e.getCode());
return "error";
}
}
validatorCode 方法,参考:http://blog.csdn.net/convict_eva/article/details/52848475。只是参数不同
public final String validatorCode(final RequestContext context, final MessageContext messageContext) throws Exception {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
HttpSession session = request.getSession();
String authcode = (String)session.getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
session.removeAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
String submitAuthcode =request.getParameter("authcode");
if(!StringUtils.hasText(submitAuthcode)){
context.getFlowScope().put("errorCode", NullAuthcodeAuthenticationException.CODE);
return "error";
}
if(submitAuthcode.equals(authcode)){
context.getFlowScope().remove("errorCode");
return "success";
}
context.getFlowScope().put("errorCode", BadAuthcodeAuthenticationException.CODE);
return "error";
}
package org.jasig.cas.web.flow;
import org.hibernate.validator.constraints.NotEmpty;
import org.jasig.cas.authentication.principal.Service;
import org.jasig.cas.web.support.ArgumentExtractor;
import org.jasig.cas.web.support.CookieRetrievingCookieGenerator;
import org.jasig.cas.web.support.WebUtils;
import org.springframework.util.StringUtils;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import java.util.List;
public class RemoteLoginAction extends AbstractAction {
/** CookieGenerator for the Warnings. */
@NotNull
private CookieRetrievingCookieGenerator warnCookieGenerator;
/** CookieGenerator for the TicketGrantingTickets. */
@NotNull
private CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator;
/** Extractors for finding the service. */
@NotEmpty
private List argumentExtractors;
/** Boolean to note whether we've set the values on the generators or not. */
private boolean pathPopulated = false;
protected Event doExecute(final RequestContext context) throws Exception {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
if (!this.pathPopulated) {
final String contextPath = context.getExternalContext().getContextPath();
final String cookiePath = StringUtils.hasText(contextPath) ? contextPath : "/";
logger.info("Setting path for cookies to: "
+ cookiePath);
this.warnCookieGenerator.setCookiePath(cookiePath);
this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
this.pathPopulated = true;
}
context.getFlowScope().put("ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
context.getFlowScope().put("warnCookieValue",Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
final Service service = WebUtils.getService(this.argumentExtractors,context);
if (service != null && logger.isDebugEnabled()) {
logger.debug("Placing service in FlowScope: " + service.getId());
}
context.getFlowScope().put("contextPath", request.getContextPath());
context.getFlowScope().put("service", service);
// 客户端必须传递loginUrl参数过来,否则无法确定登陆目标页面。作用:登录失败后重定向页面
if (StringUtils.hasText(request.getParameter("loginUrl"))) {
context.getFlowScope().put("remoteLoginUrl", request.getParameter("loginUrl"));
} else {
request.setAttribute("remoteLoginMessage", "loginUrl parameter must be supported.");
return error();
}
// 若参数包含submit则进行提交,否则进行验证票据
if (StringUtils.hasText(request.getParameter("submit"))) {
return result("submit");
} else {
return result("checkTicketGrantingTicket");
}
}
public void setTicketGrantingTicketCookieGenerator(
final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator) {
this.ticketGrantingTicketCookieGenerator = ticketGrantingTicketCookieGenerator;
}
public void setWarnCookieGenerator(final CookieRetrievingCookieGenerator warnCookieGenerator) {
this.warnCookieGenerator = warnCookieGenerator;
}
public void setArgumentExtractors(
final List argumentExtractors) {
this.argumentExtractors = argumentExtractors;
}
}
package org.jasig.cas.web.flow;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotEmpty;
import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.authentication.principal.Service;
import org.jasig.cas.web.support.ArgumentExtractor;
import org.jasig.cas.web.support.CookieRetrievingCookieGenerator;
import org.jasig.cas.web.support.WebUtils;
import org.springframework.util.StringUtils;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
public class RemoteLogoutAction extends AbstractAction {
@NotNull
private CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator;
@NotNull
private CookieRetrievingCookieGenerator warnCookieGenerator;
@NotNull
private CentralAuthenticationService centralAuthenticationService;
@NotEmpty
private List argumentExtractors;
private boolean pathPopulated = false;
@Override
protected Event doExecute(final RequestContext context) throws Exception {
final HttpServletRequest request = WebUtils
.getHttpServletRequest(context);
final HttpServletResponse response = WebUtils
.getHttpServletResponse(context);
if (!this.pathPopulated) {
final String contextPath = context.getExternalContext().getContextPath();
final String cookiePath = StringUtils.hasText(contextPath) ? contextPath:"/";
logger.info("Setting path for cookies to: " + cookiePath);
this.warnCookieGenerator.setCookiePath(cookiePath);
this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
this.pathPopulated = true;
}
context.getFlowScope().put("ticketGrantingTicketId",this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
context.getFlowScope().put("warnCookieValue",Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
final Service service = WebUtils.getService(this.argumentExtractors,context);
if (service != null && logger.isDebugEnabled()) {
logger.debug("Placing service in FlowScope: " + service.getId());
}
//回跳url 放入FlowScope 中
context.getFlowScope().put("service", service);
context.getFlowScope().put("remoteLoginUrl",request.getParameter("service"));
final String ticketGrantingTicketId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
if (ticketGrantingTicketId != null) {
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);//销毁TGT
this.ticketGrantingTicketCookieGenerator.removeCookie(response);//删除cookie
this.warnCookieGenerator.removeCookie(response);
}
return result("success");
}
public void setTicketGrantingTicketCookieGenerator(
final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator) {
this.ticketGrantingTicketCookieGenerator = ticketGrantingTicketCookieGenerator;
}
public void setWarnCookieGenerator(
final CookieRetrievingCookieGenerator warnCookieGenerator) {
this.warnCookieGenerator = warnCookieGenerator;
}
public void setArgumentExtractors(
final List argumentExtractors) {
this.argumentExtractors = argumentExtractors;
}
public void setCentralAuthenticationService(
final CentralAuthenticationService centralAuthenticationService) {
this.centralAuthenticationService = centralAuthenticationService;
}
}
4)web.xml,添加请求的servlet
cas
/remoteLogin
cas
/remoteLogout
5)在cas-servlet.xml增加请求流程
remoteLoginController
remoteLogoutController
### 配置自定义远程登录失败回调页面
remoteCallbackView.(class)=org.springframework.web.servlet.view.JstlView
remoteCallbackView.url=/WEB-INF/view/jsp/default/ui/remoteCallbackView.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
${remoteLoginMessage}
1)自定义认证过滤器 RemoteAuthenticationFilter
主要改造为:
添加登录页面url属性loginUrl,验证失败时可跳转到此url
拦截验证要过滤掉此url
页面跳转url修改
package org.jasig.cas.client.authentication;
import org.jasig.cas.client.Protocol;
import org.jasig.cas.client.configuration.ConfigurationKey;
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.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
public class RemoteAuthenticationFilter extends AbstractCasFilter {
/**
* The URL to the CAS Server login.
*/
private String casServerLoginUrl;
/**
* Whether to send the renew request or not.
*/
private boolean renew = false;
/**
* 本地登陆页面URL,登录失败重定向的页面。
*/
private String localLoginUrl;
/**
* Whether to send the gateway request or not.
*/
private boolean gateway = false;
private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
private AuthenticationRedirectStrategy authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy();
private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = null;
private static final Map> PATTERN_MATCHER_TYPES =
new HashMap>();
static {
PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class);
PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class);
PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class);
}
public RemoteAuthenticationFilter() {
this(Protocol.CAS2);
}
protected RemoteAuthenticationFilter(final Protocol protocol) {
super(protocol);
}
protected void initInternal(final FilterConfig filterConfig) throws ServletException {
if (!isIgnoreInitConfiguration()) {
super.initInternal(filterConfig);
setCasServerLoginUrl(getString(ConfigurationKeys.CAS_SERVER_LOGIN_URL));
setLocalLoginUrl(getString(new ConfigurationKey("localLoginUrl", null)));//加载配置
setRenew(getBoolean(ConfigurationKeys.RENEW));
setGateway(getBoolean(ConfigurationKeys.GATEWAY));
final String ignorePattern = getString(ConfigurationKeys.IGNORE_PATTERN);//忽略拦截url配置
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);
}
}
}
public void init() {
super.init();
CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
}
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;
//不拦截规则,添加如果访问路径为localLoginUrl且带有validated参数则跳过
URL url = new URL(localLoginUrl);
if (isRequestUrlExcluded(request) || request.getRequestURI().endsWith(url.getPath())) {
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;
}
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);
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
// 加入localLoginUrl
urlToRedirectTo += (urlToRedirectTo.contains("?") ? "&" : "?") + "loginUrl=" + URLEncoder.encode(localLoginUrl, "utf-8");
logger.debug("redirecting to \"{}\"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}
public final void setRenew(final boolean renew) {
this.renew = renew;
}
public final void setGateway(final boolean gateway) {
this.gateway = gateway;
}
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 String getLocalLoginUrl() {
return localLoginUrl;
}
public void setLocalLoginUrl(String localLoginUrl) {
this.localLoginUrl = localLoginUrl;
}
}
修改:Cas20ServiceTicketValidator parseResponseFromServer 方法:
//源码
// final String error = XmlUtils.getTextForElement(response, "authenticationFailure");
//
// if (CommonUtils.isNotBlank(error)) {
// throw new TicketValidationException(error);
// }
//验证失败,返回自定义message,在AbstractTicketValidationFilter 中捕获,做为特殊的处理
final String error = XmlUtils.getTextForElement(response,"authenticationFailure");
if (CommonUtils.isNotBlank(error)) {
throw new TicketValidationException("convicteva-TicketValidation-fail");
}
修改 AbstractTicketValidationFilter 的doFilter 方法:
} catch (final TicketValidationException e) {
//如果是票据验证失败,就跳转到请求的url
if(e.getMessage().equalsIgnoreCase("convicteva-TicketValidation-fail")){
response.sendRedirect(request.getRequestURL().toString());
return;
}
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
logger.debug(e.getMessage(), e);
onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(e);
}
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
return;
}
org.jasig.cas.client
cas-client-core
3.4.1
2)web.xml 配置
org.jasig.cas.client.session.SingleSignOutHttpSessionListener
CAS Single Sign Out Filter
org.jasig.cas.client.session.SingleSignOutFilter
casServerUrlPrefix
https://sso.convicteva.com:8443/cas
CAS Single Sign Out Filter
/*
CASFilter
org.jasig.cas.client.authentication.RemoteAuthenticationFilter
casServerLoginUrl
https://sso.convicteva.com:8443/cas/remoteLogin
serverName
http://www.convicteva.com:8888/demo/
localLoginUrl
http://www.convicteva.com:8888/demo/login/page
CASFilter
/*
CAS_Validation_Filter
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
casServerUrlPrefix
https://sso.convicteva.com:8443/cas
serverName
http://www.convicteva.com:8888/demo
CAS_Validation_Filter
/*
HttpServletRequestWrapperFilter
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
HttpServletRequestWrapperFilter
/*
assertionThreadLocalFilter
org.jasig.cas.client.util.AssertionThreadLocalFilter
assertionThreadLocalFilter
/*
IndexServlet
com.convicteva.sso.servlet.IndexServlet
IndexServlet
/
loginServlet
com.convicteva.sso.servlet.LoginServlet
loginServlet
/login/page
<%@ page language="java" pageEncoding="UTF-8"
contentType="text/html; charset=UTF-8"%>
登录
" id="errorCode"/>
<%@ page language="java" pageEncoding="UTF-8"
contentType="text/html; charset=UTF-8"%>
登录成功
您好:<%=request.getRemoteUser()%>
登录页面(www.convicteva.com:8888/demo/login/page):
登录成功:
登录成功后,访问cas,也是登录成功状态
修改代码:http://download.csdn.net/detail/convict_eva/9660148