单点登录 (SingleSign-On,SSO) ,是一种帮助用户快捷访问网络中多个站点的安全通信技术。单点登录系统基于一种安全的通信协议,该协议通过多个系统之间的用户身份信息的交换来实现单点登录。使用单点登录系统时,用户只需要登录一次,就可以访问多个系统,不需要记忆多个口令密码。
1、CAS总体架构
CAS(Central Authentication Service)是 Yale大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法。CAS的目标是允许用户访问多个应用程序只提供一次用户凭据(如用户名和密码)。
CAS 体系包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。
CAS 具有以下特点:
一个开放的,文档齐全的协议。
开源的JAVA服务器组件。
CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。
文档社区化和实现的支持。
具有广泛的客户群的支持。
CAS官方文档:https://apereo.github.io/cas/5.3.x/index.html#
2、CAS单点原理
在 CAS 的整个登录过程中,有三个重要的概念。
TGT:TGT 全称叫做 Ticket Granting Ticket,这个相当于我们平时所见到的 HttpSession 的作用,用户登录成功后,用户的基本信息,如用户名、登录有效期等信息,都将存储在此。
TGC:TGC 全称叫做 Ticket Granting Cookie,TGC 以 Cookie 的形式保存在浏览器中,根据 TGC 可以帮助用户找到对应的 TGT,所以这个 TGC 有点类似与会话 ID。
ST:ST 全称是 Service Ticket,这是 CAS Sever 通过 TGT 给用户发放的一张票据,用户在访问其他服务时,发现没有 Cookie 或者 ST ,那么就会 302 到 CAS Server 获取 ST,然后会携带着 ST 302 回来,CAS Client 则通过 ST 去 CAS Server 上获取用户的登录状态。
CAS的单点登录SSO流程如下, 应用系统要做单点登录,需要跟CAS服务进行集成,首先要理解CAS集成流程和原理。
- 用户通过浏览器访问应用1,应用1 发现用户没有登录,于是返回 302,并且携带上一个 service 参数,让用户去 CAS Server 上登录。
- 浏览器自动重定向到 CAS Server 上,CAS Server 获取用户 Cookie 中携带的 TGC,去校验用户是否已经登录,如果已经登录,则完成身份校验(此时 CAS Server 可以根据用户的 TGC 找到 TGT,进而获取用户的信息);如果未登录,则重定向到 CAS Server 的登录页面,用户输入用户名/密码,CAS Server 会生成 TGT,并且根据 TGT 签发一个 ST,再将 TGC 放在用户的 Cookie 中,完成身份校验。
- CAS Server 完成身份校验之后,会将 ST 拼接在 service 中,返回 302,浏览器将首先将 TGC 存在 Cookie 中,然后根据 302 的指示,携带上 ST 重定向到应用1。
- 应用1 收到浏览器传来的 ST 之后,拿去 CAS Server 上校验,去判断用户的登录状态,如果用户登录合法,CAS Server 就会返回用户信息给 应用1。
- 浏览器再去访问应用2,应用2 发现用户未登录,重定向到 CAS Server。
- CAS Server 发现此时用户实际上已经登录了,于是又重定向回应用2,同时携带上 ST。
- 应用2 拿着 ST 去 CAS Server 上校验,获取用户的登录信息。
- 在整个登录过程中,浏览器分别和 CAS Server、应用1、应用2 建立了会话,其中,和 CAS Server 建立的会话称之为全局会话,和应用1、应用2 建立的会话称之为局部会话;一旦局部会话成功建立,以后用户再去访问应用1、应用2 就不会经过 CAS Server 了。
3、组织用户初始化
集成cas之前需要先初始化组织用户数据,各业务系统要保持统一用户信息。
3.1、启动平台
搭建平台环境,启动平台。环境搭建参考官网在线文档。
基于非源码搭建开发环境:
https://yunchengxc.yuque.com/staff-kxgs7i/public/vk24t6
基于源码搭建开发环境:
https://yunchengxc.yuque.com/staff-kxgs7i/public/dzou8y
基于Doctor部署环境:
https://yunchengxc.yuque.com/staff-kxgs7i/public/ofbgh0
3.2、录入或导入数据
用管理员账号登录平台后台,在菜单组织用户下的组织管理、用户管理、职务管理、岗位管理录入或导入组织用户数据。
4、部署CAS服务
平台对cas 5.3进行了改造,适配平台的用户表和加密策略,需要使用平台提供的cas.war。运行CAS之前需要在数据库先执行平台的脚本,CAS获取用户信息访问平台的SYS_USER表。
4.1、修改数据库连接
打开 cas\WEB-INF\classes\application.properties
修改如下配置:
#数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/yuncheng2021?characterEncoding=UTF-8&useUnicode=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
4.2、修改单点登录用户标识类型
单点登录身份标识类型,配置通过哪个属性标识唯一用户,对应单点登录界面的登录名,默认为username。
可根据项目实际需求修改为通过phone(手机号)或email(邮箱)登录,单点登录校验后返回的用户对象中principal也会对应修改。
#单点登录身份标识类型,配置通过哪个属性标识唯一用户,对应单点登录界面的登录名,默认为username
#从用户表查询用户和单点登录返回的用户名都使用该属性
# 值范围: username(sys_user表username字段):登录名 phone(sys_user表phone字段):手机号 email(sys_user表email字段):邮箱
#yuncheng.loginNameType=username
4.3 、CAS登录登出页定制
Cas服务登录页面位置:cas\WEB-INF\classes\templates\casLoginView.html
Cas服务等出页面位置:cas\WEB-INF\classes\templates\casLogoutView.html
4.4、启动CAS服务
需要准备tomcat,把cas包放到tomcat/webapps目录下,在tomcat/bin目录下执行startup.bat(windows)或startup.sh(linux)。
启动成功后访问cas地址,界面如下图所示:
5、平台集成CAS
5.1、后端CAS配置
在application-dev(prod).yml 中配置单点登录身份标识类型(跟cas服务配置保持一致)和cas服务地址,如下所示:
yuncheng:
#单点登录身份标识类型,配置通过哪个属性标识唯一用户,对应单点登录界面的登录名,默认为username
# 从用户表查询用户和单点登录返回的用户名都使用该属性
#值范围: username(sys_user表username字段):登录名 phone(sys_user表phone字段):手机号 email(sys_user表email字段):邮箱
#loginNameType: username
#cas单点登录cas:
cas:
prefixUrl: http://cas.example.org:8443/cas
5.2、前端CAS配置
修改public/config/bootConfig.js
VUE_APP_SSO设置为cas
VUE_APP_CAS_BASE_URL配置cas服务地址
//配置即代表开启SSO单点登录,类型值包括 cas oauth2
VUE_APP_SSO:"cas",
//单点登录地址
VUE_APP_CAS_BASE_URL:"http://cas.example.org:8443/cas"
6、业务系统集成CAS
6.1、业务系统同步组织用户
业务系统用户表需要有跟单点用户表保持一致的登录标识字段,如果没有对应的用户标识字段需要添加字段。
单点登录支持三种类型的登录用户标识:username(登录名)、phone(手机号)、email(邮箱),各业务系统要保持统一的登录用户标识,并在cas服务application.properties中配置如下属性:
yuncheng.loginNameType=username(phone/ email)
6.1.1、从门户同步
1、获取所有平台用户
对应接口:List<UserActorImpl> getAllUserList()
接口地址:http://127.0.0.1:30001/api/system/sysOrgConver/getAllUserList (注:接口地址路径以实际为准)
请求类型:get
参数:无
返回值:HttpResult< List<UserActorImpl> >用户对象集合
返回值示例:
{
"code": 200,
"message": "操作成功",
"success": true,
"timestamp": 1630752126366,
"result": [{
"id": "1373536011387523073",
"loginName": "admin",
"name": "管理员",
"createTime": 1577808000000,
"deptId": "1373536559281065985",
"deptCode": " A01A06",
"orgCode": "A01A06",
" deptName": "研发部",
" email": "aaa@163.com",
"phone": "13801066662",
" weixin": "xxxxxx"
},
{
"id": "1373536011387523073",
"loginName": "admin",
"name": "管理员",
"createTime": 1577808000000,
"deptId": "1373536559281065985",
"deptCode": " A01A06",
"orgCode": "A01A06",
" deptName": "研发部",
" email": "aaa@163.com",
"phone": "13801066662",
" weixin": "xxxxxx"
}]
}
后台rest参考:
@RequestMapping(value = "/getAllUserList", 请求类型 = Request请求类型.GET)
public HttpResult<?> getAllUserList() {
return HttpResult.ok(sysOrgConverService.getAllUserList());
}
2、通过用户id获取用户信息
对应接口:UserActorImpl getUserById(String userId)
接口地址:http://127.0.0.1:30001/api/system/sysOrgConver/getUserById?userId=xxxxx (注:接口地址路径以实际为准)
请求类型:get
参数:userId
返回值:HttpResult<UserActorImpl>(用户对象)
返回值示例:
{
"code": 200,
"message": "操作成功",
"success": true,
"timestamp": 1630752126366,
"result": {
"id": "1373536011387523073",
"loginName": "admin",
"name": "管理员",
"createTime": 1577808000000,
"deptId": "1373536559281065985",
"deptCode": " A01A06",
"orgCode": "A01A06",
" deptName": "研发部",
" email": "aaa@163.com",
"phone": "13801066662",
" weixin": "xxxxxx"
}
}
后台rest参考:
@RequestMapping(value = "/getUserById", 请求类型 = Request请求类型.GET)
public HttpResult<?> getUserById(@RequestParam("userId") String userId) {
return HttpResult.ok(sysOrgConverService.getUserById(userId));
}
6.1.2、从钉钉同步
参考在线文档
https://yunchengxc.yuque.com/staff-kxgs7i/public/ir11upm4igg0egr1#OLjQt
6.2、J2ee工程集成CAS
为便于理解,本章以一个springmvc框架的demo工程为示例讲解j2ee工程如何集成cas,该demo工程目录结构如下:
注:该示例截图开发工具使用idea。
6.2.1、引入CAS客户端包
从平台提供的cas.war cas\WEB-INF\lib下拷贝cas-client-core-3.5.1.jar
到web工程WEB-INF/lib目录下
本例使用idea编译运行该工程,需要打开File->Project structure..
在Libraries中导入cas-client-core-3.5.1.jar,如下图所示:
以上配置针对非maven工程,如果业务系统是maven项目需要在pom.xml中加入如下依赖:
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.5.1</version>
</dependency>
6.2.2、配置CAS过滤器
web.xml文件中配置1个监听器,5个过滤器(其中2个可选),顺序需要固定,cas的过滤器配置在项目其他过滤器之前。
Cas过滤器配置顺序如下:
1、SingleSignOutHttpSessionListener和SingleSignOutFilter
这两个组合用于单点登录客户端统一登出处理。
2、 AuthenticationFilter
拦截请求,校验用户是否登录cas,如果没有登录重定向到cas服务登录界面,登录成功后携带ticket回调应用地址.
3、 Cas10TicketValidationFilter
拦截请求,判断请求中有ticket参数时调用cas校验服务校验ticket有效性,校验通过返回用户信息,并将用户信息保存到session中(key: _const_cas_assertion_, 用户org.jasig.cas.client.validation.Assertion).
4、HttpServletRequestWrapperFilter
可选过滤器.
这个过滤器用于将每一个请求对应的HttpServletRequest封装为其内部定义的CasHttpServletRequestWrapper,该封装类将利用之前保存在Session或request中的Assertion对象重写HttpServletRequest的getUserPrincipal()、getRemoteUser()和isUserInRole()方法。这样在我们的应用中就可以非常方便的从HttpServletRequest中获取到用户的相关信息。
5、AssertionThreadLocalFilter
可选过滤器.
这个过滤器会把Assertion对象存放到当前的线程变量中,我们在程序的任何地方都可以从线程变量中获取当前Assertion,就不需要再从Session或request中进行解析了。这个线程变量是由AssertionHolder持有的,我们在获取当前的Assertion时也只需要通过AssertionHolder的getAssertion()方法获取即可,如:Assertion assertion = AssertionHolder.getAssertion();
web.xml 配置示例如下:
<!-- 单点注销监听器 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 单点注销过滤器 -->
<filter>
<filter-name>caslogoutFilter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://192.168.3.111:9080/cas</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>caslogoutFilter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>caslogoutFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!—cas登录校验 -->
<filter>
<filter-name>casAuthenticationFilter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://192.168.3.111:9080/cas/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://192.168.3.240:9080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>casAuthenticationFilter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>casAuthenticationFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter>
<filter-name>casTicketValidationFilter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas10TicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://192.168.3.111:9080/cas</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://192.168.3.240:9080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>casTicketValidationFilter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>casTicketValidationFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
1、参数说明:
casServerUrlPrefix:cas服务地址(ip+端口+项目名)
casServerLoginUrl:cas服务登录地址(cas服务地址+/login)
serverName:cas客户端项目地址(ip+端口)
2、filter-mapping该项目拦截的jsp和action,根据实际项目情况进行调整。
如果原有系统有自己校验登录的全局过滤器或拦截器,原有登录校验失败后再判断cas是否登录。
如原系统过滤器或拦截器从session中获取用户信息,如果能获取到说明登录过,可以继续执行,如果没有则跳转到登录页面. 修改为如果没有获取到用户信息则继续获取cas登录信息,如果能获取到则说明cas登录成功,根据cas登录信息查询原系统用户对象放到session中.
cas服务application.properties配置文件中通过配置如下属性设置cas登录身份标识类型,默认为username,值范围为:username、phone、email,如果配置为phone则需要通过手机号查询用户。
yuncheng.loginNameType=username
登录校验修改示例:
本demo工程中使用自定义拦截器校验session中是否有登录用户。
该拦截器原始代码如下:
package com.itheima.core.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.itheima.core.po.User;
/**
* 登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
// 获取请求的URL
String url = request.getRequestURI();
// URL:除了登录请求外,其他的URL都进行拦截控制
if (url.indexOf("/login.action") >= 0) {
return true;
}
// 获取Session
HttpSession session = request.getSession();
User user = (User) session.getAttribute("USER_SESSION");
// 判断Session中是否有用户数据,如果有,则返回true,继续向下执行
if (user != null) {
return true;
}
// 不符合条件的给出提示信息,并转发到登录页面
request.setAttribute("msg", "您还没有登录,请先登录!");
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp")
.forward(request, response);
return false;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
修改后代码如下:
package com.itheima.core.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.itheima.core.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.itheima.core.po.User;
/**
* 登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
// 依赖注入
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
// 获取请求的URL
String url = request.getRequestURI();
// URL:除了登录请求外,其他的URL都进行拦截控制
if (url.indexOf("/login.action") >= 0) {
return true;
}
// 获取Session
HttpSession session = request.getSession();
User user = (User) session.getAttribute("USER_SESSION");
// 判断Session中是否有用户数据,如果有,则返回true,继续向下执行
if (user != null) {
return true;
}else{
String remoteUser = request.getRemoteUser();
if(StringUtils.isNotEmpty(remoteUser)){
user = userService.findUserByUserCode(remoteUser);
if(user!=null){
session.setAttribute("USER_SESSION", user);
return true;
}
}
}
// 不符合条件的给出提示信息,并转发到登录页面
request.setAttribute("msg", "您还没有登录,请先登录!");
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp")
.forward(request, response);
return false;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
修改内容:
判断session中没有用户信息则获取remoteUser(remoteUser为单点校验成功后由org.jasig.cas.client.util.HttpServletRequestWrapperFilter过滤器将登录用户放入request中),根据remoteUser查询用户表获取用户对象放入session中,并返回ture。
6.2.3、修改项目退出逻辑
在原有系统用户注销逻辑上增加cas注销逻辑,将项目原来退出后跳转到项目登录页面改为重定向到cas退出页面。
示例:
本demo工程退出代码在com.itheima.core.web.controller.UserController.java中,退出原始代码如下:
/**
* 退出登录
*/
@RequestMapping(value = "/logout.action")
public String logout(HttpSession session) {
// 清除Session
session.invalidate();
// 重定向到登录页面的跳转方法
return "redirect:login.action";
}
修改后代码:
/**
* 退出登录
*/
@RequestMapping(value = "/logout.action")
public void logout(HttpSession session, HttpServletResponse response) throws IOException {
// 清除Session
session.invalidate();
String service = "http://192.168.3.240:9080/boot_crm" ;
String casLogoutUrl = "http://192.168.3.111:9080/cas/logout?service=" + service;
// 去退出页面
response.sendRedirect(casLogoutUrl);
// 重定向到登录页面的跳转方法
//return "redirect:login.action";
}
发布于 2023-01-29 21:28・IP 属地北京