首页
Web开发
Windows程序
编程语言
数据库
移动开发
系统相关
微信
其他好文
会员
首页
>
其他好文
> 详细
CAS 之自定义登录页实践
时间:
2015-06-30 18:49:10
阅读:
293
评论:
0
收藏:
0
[点我收藏+]
标签:
1. 动机
用过 CAS 的人都知道 CAS-Server端是单独部署的,作为一个纯粹的认证中心。在用户每次登录时,都需要进入CAS-Server的登录页填写用户名和密码登录,但是如果存在多个子应用系统时,它们可能都有相应风格的登录页面,我们希望直接在子系统中登录成功,而不是每次都要跳转到CAS的登录页去登录。
2. 开始分析问题
其实仔细想一想,为什么不能直接在子系统中将参数提交至 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?
3. 可能的解决方案
一般对于获取登录ticket的解决方案可能大多数人都会提到两种方法:
AJAX: 熟悉 Ajax 的可能都知道,它的请求方式是严格按照沙箱安全模型机制的,严格情况下会存在跨域安全问题。
IFrames: 这也是早期的 ajax 实现方式,在页面中嵌入一个隐藏的IFrame,然后通过表单提交到该iframe来实现不刷新提交,不过使用这种方式同样会带来两个问题:
a. 登录成功之后如何摆脱登录后的IFrame呢?如果成功登录可能会导致整个页面重定向,当然你能在form中使
用属性target="_parent",使之弹出,那么你如何在父页面显示错误信息呢?
b. 你可能会受到布局的限止(不允许或不支持iframe)
对于以上两种方案,并非说不能实现,只是说对于一个灵活的登录系统来说仍然还是会存在一定的局限性的,我们坚信能有更好的方案来解决这个问题。
4. 通过JS重定向来获取login ticket (lt)
当第一次进入子系统的登录页时,通过 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 的操作。
5. 开始实践
首先,在我们的子系统中应该有一个登录页面,通过输入用户名和密码提交至cas认证中心。不过前提是先要获取到 login tickt id. 也就是说当用户第一次进入子系统的登录页面时,在该页面中会通过js跳转到 cas/login 中的获取login ticket. 在 cas/login 的 flow 中先会判断请求的参数中是否包含了 get-lt 的参数。
在cas的 login flow 中加入 ProvideLoginTicketAction 的流,主要用于判断该请求是否是来获取 lt,在cas-server端声明获取 login ticket action 类:
com.denger.sso.web.ProvideLoginTicketAction
Java代码
/**
* 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 中:
Xml代码
<
bean
id
=
"provideLoginTicketAction"
class
=
"com.denger.sso.web.ProvideLoginTicketAction"
/>
还需要定义 loginTicket 的生成页也就是当返回 loginTicketRequested 的 view:
viewRedirectToRequestor.jsp
Java代码
<%@ 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 中:
Config代码
### Redirect with login ticket view
casRedirectToRequestorView.(class)=org.springframework.web.servlet.view.JstlView
casRedirectToRequestorView.url=/WEB-INF/view/jsp/default/ui/viewRedirectToRequestor.jsp
相关
com.denger.sso.util.CasUtility
代码:
Java代码
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 √obrig 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 :
org.jasig.cas.web.flow.AuthenticationViaFormAction
修改 submit方法 中代码下如:
Java代码
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 请求流中:
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:
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
= 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参数。
6. 不足之处
1. 可以发现,每次用户访问 登录页面时都要进行两次重定向的操作,虽然很快,但是在有些情况仍然能看到登录页面闪了一下。 当然这也是有办法可以解决的!
2. 可以发现,当登录失败之后,会将错误信息以参数的方式进行传递,看上去这并非专业做法。可以定义一些错误标识,比如 1 是用户名或密码错误之类的。
CAS 之自定义登录页实践
标签:
原文地址:http://my.oschina.net/u/2273085/blog/472591
踩
(
0
)
赞
(
0
)
举报
评论
一句话评论(
0
)
登录后才能评论!
分享档案
更多>
2021年07月29日 (22)
2021年07月28日 (40)
2021年07月27日 (32)
2021年07月26日 (79)
2021年07月23日 (29)
2021年07月22日 (30)
2021年07月21日 (42)
2021年07月20日 (16)
2021年07月19日 (90)
2021年07月16日 (35)
周排行
更多
分布式事务
2021-07-29
OpenStack云平台命令行登录账户
2021-07-29
getLastRowNum()与getLastCellNum()/getPhysicalNumberOfRows()与getPhysicalNumberOfCells()
2021-07-29
【K8s概念】CSI 卷克隆
2021-07-29
vue3.0使用ant-design-vue进行按需加载原来这么简单
2021-07-29
stack栈
2021-07-29
抽奖动画 - 大转盘抽奖
2021-07-29
PPT写作技巧
2021-07-29
003-核心技术-IO模型-NIO-基于NIO群聊示例
2021-07-29
Bootstrap组件2
2021-07-29
友情链接
兰亭集智
国之画
百度统计
站长统计
阿里云
chrome插件
新版天听网
关于我们
-
联系我们
-
留言反馈
© 2014
mamicode.com
版权所有 联系我们:gaon5@hotmail.com
迷上了代码!