【SSO单点系列】(2):CAS4.0 之 跨域 Ajax 登录实践
CAS4.0 之 跨域 Ajax 登录实践
一、问题描述
CAS实现单点 实现一处登录 可访问多个应用 。 但是原登录是CAS默认登录页面和登出页面是无法重定向到自定义页面的 此处使用Ajax+Iframe 的方法来实现自定义页面跨域提交登录。
二、问题分析
CAS在登录认证时主要参数说明:
service [OPTIONAL] 登录成功后重定向的URL地址;
username [REQUIRED] 登录用户名;
password [REQUIRED] 登录密码;
lt
[REQUIRED] 登录令牌;
主要有四个参数,其中的三个参数倒好说,最关键的就是 lt , 据官方说明该参数是login ticket id, 主要是在登录前产生的一个唯一的“登录门票”,然后提交登录后会先取得"门票",确定其有效性后才进行用户名和密码的校验,否则直接重定向至 cas/login 页。
于是,便打开CAS-Server的登录页,发现其每次刷新都会产生一个 lt, 其实就是 Spring WebFlow 中的 flowExecutionKey值。
那么问题的关键就在于在子系统中如何获取 lt 也就是登录的ticket?
三、 可能的解决方案
一般对于获取登录ticket的解决方案可能大多数人都会提到两种方法:
AJAX: 熟悉 Ajax 的可能都知道,它的请求方式是严格按照沙箱安全模型机制的,严格情况下会存在跨域安全问题。
IFrames: 这也是早期的 ajax 实现方式,在页面中嵌入一个隐藏的IFrame,然后通过表单提交到该iframe来实现不刷新提交,不过使用这种方式同样会带来两个问题:
登录成功之后如何摆脱登录后的IFrame呢?如果成功登录可能会导致整个页面重定向,当然你能在form中使用属性 target="_parent",使之弹出,那么你如何在父页面显示错误信息呢?
b. 你可能会受到布局的限止(不允许或不支持iframe) 对于以上两种方案,并非说不能实现,只是说对于一个灵活的登录系统来说仍然还是会存在一定的局限性的,我们坚信能有更好的方案来解决这个问题。
四、 通过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 的操作。
五、实现
1 、客户端iframe提交代码
<form action="http://www.myCas.com:18080/login" method="post"
onsubmit="return loginValidate();" target="ssoLoginFrame">
<ul>
<span class="red" style="height:12px;" id="J_ErrorMsg"></span> <li><em>用户名:</em> <input name="username" id="J_Username" value="2"
type="text" autocomplete="off" class="line" style="width: 180px" />
</li>
<li><em>密 码:</em> <input name="password" type="password" value="2"
id="J_Password" class="line" style="width: 180px" /></li> <li class="mai"><em> </em> <input type="checkbox"
name="rememberMe" id="rememberMe" value="true" /> 自动登录 <a
href="/retrieve">忘记密码?</a></li>
<li><em> </em>
isajax:<input type="text" name="isajax" value="true" />
isframe:<input type="text" name="isframe" value="true" />
lt:<input type="text" name="lt" value="" id="J_LoginTicket">
execution: <input type="text" name="execution" id="execution" value="">
_eventId:<input type="text" name="_eventId" value="submit" />
<input name="" type="submit" value="登录" class="loginbanner" />
ticket:<input type="text" name="ticket" value="" id="ticket">
<input type="hidden" name="loginUrl" value="http://www.myApp1.com:8080/test.jsp" />
</li>
</ul> </form>
<a href="javascript:void(0)" class="easyui-linkbutton" onClick="checkForLoginTicket()">单点登录</a>
</div>
<script> $(document).ready(function() {
checkForLoginTicket();
});
var myCas = 'http://www.myCas.com:18080';
var myApp1 = 'http://ciat.padx.cn:8080'; var loginTicket;
function checkForLoginTicket() {
var loginTicketProvided = false;
var query = '';
casLoginURL = myCas+'/login';
thisPageURL = myApp1+'/test.jsp?&n='
+ new Date().getTime();
thisPageURL2 = myApp1+'/user-center.action' ; casLoginURL += '?login-at=' + encodeURIComponent(thisPageURL)+'&service=' + encodeURIComponent(thisPageURL2);
query = window.location.search;
queryquery = query.substr(1); var param = 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];
$('#J_LoginTicket').val(loginTicket);
loginTicketProvided = true;
}
if (temp[0] == '?ticket') {
loginTicketProvided = true;
$('#ticket').val(temp[1] );
}
if (temp[0] == 'execution') {
$('#execution').val(temp[1] );
} if (temp[0] == 'error_message') {
error = temp[1];
}
i++;
}
// 判断是否已经获取到 lt 参数,如果未获取到则跳转至 cas/login 页,并且带上请求参数 get-lt=true。 第一次进该页面时会进行一次跳转
if (!loginTicketProvided) {
location.href = casLoginURL + '&get-lt=true';
}
} //-------------------- // 登录验证函数, 由 onsubmit 事件触发
var loginValidate = function() {
var msg;
if ($.trim($('#J_Username').val()).length == 0) {
msg = "用户名不能为空。";
} else if ($.trim($('#J_Password').val()).length == 0) {
msg = "密码不能为空。";
}
if (msg && msg.length > 0) {
$('#J_ErrorMsg').fadeOut().text(msg).fadeIn();
return false;
// Can't request the login ticket.
} else if ($('#J_LoginTicket').val().length == 0) {
// $('#J_ErrorMsg').text('服务器正忙,请稍后再试..');
// return false;
} else {
// 验证成功后,动态创建用于提交登录的 iframe
$('body').append($('<iframe/>').attr({
style : "display:none;width:0;height:0",
id : "ssoLoginFrame",
name : "ssoLoginFrame",
src : "javascript:false;"
}));
return true;
}
}
</script>
2、客户端web.xml
!--单点退出配置-->
<!--用于单点退出,该过滤器用于实现单点登出功能,可选配置 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener> <!--该过滤器用于实现单点登出功能,可选配置。 -->
<filter>
<filter-name>CASSingle Sign OutFilter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CASSingle Sign OutFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>CASFilter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://www.myCas.com:18080/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://ciat.padx.cn:8080</param-value>
</init-param>
</filter> <filter-mapping>
<filter-name>CASFilter</filter-name>
<url-pattern>/user-center.action</url-pattern>
<url-pattern>/user-center!validate2.action</url-pattern>
</filter-mapping> <!--该过滤器负责对Ticket的校验工作,必须启用它 -->
<filter>
<filter-name>CASValidationFilter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://www.myCas.com:18080</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://ciat.padx.cn:8080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CASValidationFilter</filter-name>
<url-pattern>/user-center.action</url-pattern>
</filter-mapping> <!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
<filter>
<filter-name>CASHttpServletRequest WrapperFilter</filter-name>
<filter-class>
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>CASHttpServletRequest WrapperFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 -->
<filter>
<filter-name>CASAssertion Thread LocalFilter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CASAssertion Thread LocalFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3、服务端
向服务端发送请求为3 个部分:
1、 请示页面获取lt登录页面的密钥
2、 发送用户名密码
3、 返回服务端发来的ST在确认是否成功登录
服务端修改login-webflow.xml(添加)
<!-- 添加如下配置 :-->
<action-state id="provideLoginTicket">
<evaluate expression="provideLoginTicketAction"/>
<transition on="loginTicketRequested" to ="ajaxgenerateLoginTicket" />
<transition on="continue" to="generateLoginTicket" />
<transition on="newapp" to="generateServiceTicket" />
</action-state>
<view-state id="viewRedirectToRequestor" view="casRedirectToRequestorView" model="credential">
<binder>
<binding property="username" />
<binding property="password" />
</binder>
<on-entry>
<set name="viewScope.commandName" value="'credential'" />
</on-entry>
<transition on="submit" bind="true" validate="true" to="realSubmit">
<evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credential)" />
</transition>
</view-state> <action-state id="ajaxgenerateLoginTicket">
<evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" />
<transition on="generated" to="viewRedirectToRequestor" />
</action-state>
<!-- 添加结束处-->
添加ProvideLoginTicketAction.java
1 public class ProvideLoginTicketAction extends AbstractAction {
2
3 @Override
4 protected Event doExecute(RequestContext context) throws Exception {
5 final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
6 final Service service = WebUtils.getService(context);
7 final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
8 if(ticketGrantingTicket!=null){
9 return result("newapp");
10 }
11 if (request.getParameter("get-lt") != null && request.getParameter("get-lt").equalsIgnoreCase("true")) {
12 return result("loginTicketRequested");
13 }
14 return result("continue");
15 }
16
17 }
default_views.properties
casRedirectToRequestorView.(class)=org.springframework.web.servlet.view.JstlView
casRedirectToRequestorView.url=/WEB-INF/view/jsp/default/ui/viewRedirectToRequestor.jsp
添加viewRedirectToRequestor.jsp
1 <%@ page contentType="text/html; charset=UTF-8"%>
2 <%@ page import="org.jasig.cas.util.CasUtility"%>
3 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
4 <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
5 <%
6 String separator = "";
7 // 需要输入 login-at 参数,当生成lt后或登录失败后则重新跳转至 原登录页,并传入参数 lt 和 error_message
8 String referer = request.getParameter("login-at");
9
10 referer = CasUtility.resetUrl(referer);
11 if (referer != null && referer.length() > 0) {
12 separator = (referer.indexOf("?") > -1) ? "&" : "?";
13 %>
14 <html>
15 <title>cas get login ticket</title>
16 <head>
17 <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
18 <script>
19 var redirectURL = "<%=referer + separator%>lt=${loginTicket}&execution=${flowExecutionKey}";
20 window.location.href = redirectURL;
21 </script>
22 </head>
23 <body></body>
24 </html>
25 <%
26 } else {
27 %>
28 <%
29 }
30 %>
服务端在获取请求时会进入ProvideLoginTicketAction.java进行判断传入参数get-lt
值为false不是ajax在请求获取lt值,那么按原流程走
值为true 生成lt 并 进入自定义返回页面viewRedirectToRequestor.jsp 回传
以上代码虽已可以成功登录但是客户端只有iframe里的内容显示已成功 iframe外需要刷新页面才可以,下面实现自动刷新
1 <action-state id="generateServiceTicket">
2 <evaluate expression="generateServiceTicketAction" />
3 <!--<transition on="success" to ="warn" /> -->
4 <transition on="success" to="loginResponse" />
5 <transition on="authenticationFailure" to="handleAuthenticationFailure" />
6 <transition on="error" to="generateLoginTicket" />
7 <transition on="gateway" to="gatewayServicesManagementCheck" />
8 </action-state>
9
10 <action-state id="loginResponse">
11 <evaluate expression="ajaxLoginServiceTicketAction" />
12 <!--非ajax/iframe方式登录,采取原流程处理 -->
13 <transition on="success" to="warn" />
14 <transition on="error" to="generateLoginTicket" />
15 <!-- 反之,则进入 viewAjaxLoginView 页面 -->
16 <transition on="local" to="viewAjaxLoginView" />
17 </action-state>
generateServiceTicket内的返回success修改为loginResponse 并 新增loginResponse内容
添加AjaxLoginServiceTicketAction.java
1 public class AjaxLoginServiceTicketAction extends AbstractAction {
2
3 // The default call back function name.
4 protected static final String J_CALLBACK = "feedBackUrlCallBack";
5
6 protected Event doExecute(final RequestContext context) {
7 HttpServletRequest request = WebUtils.getHttpServletRequest(context);
8 Event event = context.getCurrentEvent();
9 boolean isAjax = BooleanUtils.toBoolean(request.getParameter("isajax"));
10
11 if (!isAjax){ // 非 ajax/iframe 方式登录,返回当前 event.
12 return event;
13 }
14 boolean isLoginSuccess;
15 // Login Successful.
16 if ("success".equals(event.getId())){ //是否登录成功
17 final Service service = WebUtils.getService(context);
18 final String serviceTicket = WebUtils.getServiceTicketFromRequestScope(context);
19 if (service != null){ //设置登录成功之后 跳转的地址
20 request.setAttribute("service", service.getId());
21 }
22 request.setAttribute("ticket", serviceTicket);
23 isLoginSuccess = true;
24 } else { // Login Fails..
25 isLoginSuccess = false;
26 }
27
28 boolean isFrame = BooleanUtils.toBoolean(request.getParameter("isframe"));
29 String callback = request.getParameter("callback");
30 if(StringUtils.isEmpty(callback)){ // 如果未转入 callback 参数,则采用默认 callback 函数名
31 callback = J_CALLBACK;
32 }
33 if(isFrame){ // 如果采用了 iframe ,则 concat 其 parent 。
34 callback = "parent.".concat(callback);
35 }
36 request.setAttribute("isFrame", isFrame);
37 request.setAttribute("callback", callback);
38 request.setAttribute("isLogin", isLoginSuccess);
39
40 return new Event(this, "local"); // 转入 ajaxLogin.jsp 页面
41 }
42
43 }
default_views.properties
1 viewAjaxLoginView.(class)=org.springframework.web.servlet.view.JstlView
2 viewAjaxLoginView.url=/WEB-INF/view/jsp/default/ui/ajaxLogin.jsp
新增ajaxLogin.jsp
1 <%@ page contentType="text/html; charset=UTF-8"%>
2 <html>
3 <head>
4 <title>正在登录....</title>
5 </head>
6 <body>
7 <script type="text/javascript">
8 <%
9 Boolean isFrame = (Boolean)request.getAttribute("isFrame");
10 Boolean isLogin = (Boolean)request.getAttribute("isLogin");
11 // 登录成功
12 if(isLogin){
13 if(isFrame){%>
14 parent.location.replace('${service}?ticket=${ticket}')
15 <%} else{%>
16 location.replace('${service}?ticket=${ticket}')
17 <%}
18 }
19 %>
20 // 回调
21 ${callback}({'login':${isLogin ? '"success"': '"fails"'}, 'msg': ${isLogin ? '""': '"用户名或密码错误!"'}})
22 </script>
23 </body>
24 </html>
【SSO单点系列】(2):CAS4.0 之 跨域 Ajax 登录实践的更多相关文章
- 【SSO单点系列】(2):CAS4.0 登录页的个性化定制
上一篇 [SSO单点系列](1):CAS环境的搭建介绍了CAS最简单环境的搭建,以及一个例子用来讲解CAS的一个最基础的用法. 今天主要是介绍如何对CAS登录页进行个性化定制. 一.开始 下图是 ...
- vue-cli 3.0之跨域请求代理配置及axios路径配置
vue-cli 3.0之跨域请求代理配置及axios路径配置 问题:在前后端分离的跨域请求中,报跨域问题 配置: vue.config.js: module.exports = { runtimeCo ...
- .NET技术-4.0. NETCORE跨域
.NET技术-4.0. NETCORE跨域 1.安装程序CORS程序包,一般默认都带了此程序包的 Install-Package Microsoft.AspNetCore.Mvc.Cors 2.配置C ...
- 跨域Ajax请求WebService方法
一.允许跨域Ajax请求,更改如下配置: 在要调用的WebService上面添加特性标签: 二.以如下返回用户信息的WebService方法为例 三.在另一个网站上通过Ajax访问webService ...
- 使用$.getJSON实现跨域ajax请求
jQuery中常用getJSON来调用并获取远程的JSON字符串,将其转换为JSON对象,如果成功,则执行回调函数.原型如下: jQuery.getJSON( url, [data], [callba ...
- Ajax与跨域Ajax
Ajax 对于WEB应用程序:用户浏览器发送请求,服务器接收并处理请求,然后返回结果,往往返回就是字符串(HTML),浏览器将字符串(HTML)渲染并显示浏览器上.对于传统的Web应用,一个简单操作需 ...
- 跨域资源共享(CORS)--跨域ajax
几年前,网站开发者都因为ajax的同源策略而撞了南墙.当我们惊叹于XMLHttpRequest对象跨浏览器支持所带来的巨大进步时,我们很快发现没有一个方法可以使我们用JavaScript实现请求跨域访 ...
- 06: AJAX全套 & jsonp跨域AJAX
目录: 1.1 AJAX介绍 1.2 jQuery AJAX(第一种) 1.3 原生ajax(第二种) 1.4 iframe“伪”AJAX(第三种) 1.5 jsonp跨域请求 1.6 在tornad ...
- 跨域Ajax -- jsonp和cors
跨域Ajax - jsonp - cors 参考博客: http://www.cnblogs.com/wupeiqi/articles/5703697.html http://www.cnblogs. ...
- 第二百七十四节,同源策略和跨域Ajax
同源策略和跨域Ajax 什么是同源策略 尽管浏览器的安全措施多种多样,但是要想黑掉一个Web应用,只要在浏览器的多种安全措施中找到某种措施的一个漏洞或者绕过一种安全措施的方法即可.浏览器的各种保安措 ...
随机推荐
- appcrawler(2.1.3)采坑
转发:https://www.jianshu.com/p/d97290136bad 遇到的坑: 问题1: Exception in thread "main" org.openqa ...
- nginx从仅支持80到支持80和443
测试环境的域名,之前只支持http.开发同学不知啥时候改了数据库配置,导致所有跳转到到了https了.所以得加上https访问. 配置文件之前是这样的: 1 server { 2 listen 80; ...
- 《__cplusplus修饰符的作用:C和CPP接口互相调用时候,编译没问题,链接提示未定义问题》
关于__cplusplus修饰符说明如下: __cplusplus是cpp中的自定义宏,那么定义了这个宏的话表示这是一段cpp的代码,也就是说,上面的代码的含义是:如果这是一段cpp的代码,那么加入e ...
- thunar文件管理器修改默认的关联的终端
有时候在文件管理器的文件夹中打开终端操作很方便.目前好用的文件管理器基本和虚拟中终端基本上是各个桌面环境的配套产品. 比如xfce环境的thunar文件管理器如果想搭配lxde环境的lxtermina ...
- 【已解决】SpringBoot + Mybatis-plus 实体类属性注解 @TableField 无法获取到数据库值(属性变量名带下划线)
问题描述: 实体类变量的命名格式 如果采用的是 XX_XX带下划线的形式,那么在低版本的mybatis-plus是不支持和数据库映射的. 如果是单个单词不存在这个问题,如果出现多个单词,尽量采用驼峰式 ...
- CAN通讯模板
#define MyCAN_SJW CAN_SJW_3tq#define MyCAN_BS1 CAN_BS1_5tq#define MyCAN_BS2 CAN_BS2_3tq#define MyCAN ...
- input file 图片上传前预览
1.获取到这个File对象之后就可以获取到上传文件的一些属性,比如:lastModified(代表文件的修改日期,而非上传日期).name.size(单位是b).type(例如图片就是"im ...
- 关闭内核Debug_log打印!
echo 0 > /proc/sys/kernel/printk \r
- java post格式发送application/x-www-form-urlencoded
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.http.*; ...
- Python基于粒子群优化的投资组合优化研究|附代码数据
全文链接:http://tecdat.cn/?p=6811 最近我们被客户要求撰写关于粒子群优化的研究报告,包括一些图形和统计输出. 我今年的研究课题是使用粒子群优化(PSO)的货币进位交易组合优化. ...