CAS 之自定义登录页实践

2023-05-16

[size=large][b]1. 动机[/b][/size]
用过 CAS 的人都知道 CAS-Server端是单独部署的,作为一个纯粹的认证中心。在用户每次登录时,都需要进入CAS-Server的登录页填写用户名和密码登录,但是如果存在多个子应用系统时,它们可能都有相应风格的登录页面,我们希望直接在子系统中登录成功,而不是每次都要跳转到CAS的登录页去登录。

[size=large][b]2. 开始分析问题[/b][/size]
其实仔细想一想,为什么不能直接在子系统中将参数提交至 cas/login 进行登录呢? 于是便找到了CAS在登录认证时主要参数说明:
service [OPTIONAL] 登录成功后重定向的URL地址;
username [REQUIRED] 登录用户名;
password [REQUIRED] 登录密码;
lt [REQUIRED] 登录令牌;
主要有四个参数,其中的三个参数倒好说,最关键的就是 lt , 据官方说明该参数是login ticket id, 主要是在登录前产生的一个唯一的“登录门票”,然后提交登录后会先取得"门票",确定其有效性后才进行用户名和密码的校验,否则直接重定向至 cas/login 页。
于是,便打开CAS-Server的登录页,发现其每次刷新都会产生一个 lt, 其实就是 Spring WebFlow 中的 flowExecutionKey值。 那么问题的关键就在于在子系统中如何获取 lt 也就是登录的ticket?

[size=large][b]3. 可能的解决方案[/b][/size]
一般对于获取登录ticket的解决方案可能大多数人都会提到两种方法:
[list]
[*] AJAX: 熟悉 Ajax 的可能都知道,它的请求方式是严格按照沙箱安全模型机制的,严格情况下会存在跨域安全问题。
[*] IFrames: 这也是早期的 ajax 实现方式,在页面中嵌入一个隐藏的IFrame,然后通过表单提交到该iframe来实现不刷新提交,不过使用这种方式同样会带来两个问题:
a. 登录成功之后如何摆脱登录后的IFrame呢?如果成功登录可能会导致整个页面重定向,当然你能在form中使
用属性target="_parent",使之弹出,那么你如何在父页面显示错误信息呢?
b. 你可能会受到布局的限止(不允许或不支持iframe)
[/list] 对于以上两种方案,并非说不能实现,只是说对于一个灵活的登录系统来说仍然还是会存在一定的局限性的,我们坚信能有更好的方案来解决这个问题。

[size=large][b]4. 通过JS重定向来获取login ticket (lt)[/b][/size]
当第一次进入子系统的登录页时,通过 JS 进行redirect到cas/login?get-lt=true获取login ticket,然后在该login中的 flow 中检查是否包含get-lt=true的参数,如果是的话则跳转到lt生成页,生成后,并将lt作为该redirect url 中的参数连接,如 remote-login.html?lt=e1s1,然后子系统再通过JS解析当前URL并从参数中取得该lt的值放置登录表单中,即完成 lt 的获取工作。其中进行了两次 redirect 的操作。

[size=large][b]5. 开始实践 [/b][/size]
首先,在我们的子系统中应该有一个登录页面,通过输入用户名和密码提交至cas认证中心。不过前提是先要获取到 login tickt id. 也就是说当用户第一次进入子系统的登录页面时,在该页面中会通过js跳转到 cas/login 中的获取login ticket. 在 cas/login 的 flow 中先会判断请求的参数中是否包含了 get-lt 的参数。
在cas的 login flow 中加入 ProvideLoginTicketAction 的流,主要用于判断该请求是否是来获取 lt,在cas-server端声明获取 login ticket action 类:
[b]com.denger.sso.web.ProvideLoginTicketAction[/b]
/**
* Opens up the CAS web flow to allow external retrieval of a login ticket.
*
* @author denger
*/
public class ProvideLoginTicketAction extends AbstractAction{

@Override
protected Event doExecute(RequestContext context) throws Exception {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);

if (request.getParameter("get-lt") != null && request.getParameter("get-lt").equalsIgnoreCase("true")) {
return result("loginTicketRequested");
}
return result("continue");
}

}
// 如果参数中包含 get-lt 参数,则返回 loginTicketRequested 执行流,并跳转至 loginTicket 生成页,否则 则跳过该flow,并按照原始login的流程来执行。

并且将该 action 声明在 cas-servlet.xml 中:
<bean id="provideLoginTicketAction" class="com.denger.sso.web.ProvideLoginTicketAction" />     



还需要定义 loginTicket 的生成页也就是当返回 loginTicketRequested 的 view:
[b]viewRedirectToRequestor.jsp[/b]
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page import="com.denger.sso.util.CasUtility"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%
String separator = "";
// 需要输入 login-at 参数,当生成lt后或登录失败后则重新跳转至 原登录页,并传入参数 lt 和 error_message
String referer = request.getParameter("login-at");

referer = CasUtility.resetUrl(referer);
if (referer != null && referer.length() > 0) {
separator = (referer.indexOf("?") > -1) ? "&" : "?";
%>
<html>
<title>cas get login ticket</title>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script>
var redirectURL = "<%=referer + separator%>lt=${flowExecutionKey}";
<spring:hasBindErrors name="credentials">
var errorMsg = '<c:forEach var="error" items="${errors.allErrors}"><spring:message code="${error.code}" text="${error.defaultMessage}" /></c:forEach>';
redirectURL += '&error_message=' + encodeURIComponent (errorMsg);
</spring:hasBindErrors>
window.location.href = redirectURL;
</script>
</head>
<body></body>
</html>
<%
} else {
%>
<script>window.location.href = "/member/login";</script>
<%
}
%>

并且需要将该 jsp 声明在 default._views.properites 中:
### Redirect with login ticket view
casRedirectToRequestorView.(class)=org.springframework.web.servlet.view.JstlView
casRedirectToRequestorView.url=/WEB-INF/view/jsp/default/ui/viewRedirectToRequestor.jsp


相关[b] com.denger.sso.util.CasUtility[/b] 代码:
public class CasUtility {

/**
* Removes the previously attached GET parameters "lt" and "error_message"
* to be able to send new ones.
*
* @param casUrl
* @return
*/
public static String resetUrl(String casUrl) {
String cleanedUrl;
String[] paramsToBeRemoved = new String[] { "lt", "error_message", "get-lt" };
cleanedUrl = removeHttpGetParameters(casUrl, paramsToBeRemoved);
return cleanedUrl;
}

/**
* Removes selected HTTP GET parameters from a given URL
*
* @param casUrl
* @param paramsToBeRemoved
* @return
*/
public static String removeHttpGetParameters(String casUrl,
String[] paramsToBeRemoved) {
String cleanedUrl = casUrl;
if (casUrl != null) {
// check if there is any query string at all
if (casUrl.indexOf("?") == -1) {
return casUrl;
} else {
// determine the start and end position of the parameters to be
// removed
int startPosition, endPosition;
boolean containsOneOfTheUnwantedParams = false;
for (String paramToBeErased : paramsToBeRemoved) {
startPosition = -1;
endPosition = -1;
if (cleanedUrl.indexOf("?" + paramToBeErased + "=") > -1) {
startPosition = cleanedUrl.indexOf("?"
+ paramToBeErased + "=") + 1;
} else if (cleanedUrl.indexOf("&" + paramToBeErased + "=") > -1) {
startPosition = cleanedUrl.indexOf("&"
+ paramToBeErased + "=") + 1;
}
if (startPosition > -1) {
int temp = cleanedUrl.indexOf("&", startPosition);
endPosition = (temp > -1) ? temp + 1 : cleanedUrl
.length();
// remove that parameter, leaving the rest untouched
cleanedUrl = cleanedUrl.substring(0, startPosition)
+ cleanedUrl.substring(endPosition);
containsOneOfTheUnwantedParams = true;
}
}

// wenn nur noch das Fragezeichen vom query string übrig oder am
// schluss ein "&", dann auch dieses entfernen
if (cleanedUrl.endsWith("?") || cleanedUrl.endsWith("&")) {
cleanedUrl = cleanedUrl.substring(0,
cleanedUrl.length() - 1);
}
// parameter mehrfach angegeben wurde...
if (!containsOneOfTheUnwantedParams)
return casUrl;
else
cleanedUrl = removeHttpGetParameters(cleanedUrl,
paramsToBeRemoved);
}
}
return cleanedUrl;
}


还有一处需要调整的地方就是当用户名和密码验证失败后,应该重新返回至子系统登录页,也就是 login-at 参数值,此时同样需要重新生成 login ticket。 于是找到 cas 登录验证处理 action :[b]org.jasig.cas.web.flow.AuthenticationViaFormAction[/b] 修改 submit方法 中代码下如:

try {
WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
putWarnCookieIfRequestParameterPresent(context);
return "success";
} catch (final TicketException e) {
populateErrorsInstance(e, messageContext);
// 当验证失败后,判断参数中是否获否 login-at 参数,如果包含的话则跳转至 login ticket 获取页
String referer = context.getRequestParameters().get("login-at");
if (!org.apache.commons.lang.StringUtils.isBlank(referer)) {
return "errorForRemoteRequestor";
}
return "error";
}



接下来要做的就是将该action 的处理加入到 login-webflow.xml 请求流中:
<on-start>
<evaluate expression="initialFlowSetupAction" />
</on-start>
<!-- 添加如下配置 :-->
<action-state id="provideLoginTicket">
<evaluate expression="provideLoginTicketAction"/>
<transition on="loginTicketRequested" to ="viewRedirectToRequestor" />
<transition on="continue" to="ticketGrantingTicketExistsCheck" />
</action-state>

<view-state id="viewRedirectToRequestor" view="casRedirectToRequestorView" model="credentials">
<var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
<binder>
<binding property="username" />
<binding property="password" />
</binder>
<on-entry>
<set name="viewScope.commandName" value="'credentials'" />
</on-entry>
<transition on="submit" bind="true" validate="true" to="realSubmit">
<set name="flowScope.credentials" value="credentials" />
<evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
</transition>
</view-state>
<!---添加结束处 --->
<decision-state id="ticketGrantingTicketExistsCheck">
<if test="flowScope.ticketGrantingTicketId neq null" then="hasServiceCheck" else="gatewayRequestCheck" />
</decision-state>

<!-- ..... 省略中间代码 ...-->

<action-state id="realSubmit">
<evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />
<transition on="warn" to="warn" />
<transition on="success" to="sendTicketGrantingTicket" />
<transition on="error" to="viewLoginForm" />
<!--加入该transition , 当验证失败之后重新获取login ticket -->
<transition on="errorForRemoteRequestor" to="viewRedirectToRequestor" />
</action-state>


好了,至此,对server端的调整基本上已经大功告成了,现在开始写一个测试远程登录的 html:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test remote Login using JS</title>
<script type="text/javascript">
function prepareLoginForm() {
$('myLoginForm').action = casLoginURL;
$("lt").value = loginTicket;
}

function checkForLoginTicket() {
var loginTicketProvided = false;
var query = '';
casLoginURL = 'http://192.168.6.1:8080/member/login';
thisPageURL = 'http://192.168.6.1:8080/member/test-login.html';
casLoginURL += '?login-at=' + encodeURIComponent (thisPageURL);

query = window.location.search;
query = query.substr (1);


var param = new Array();
//var value = new Array();
var temp = new Array();
param = query.split ('&');

i = 0;
// 开始获取当前 url 的参数,获到 lt 和 error_message。
while (param[i]) {
temp = param[i].split ('=');
if (temp[0] == 'lt') {
loginTicket = temp[1];
loginTicketProvided = true;
}
if (temp[0] == 'error_message') {
error = temp[1];
}
i++;
}
// 判断是否已经获取到 lt 参数,如果未获取到则跳转至 cas/login 页,并且带上请求参数 get-lt=true。 第一次进该页面时会进行一次跳转
if (!loginTicketProvided) {
location.href = casLoginURL + '&get-lt=true';
}
}

var $ = function(id){
return document.getElementById(id);
}


checkForLoginTicket();
onload = prepareLoginForm;
</script>
</head>
<body>
<h2>Test remote Login using JS</h2>
<form id="myLoginForm" action="" method="post">
<input type="hidden" name="_eventId" value="submit" />
<table>
<tr>
<td id="txt_error" colspan="2">

<script type="text/javascript" language="javascript">
<!--
if ( error ) {

error = decodeURIComponent (error);

document.write (error);
}
//-->
</script>

</td>
</tr>
<tr>
<td>Username:</td>
<td><input type="text" value="" name="username" ></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="text" value="" name="password" ></td>
</tr>
<tr>
<td>Login Ticket:</td>
<td><input type="text" name="lt" id="lt" value=""></td>
</tr>
<tr>
<td>Service:</td>
<td><input type="text" name="service" value="http://www.google.com.hk"></td>
</tr>
<tr>
<td align="right" colspan="2"><input type="submit" /></td>
</tr>
</table>
</form>
</body>
</html>


开始测试,直接访问:http://192.168.6.1:8080/member/test-login.html 发现进行了二次重定向,进入该页面 js 未发现 lt 参数,于是重定向到 http://192.168.6.1:8080/member/login?login-at=http://192.168.6.1:8080/member/test-login.html &get-lt=true ,然后又从该页重定向到 http://192.168.6.1:8080/member/test-login.html?lt=e1s1 ,可以发现,其中的 lt 就是我们所需要的 login ticket参数。


[size=large][b]6. 不足之处 [/b][/size]
1. 可以发现,每次用户访问 登录页面时都要进行两次重定向的操作,虽然很快,但是在有些情况仍然能看到登录页面闪了一下。 当然这也是有办法可以解决的!
2. 可以发现,当登录失败之后,会将错误信息以参数的方式进行传递,看上去这并非专业做法。可以定义一些错误标识,比如 1 是用户名或密码错误之类的。

PS:参考:https://wiki.jasig.org/display/CAS/Using+CAS+without+the+Login+Screen 如有不足之处,欢迎指正~
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CAS 之自定义登录页实践 的相关文章

  • cas mysql_CAS服务器动态验证,CAS使用MySQL数据库验证-(二)

    步骤 一 搭建CAS服务器 gt 二 修改application properties 静态验证的配置 xff0c 需要注释 xff1a CAS Authentication Credentials cas authn accept use
  • 使用cas-overlay-template搭建cas服务器

    背景 在多服务统一帐号的应用集中 xff0c 单点登录是必不可少的 CAS就是成熟的单点登录框架之一 Github地址 https github com apereo cas 现在我们就通过一系列快速简单的构建方式实现一个简单的单点登录系统
  • shiro-cas------本地配置cas为HTTPS登录

    上一篇 xff1a shiro cas 搭建基础cas服务器 解决上图所提示的问题 xff08 在本地 xff09 xff0c 需要配置https请求 首先给这个服务起个域名 xff1a shiro sso com 配置到本地的host文件
  • CAS服务(5.3)使用mysql验证

    CAS服务使用mysql验证 一 添加相关依赖 在pom文件里添加下面的依赖 这里cas的版本是5 3 14 lt dependency gt lt groupId gt org apereo cas lt groupId gt lt ar
  • CAS server6.x配置与部署笔记

    由于最近将公司的springboot升级到了springboot2 xff0c 而5 x的cas server使用的是springboot1 xff0c 因此为了更方便的开发cas server xff0c 将cas server升级为6
  • 实战:CAS搭建

    一 CAS服务器的搭建 1 下载CAS服务器的源码 xff0c 我下载的是CAS Maven WAR Overlay 的分支4 2 X版本 注 xff1a 如若不想了解查找下载地方过程 xff0c 请直接参见 xff08 3 xff09 的
  • cas + tomcat 配置步骤详细笔记(一)

    首先需要准备资源如下 xff1a cas server 4 0 0 release zip xff0c cas client 2 0 11 zip xff0c apache tomcat 6 0 29 下面操作在dos下操作 xff08 开
  • CAS 之自定义登录页实践

    size 61 large b 1 动机 b size 用过 CAS 的人都知道 CAS Server端是单独部署的 xff0c 作为一个纯粹的认证中心 在用户每次登录时 xff0c 都需要进入CAS Server的登录页填写用户名和密码登
  • springboot整合cas

    1 创建springboot项目后在pom中添加 span class token tag span class token tag span class token punctuation lt span dependency span
  • 使用cas-overlay-template 6.2服务部署到整合cas-client

    1 什么sso是单点登录 单点登录 xff08 Single Sign On xff09 xff0c 简称为 SSO xff0c 是比较流行的企业业务整合的解决方案之一 SSO的定义是在多个应用系统中 xff0c 用户只需要登录一次就可以访
  • shiro-cas------自定义登录页面

    我的自定义登录页 xff08 需要登录页面的 xff0c 推荐给你们一个登陆页面地址 xff09 我的项目结构 xff1a 学习过程参考官方文档https apereo github io cas 5 3 x installation Us
  • CAS 未认证授权服务 不允许使用CAS来认证您访问的目标应用

    资源环境 CAS服务端 CAS 5 3 2 服务端 CAS客户端 Spring Boot CAS 客户端 访问过程 1 CAS 客户端访问本地项目指定端口 http localhost 9100 cas index 2 CAS 客户端调整至
  • Cas5.3服务器集成DM8 达梦数据库

    DM8达梦数据库相关准备 1 安装DM8达梦数据库并安装相关数据库实例 省略一千字 2 新建ucas auth user表 并增加相关用户条记录 DROP TABLE IF EXISTS ucas auth user CREATE TABL
  • cas 编译安装依赖时提示: Failure to find net.shibboleth.tool:xmlsectool:jar:2.0.0

    错误信息 Could not resolve dependencies for project org apereo cas cas overlay war 1 0 Failure to find net shibboleth tool x
  • cas5.3.2单点登录-Cas Server开启Oauth2.0协议(二十)

    原文地址 转载请注明出处 https blog csdn net qq 34021712 article details 82290876 王赛超 学习Cas这么久了 一直在按照CAS自身的协议接入 Cas的强大在于有官方的插件 可以支持其
  • CAS AD LDAP 32 错误

    当我尝试使用 CAS 登录时 我看到了这一点 CAS 通过 LDAP 对 AD 进行身份验证 SEVERE Servlet service for servlet cas threw exception javax naming NameN
  • 通过 JMH 测量 sun.misc.Unsafe.compareAndSwap 中的奇怪行为

    我决定使用不同的锁定策略来测量增量 并为此使用 JMH 我使用 JMH 来检查吞吐量和平均时间 并使用简单的自定义测试来检查正确性 有六种策略 原子数 读写锁定计数 与易失性同步 无易失性的同步块 sun misc Unsafe compa
  • 如何将CAS认证与Spring Security集成?

    我已将 spring security 集成到我的项目中 并且之前使用 hibernate 验证用户详细信息 现在我必须使用 CAS 来完成它 这是我当前的 Spring security xml
  • Spring Security with CAS 跳过会话固定保护

    我有一个使用 spring security 和 CAS spring 3 0 5 cas 3 4 5 的应用程序 但是当我登录时 会话 ID 没有改变 当我登录时CasAuthenticationFilter执行身份验证 如果身份验证成功
  • CAS 注销和 cookie 消除

    我刚刚制作了一个 HelloWorld servlet 并在其上实现了 CAS 我能够毫无问题地登录 并且 CAS 在我的浏览器中设置 3 个 cookie CASGT 并为 cas 设置 2 个 JSESSIONID 1 另一个为 hel

随机推荐