前后端分离的项目集成CAS
前后端分离的项目集成CAS
关于前后端分离的项目如何集成CAS,网上很少有资料博客,即便是有的也只是单纯的说下怎么做,并没有解释为什么,甚至大部分在前后端分离的项目中"成功"的集成了cas
的,也是懵懵懂懂.
本文从原理层面来进行一定的剖析,再来看如何解决.
采用环境:Tomcat 8.5
,cas 3.4.1
.
本文内容和Tomcat
及cas
版本关系不大.
主要涉及以下知识:
cas
认证原理,及客户端的认证源码分析.session
、cookie
相关知识
主要解决如下两个问题:
- 前后端分离项目如何接入
cas
. - 如何和项目原有的登录做集成.
不想看原理,那直接去看如何对接也可以,其中也有一定的流程分析.
CAS认证原理
先说说后端是如何判断一个请求是否是认证过的请求.
个人认为,大体上是分为两大类(如果有更准确的):
-
基于
session
的认证.即:后端把认证信息,存放在session
中,而这个session
的唯一标识对应到前端,就是名为JSESSIONID
的cookie
(JSESSIONID
的值为后端session
的ID),前端每次请求都会带上JSESSIONID
,后端再找到对应的session
信息来进行权限判断. -
基于
token
的认证.简单的说,就是把认证信息,后端通过一定的算法生成了一个可以反解析的字符串,这个字符串就是所谓的token
,前端每次请求都要附带上这个token
.关token
的生成,可以参照JWT
.
CAS
认证原理
cas
是属于前者,即基于session
的认证方式.
cas
是把认证信息放在了session
的attribute
中(可通过request.getSession().getAttribute("_const_cas_assertion_")
)
我们在对接时经常撞见的各种问题,其实从本质上来说,就是一种问题,前端发送给后端的请求头里没有JSESSIONID
.
图源:https://apereo.github.io/cas/5.3.x/images/cas_flow_diagram.png
其中的ST
就是所谓的唯一令牌,用过即失效.这个令牌也可能是采用JWT
来生成的.
图源:https://apereo.github.io/cas/5.3.x/images/cas_flow_jwt_diagram.png
关于session
、cookie
及JSESSIONID
在其中起到的作用
既然是以基于session
的认证来说明,那就要先从session
、cookie
、以及把他们关联起来的JSESSIONID
说起.
session
是什么?
众所周知,Http协议是一种无状态协议,每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录;
为了弥补Http的无状态特性,session
应运而生.服务器可以利用session
存储客户端在同一个会话期间的一些操作记录,而服务端的这个session
,对应到浏览器端,则是名为JSESSIONID
的cookie
,JSESSIONID
的值就是session
的id.
那么再看两个问题:
- 服务器如何判断客户端发送过来的请求是属于同一个seesion?
答:用session
的id来进行区分,如果id相同,那就认为是同一个会话.在Tomcat
中,session
的id的默认名字是JSESSIONID
.对应到前端就是名为JSESSIONID
的cookie
.
session
的id是在什么时候创建,又是怎样在前后端传输的?
答:tomcat
在第一次接收到一个请求时,会创建一个session
对象,同时生成一个session id
,并通过响应头的Set-Cookie:"JSESSIONID=XXXXXXX"
命令,向客户端发送要求设置Cookie
的响应.
前端在后续的每次请求时,会带上所有cookie
信息,自然也就包含了JSESSIONID
这个cookie
.Tomcat
据此来查找到对应的session
.如果指定session
不存在(比如我们随手编一个JSESSIONID
,那对应的session
肯定不存在),那么就会创建一个新的session
,其id的值就是请求中的JSESSIONID
的值.
这样我们在代码中就可以通过request.getSession()
来获取到当前的会话信息.
客户端的cas过滤器是如何处理请求的
JSESSIONID
或者说是session
在具体到cas
前后端对接上,又是起到什么作用呢?
前面我们有说过,认证大体上分为两大类:基于session
和基于token
.基于session
的认证的实现,简单的说就是把认证相关信息放在session
中,而JSESSIONID
就是session
的唯一标识.cas
也不例外.
来看下cas
的是如何判断session
是否已通过认证的.
客户端的web.xml
中配置的cas filter
,大致是这样的:
<filter>
<filter-name>CAS Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>${cas.serverUrl}/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>${cas.clientUrl}</param-value>
</init-param>
<init-param>
<param-name>ignorePattern</param-name>
<param-value>.*/login|.*/unsafe|.*/api/app/token/*|.*\.ico|.*\.js(?!p)|.*\.css|</param-value>
</init-param>
<init-param>
<param-name>ignoreUrlPatternType</param-name>
<param-value>REGEX</param-value>
</init-param>
...
</filter>
<filter-mapping>
<filter-name>CAS Filter</filter-name>
<url-pattern>/cas.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CAS Filter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
这个过滤器就是用来负责校验每个请求是否是认证的请求.
看下对应的实现(我采用的cas
版本是3.4.1
,其它版本自行查看对应源码,应该是大同小异).
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;
// 判断请求是否不需要过滤
if (isRequestUrlExcluded(request)) {
logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
return;
}
final HttpSession session = request.getSession(false);
// CONST_CAS_ASSERTION = "_const_cas_assertion_"
final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
// 存在assertion,即认为这是一个已通过认证的请求.予以放行
if (assertion != null) {
filterChain.doFilter(request, response);
return;
}
// 不存在 assertion,那么就来判断这个请求是否是用来校验ST的(校验通过后会将信息写入assertion)
final String serviceUrl = constructServiceUrl(request, response);
final String ticket = retrieveTicketFromRequest(request);
final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
// 是校验ST的请求,予以放行
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);
final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
logger.debug("redirecting to \"{}\"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}
可以看到,cas
正是通过session
中是否有assertion
的信息来判断一个请求是否合法.
而这个assertion
信息,又是在登陆成功后第一次重定向回客户端校验ST
之后(这里的客户端指的是后台,此时重定向回客户端的请求附带有ST参数)写入session
中的.具体可参照下对应的实现源码,比如我这边CAS Validation Filter
的配置大致如下:
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
</filter-class>
</filter>
查看Cas20ProxyReceivingTicketValidationFilter
源码,可以在其父类AbstractTicketValidationFilter
的doFilter()
方法中找到对应的代码,如下:
final Assertion assertion = this.ticketValidator.validate(ticket,
constructServiceUrl(request, response));
logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
// CONST_CAS_ASSERTION = "_const_cas_assertion_"
request.setAttribute(CONST_CAS_ASSERTION, assertion);
// useSession 对应配置项中的useSession参数,缺省值为true.但这个配置在3.4版本之后是弃用的,后续随时可能会被移除.
if (this.useSession) {
request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
}
第一次重定向回客户端的请求肯定是可以通过cas的认证的,那么只要这个后续的请求和第一个是同一个session
,那就一定可以通过cas filter
的过滤.
前面我们也说了,只要请求中的JSESSIONID
是一致的,那就会被认定是同一个session
.
实际上前后端分离的项目对接cas
时,从登录成功后默认跳转的第一个地址(也就是登录界面地址栏里的service
参数)来看,大体上来看无非就是两种路子:
- 默认跳转的是前端.如果看懂了上面的流程,就会意识到,这个是行不通的–从cas服务端跳转回的第一个地址必须是经过后端
cas filter
的地址. - 默认跳转的是后端,后端再重定向回前端.这个思路上是对的,如果能成功跳转回前端,但是后续请求依旧说未认证,那么问题基本上就是由于后续请求和第一个请求不是同一个
session
导致的.
前后端分离项目如何集成CAS
这里说的前后端分离,是指部署也分离的项目.
关键就是这两点:
- 登录成功后跳转的第一个地址必须是能进入后端
cas filter
的地址(用来做重定向,可以是一个接口,也可以是一个.jsp
界面),保证cas
可以把认证信息写入session
. - 后续请求必须和第一个请求(即调整后端的那个请求)是属于同一个
session
.
只要能保证这两点,那就绝对没问题.下面这是我的解决思路:
假设cas
服务端地址为192.168.0.90:8080/cas
,后端地址为192.168.0.100:8080/api-server
,前端地址为192.168.0.120
,后端增加一个cas.jsp
来做重定向.
具体步骤:
1. 修改web.xml
增加对cas.jsp
的拦截
随手建一个cas.jsp
,放到后端webapp
目录下,里面暂时不用写什么代码,后面再加.
修改web.xml
中的cas filter
的配置,增加对cas.jsp
的拦截.
CAS Filter
/cas.jsp
2.未登录时,前端重定向后端cas.jsp
调整下前端的js逻辑,在发现未登录时跳转cas.jsp
(准确的说,最终目的是保证登录成功后第一个跳转的界面是cas.jsp
)
// 参考代码如下
if(notLogin){
window.location.href='192.168.0.100:8080/api-server/cas.jsp';
}
cas.jsp
被CAS Filter
拦截,那么会再自动重定向到cas
登录界面:192.168.0.90:8080/cas/login?service=192.168.0.100:8080/api-server/cas.jsp
.
如果想节省一次重定向的开销,前端也可以直接重定向到登录界面,注意加上service
参数即可
//和上面代码结果一样,但是节省一次重定向开销.参考代码如下
if(notLogin){
window.location.href=`192.168.0.90:8080/cas/login?service=192.168.0.100:8080/api-server/cas.jsp`;
}
如果一切顺利的话,这个时候登录成功,那么就会进入cas.jsp
界面.这个进入cas.jsp
的请求的session
,是通过了cas
认证的.
3. cas.jsp
重定向到前端
修改cas.jsp
,加入如下代码:
// 前端地址.
String url = "192.168.0.120";
response.sendRedirect(url);
那么此时,登录成功后最终会进入前端界面.下面我们只需要保证,后续前端发起的请求,和刚刚这个经过了cas.jsp
的请求,是属于同一个session
就行了,或者说,JSEESIONID
这个cookie
能正常写入前端的域下.
4. 保证将JSESSIONID
写入前端cookie
中
在创建一个新的session
后,Tomcat
的Session Manager
在response
中会加上Set-cookie=JSESSIONID=123123
这个头,来让浏览器写入JSESSIONID
.但是这个是写在后端的域下面而不是前端的,而在正常配置中,cookie
是没法跨域读写的.
导致的结果就是,登录成功了也跳转回了前端界面,但是后续的请求因为请求中没有JSESSIONID
这个cookie
(实际上也不是必须放在cookie
里),导致后续的每个请求,对于Tomcat
来说,都是一个新的请求,和前面的请求半毛钱关系都没,进而创建新的session
,那cas filter
自然也就不会放行.
那么这一步要解决的问题就是如何将JSESSIONID
写入前端的域下的问题.
那大体上的方案,也就如下三种:
- 跨域写
cookie
.大体上就是ajax
请求加上{crossDomain: true, xhrFields: {withCredentials: true}}
,后端响应头加上response.addHeader("Access-Control-Allow-Credentials", "true")
. - 让前后端
不跨域
.比如,用nginx
将前后端反向代理到同一个域下,无论是访问前端界面还是调用后端接口还是后端cas filter
中的配置都是用这个代理后的地址. - 前端手动写入
JSESSIONID
.比如,cas.jsp
在重定向回前端时,url后面附带上JSESSIONID
的值,前端js获取到地址栏里的值,再手动写入cookie
中.
就以第三种为例:
修改cas.jsp
.
//获取sessionid
String jsessionid = session.getId();
// 前端地址.
String url = "192.168.0.120";
response.sendRedirect(url + "?jsessionid=" + jsessionid);
前端js:
// getJsessionIdFromUrl() 从地址栏里获取jseesionid参数的值,具体逻辑自行实现
var jsessionid = getJsessionIdFromUrl();
// setCookie() 写入cookie中,具体逻辑自行实现
setCookie('jsessionid',jsessionid);
前端后续再发送的请求,带上JSESSIONID
这个cookie
,就没事了.
回顾下,总体的解决流程就是:
- 前端判断没登录,跳转
192.168.0.100:8080/api-server/cas.jsp
192.168.0.100:8080/api-server/cas.jsp
的请求到达cas filter
,cas filter
发现session
未认证且没有ST
参数,302
重定向到cas
服务端登录界面192.168.0.90:8080/cas/login?service=192.168.0.100:8080/api-server/cas.jsp
- 向
cas
服务端登录界面输入用户名密码,登录成功 - 根据
service
参数,302
重定向到192.168.0.100:8080/api-server/cas.jsp?ST=ABC1234567
,注意此时是附带上了ST
参数. 192.168.0.100:8080/api-server/cas.jsp?ST=ABC1234567
请求先进入后端cas filter
发现session
未认证但是有ST
参数,予以放行- 请求再进入后端
CAS Validation Filter
,向cas
服务端校验ST
,校验通过,将结果(即服务端返回的用户信息)写入session
- 请求到达
192.168.0.100:8080/api-server/cas.jsp
界面 cas.jsp
做一系列处理(比如二次登录)后,将JSESSIONID
附带在url参数中,再重定向回前端- 前端从地址栏url中获取到
JSESSIONID
参数,写入cookie
中.
当然细节上还有很多可优化可完善的地方,但大致上就是这个流程思路.
CAS
如何和原有的登录认证做对接
常见的一个问题就是,我这个项目本身已经有了一套登录体系,现在要集成cas
,应当如何去做.
尤其是对接第三方的cas
,cas
服务端采用的用户库都和我们自己的不一样,这个该怎么去做.
大体思路依旧是通过前面我们增加的这个cas.jsp
.在cas.jsp
中根据cas
返回的用户信息(一般都有用户名,但是不会有也不应当有密码),做二次登录,二次登录成功后再跳转前端.
参考代码如下:
// cas.jsp 内容
// cas会将用户信息写入session中. request.getAttribute("_const_cas_assertion_") 也可以拿到.
Object object = request.getSession().getAttribute("_const_cas_assertion_");
// org.jasig.cas.client.validation.Assertion
Assertion assertion = (Assertion) object;
// 获取到用户名
String userName = assertion.getPrincipal().getName()
/*
假设原有登录是调用的 loginService.login(userName,password);方法
那我们增加一个免密登录方法 loginService.loginWithOutPWD(userName),
里面的处理逻辑和返回值 同loginService.login(userName,password)基本一致,唯独不再需要密码.
*/
Object obj = loginService.loginWithOutPWD(userName);
// isLogin判断是否二次登录成功
if(isLogin(obj)){
// 二次登录成功,调整前端. 如果原有登录有其它参数需要给前端,也可以附带在url后面.
//获取sessionid
String jsessionid = session.getId();
// 前端地址.
String url = "192.168.0.120";
response.sendRedirect(url + "?jsessionid=" + jsessionid);
}else{
// 二次登录失败
//...
}
最后再强调下,我尽量剖析出关键问题所在,然后提供的是一个解决思路,涉及的代码也是简化了的.其中有很多可以优化的地方,文中没提及的问题稍加修改也可以进行解决.比如:
cas.jsp
中重定向回前端的地址,写死肯定不合适,如果要换成动态指定该怎么办.cas.jsp
去掉,换成一个restful
的服务该怎么办- 在 保证将
JSESSIONID
写入前端cookie
中 这一部分提到了三种解决方案,但最后只是以第三种为例来说明.如果是前两种,那具体该怎么办(实际上前两种可能改动量可能会更小).
以上.
- 使用strace分析exp的奇怪问题(r3笔记第41天)
- Python文本挖掘:知乎网友如何评价《人民的名义》
- 怎样做中文文本的情感分析?
- 由一条日志警告所做的调优分析(r3笔记第40天)
- 生产环境sql语句调优实战第十篇(r3笔记第39天)
- memory_target设置不当导致数据库无法启动的问题(r3笔记第38天)
- python利用结巴分词做新闻地图
- 数据库静默安装总结(r3笔记第58天)
- 用TensorFlow实现文本分析模型,做个聊天机器人
- 深度学习:用tensorflow建立线性回归模型
- 用python基于2015-2016年的NBA常规赛及季后赛的统计数据分析
- 数值信息的机器级存储
- ABAP和Java里关于DEFAULT(默认)机制的一些语言特性
- Golang语言社区--golang 进度下载文件
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 使用Angular rxjs进行优雅限流
- Nginx自动重定向
- dotnet OpenXML SDK 形状的翻转与旋转
- C# dotnet 使用 AsyncEx 库的 AsyncLock 异步锁
- bt5.9手动开心
- 项目中多个文件引入同一份公共样式less文件导致编译打包后有多份样式
- Angular rxjs Observable的异步行为
- Angular rxjs里自定义operator的使用
- tensorflow 生成指定大小的赋值0的张量 np.zeros 在TF中对应的语句 生成全0张量
- Angular rxjs fromEvent使用的一个例子
- 在StackBlitz上setup SAP Spartacus
- [898]python获取两个list交集|并集|差集
- [897]使用Maxwell实时同步mysql数据
- Magicodes.IE之导入导出筛选器
- 一文搞懂Flink生成StreamGraph