开源的 CAS 单点登录本身已经提供了一个统一的登录页面,也就是我们配置好之后,所有的没有登录的请求都会拦截到自带的登录页面,我们可以根据自己的需求来改造这个页面,但是,需求是多种多样的并且有时候总是更改的,我就碰到过这种情况,有时就会让人很崩溃。
很多时候,我们都是接手别人的代码,而且大多都是比较成熟,并且有一定时间的项目,我们总是在前人的基础进行修修补补。就拿单点登录来说,我相信大多数项目都有一套自己比较成熟的登录过滤机制,如果我们集成单点登录功能,也是使用我们以前的登录页面,这时候我们就要对 CAS 进行改造。
在我第一次接手这个需求之前,我在网上查找了很多资料,大多数都是教我们将 CAS 集成进项目,我在集成的过程发现,因为我的项目本来就是一个老项目,使用的是 Spring Security 来进行登录过滤,要将原来的登录逻辑改成 CAS 的,这个比较难,往往会碰到很多 Bug,代码的耦合度有点高,牵一发而动全身,不胜其烦。所以,我感觉这个方法并不适合一个老项目。后来,我想了一个办法,另外单独写了一个登录的服务,所有的请求都拦截到这个新的服务,然后根据访问的链接,验证成功后再跳转回去,这样做的好处有,可扩展性比较强,以后如果有新的项目要加进来,只需添加一些配置即可,缺点就是需要单独启动一个服务,这样开销较大,要是这个服务崩,其他的也登录不进去了。
这时,我们就要发挥搜索引擎的巨大优势和身为程序员的主观能动性了,在我一通搜索之后,我终于找到了一个比较合适的解决方案了,那就是改造 CAS,自定义登录页面。
方法
将 CAS 原有的过滤器改为我们自己的过滤器,新增一个 remoteLogin 类,将 CAS 原有的处理登录的类换成我们自己的,其他的校验还是 CAS 自己的,新增一个校验登录失败的页面,将错误信息返回给客户端。我们需要修改客户端和 CAS 认证服务器端两处。
修改客户端
1. 新增 RemoteAuthenticationFilter.java,可以自定义路径,web.xml 文件能够正确引用即可。
public class RemoteAuthenticationFilter extends AbstractCasFilter {
public static final String CONST_CAS_GATEWAY = "_const_cas_gateway_";
/**.
* 本地登陆页面URL.
*/
private String localLoginUrl;
/**.
* 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;
protected void initInternal(final FilterConfig filterConfig) throws ServletException {
super.initInternal(filterConfig);
setCasServerLoginUrl(getPropertyFromInitParams(filterConfig, "casServerLoginUrl", null));
log.trace("Loaded CasServerLoginUrl parameter: " + this.casServerLoginUrl);
setLocalLoginUrl(getPropertyFromInitParams(filterConfig, "localLoginUrl", null));
log.trace("Loaded LocalLoginUrl parameter: " + this.localLoginUrl);
setRenew(Boolean.parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false")));
log.trace("Loaded renew parameter: " + this.renew);
setGateway(Boolean.parseBoolean(getPropertyFromInitParams(filterConfig, "gateway", "false")));
log.trace("Loaded gateway parameter: " + this.gateway);
}
public void init() {
super.init();
CommonUtils.assertNotNull(this.localLoginUrl, "localLoginUrl cannot be null.");
CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final HttpSession session = request.getSession(false);
final String ticket = request.getParameter(getArtifactParameterName());
final Assertion assertion = session != null
? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
final boolean wasGatewayed = session != null && session.getAttribute(CONST_CAS_GATEWAY) != null;
// 如果访问路径为localLoginUrl且带有validated参数则跳过
URL url = new URL(localLoginUrl);
final boolean isValidatedLocalLoginUrl = request.getRequestURI().endsWith(url.getPath())
&& CommonUtils.isNotBlank(request.getParameter("validated"));
if (!isValidatedLocalLoginUrl && CommonUtils.isBlank(ticket) && assertion == null
&& !wasGatewayed) {
log.debug("no ticket and no assertion found");
if (this.gateway) {
log.debug("setting gateway attribute in session");
request.getSession(true).setAttribute(CONST_CAS_GATEWAY, "yes");
}
final String serviceUrl = constructServiceUrl(request, response);
if (log.isDebugEnabled()) {
log.debug("Constructed service url: " + serviceUrl);
}
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
getServiceParameterName(), serviceUrl, this.renew, this.gateway);
// 加入localLoginUrl
urlToRedirectTo += (urlToRedirectTo.contains("?") ? "&" : "?") + "loginUrl="
+ URLEncoder.encode(localLoginUrl, "utf-8");
if (log.isDebugEnabled()) {
log.debug("redirecting to '" + urlToRedirectTo + "'");
}
response.sendRedirect(urlToRedirectTo);
return;
}
if (session != null) {
log.debug("removing gateway attribute from session");
session.setAttribute(CONST_CAS_GATEWAY, null);
}
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 final void setCasServerLoginUrl(final String casServerLoginUrl) {
this.casServerLoginUrl = casServerLoginUrl;
}
public final void setLocalLoginUrl(String localLoginUrl) {
this.localLoginUrl = localLoginUrl;
}
}
2. web.xml ,将原来的过滤器换成上面的过滤器,然后配置以下配置。如果要实现单点登出功能,只需将单点登出的配置放到最上面,使用 CAS 自带的单点登出功能即可。
<!-- 单点登出 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<!-- 只需改成 CAS 认证服务器的地址,例如 -->
<param-value>http://127.0.0.1:8080/cas</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-na