应用系统基于CAS实现单点登录的解决方案

单点登录 (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,应用1 发现用户没有登录,于是返回 302,并且携带上一个 service 参数,让用户去 CAS Server 上登录。
  2. 浏览器自动重定向到 CAS Server 上,CAS Server 获取用户 Cookie 中携带的 TGC,去校验用户是否已经登录,如果已经登录,则完成身份校验(此时 CAS Server 可以根据用户的 TGC 找到 TGT,进而获取用户的信息);如果未登录,则重定向到 CAS Server 的登录页面,用户输入用户名/密码,CAS Server 会生成 TGT,并且根据 TGT 签发一个 ST,再将 TGC 放在用户的 Cookie 中,完成身份校验。
  3. CAS Server 完成身份校验之后,会将 ST 拼接在 service 中,返回 302,浏览器将首先将 TGC 存在 Cookie 中,然后根据 302 的指示,携带上 ST 重定向到应用1。
  4. 应用1 收到浏览器传来的 ST 之后,拿去 CAS Server 上校验,去判断用户的登录状态,如果用户登录合法,CAS Server 就会返回用户信息给 应用1。
  5. 浏览器再去访问应用2,应用2 发现用户未登录,重定向到 CAS Server。
  6. CAS Server 发现此时用户实际上已经登录了,于是又重定向回应用2,同时携带上 ST。
  7. 应用2 拿着 ST 去 CAS Server 上校验,获取用户的登录信息。
  8. 在整个登录过程中,浏览器分别和 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": "[email protected]",
"phone": "13801066662",
" weixin": "xxxxxx"
},
{
"id": "1373536011387523073",
"loginName": "admin",
"name": "管理员",
"createTime": 1577808000000,
"deptId": "1373536559281065985",
"deptCode": " A01A06",
"orgCode": "A01A06",
" deptName": "研发部",
" email": "[email protected]",
"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": "[email protected]",
"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 属地北京

猜你喜欢

转载自blog.csdn.net/wxz258/article/details/128794490