1. 项目概述:为什么我们需要CAS单点登录?

在任何一个稍具规模的企业内部,你大概率会面对这样一个场景:早上打开电脑,登录OA系统处理请假,然后切换到CRM系统查看客户跟进,接着打开项目管理工具Jira更新任务进度,最后还得去财务系统提交报销单。每切换一个系统,就要输入一次用户名和密码,繁琐不说,密码记混了还得重置,一天下来,宝贵的精力全耗在登录上了。这背后暴露的,正是传统多系统独立认证的痛点——用户体验割裂、密码管理复杂、安全策略难以统一。

单点登录(Single Sign-On, SSO)就是为了解决这个问题而生的。它的核心理念很简单: 一次登录,处处通行 。而CAS(Central Authentication Service,中央认证服务)则是实现SSO最经典、最广泛采用的开放协议和框架之一。它由耶鲁大学发起,如今已成为一个成熟的开源项目,被无数企业和组织用于构建统一身份认证中心。

我之所以选择结合Spring Boot和Vue来实践CAS集成,是因为这恰好代表了当前企业级应用开发的主流技术栈:Spring Boot以其“约定大于配置”的理念,能让我们快速构建稳健的后端服务;而Vue则以其渐进式、易上手的特性,成为前端开发者的宠儿。将CAS嵌入到这个技术组合中,意味着我们能构建一个既安全可靠,又拥有现代交互体验的认证体系。无论你是正在为公司的多个微服务寻找统一的登录方案,还是想在自己的个人项目中实践企业级安全架构,这篇从原理到落地的指南都将为你提供一条清晰的路径。

2. CAS单点登录的核心原理与工作流程拆解

在动手写代码之前,我们必须先吃透CAS的工作原理。很多集成失败的问题,根源都在于对流程一知半解。CAS的核心流程可以概括为 两次重定向、一次票据验证 ,其交互涉及三个关键角色: 客户端(Client/Browser) 受保护应用(Service, 即我们的Spring Boot应用) CAS服务器(CAS Server)

2.1 核心票据(Ticket)机制解析

CAS的整个安全体系建立在几种关键的“票据”之上,理解它们就像拿到了打开大门的钥匙:

  1. TGT (Ticket Granting Ticket) :这是CAS Server在用户首次成功登录后,在服务器端创建的一个“总门票”。它关联了用户的身份信息,并拥有一个全局唯一的ID。TGT本身不会发给浏览器,而是存储在CAS Server的会话中(例如Redis或数据库)。
  2. TGC (Ticket Granting Cookie) :这是一个由CAS Server颁发给浏览器端的加密Cookie,其值通常就是TGT的ID。你可以把它理解为“总门票的取票凭证”。浏览器在访问CAS Server的登录页面时会携带此Cookie。CAS Server通过验证TGC,就能找到对应的TGT,从而判断用户是否已登录。 这是实现“一次登录”的关键
  3. ST (Service Ticket) :当用户试图访问某个具体的受保护应用(如我们的Spring Boot服务)时,CAS Server会为该应用生成一个一次性的、短生命周期的票据,这就是ST。ST由TGT签发,并且与特定的Service URL绑定。应用收到ST后,必须向CAS Server验证其有效性,验证通过后即可建立本地会话。

注意 :ST是一次性的,使用后立即失效,这有效防止了票据被截获和重放攻击。TGC是加密的HttpOnly Cookie,增强了安全性。

2.2 标准CAS 3.0协议交互流程详解

让我们跟随一个首次访问的用户,一步步拆解整个流程:

  1. 用户访问受保护应用 :用户浏览器请求 https://app.company.com/home
  2. 应用发现未登录,重定向至CAS Server :我们的Spring Boot应用(集成了CAS Client)的过滤器会拦截该请求,检查本地会话(如HttpSession)中是否存在已登录用户。如果不存在,它会构造一个包含当前应用地址(Service URL)的登录请求,并重定向浏览器到CAS Server的登录地址。例如: https://cas.server.com/login?service=https://app.company.com/home
  3. CAS Server检查TGC :浏览器到达CAS Server登录页。CAS Server首先检查请求中是否携带有效的TGC Cookie。
    • 如果TGC有效 :说明用户已在其他系统登录过。CAS Server直接使用该TGC找到对应的TGT,并跳到第5步,生成ST。
    • 如果TGC无效或不存在 :向用户展示登录表单。
  4. 用户提交凭证登录 :用户输入用户名密码并提交。
  5. CAS Server验证凭证并创建票据 :CAS Server验证凭证通过后,会在服务器端创建一个TGT,并生成一个与之关联的TGC,通过Set-Cookie头种到用户的浏览器。同时,它会为之前请求中带的 service 参数(即我们的应用地址)生成一个唯一的ST。
  6. CAS Server携带ST重定向回应用 :CAS Server将浏览器重定向回最初的Service URL,并在URL后附上ST。例如: https://app.company.com/home?ticket=ST-123456-abcdef
  7. 应用向CAS Server验证ST :我们的Spring Boot应用(CAS Client)从请求参数中获取到这个 ticket (ST),然后在后端( 注意:不是浏览器 )发起一个HTTPS请求到CAS Server的 /serviceValidate /p3/serviceValidate 端点,将ST和自身的Service URL发送过去进行验证。
  8. CAS Server验证并返回用户信息 :CAS Server验证ST是否有效(是否由自己签发、是否未被使用过、是否匹配对应的Service)。验证通过后,返回一个XML格式的响应,其中包含认证成功的标识和用户的基本信息(如username)。
  9. 应用建立本地会话 :Spring Boot应用收到成功的验证响应后,从中解析出用户名(通常是 <cas:user> 标签内的值),然后根据自身业务逻辑,可以查询数据库获取更详细的用户信息、角色权限等。最后,它在自己的会话(HttpSession)中标记该用户为已登录状态,并可能颁发自己的应用级Cookie或Token。
  10. 用户访问应用其他资源 :此后,用户在该应用内的访问,都会由应用的本地会话机制来管理,不再需要经过CAS Server,直到本地会话过期。

这个流程清晰地展示了“单点”是如何工作的:用户只在CAS Server登录一次(步骤4),获得了TGC。之后访问任何集成了CAS Client的应用,都会由CAS Client和CAS Server在后台通过ST完成自动认证(步骤7-9),用户无感知。

3. 环境准备与核心组件选型

理论清晰了,接下来就要搭建战场。一个完整的CAS实践环境需要三部分:CAS Server、Spring Boot CAS Client(后端服务)和Vue前端应用。我们的目标是搭建一个可运行、可调试的本地开发环境。

3.1 CAS Server的部署选择:自建 vs 云服务

这是第一个关键决策点。CAS Server本身是一个复杂的Java Web应用,自建意味着完全的控制权和定制能力,但也带来了部署和维护成本。

  • 方案一:使用Docker快速部署官方CAS Server(推荐用于学习和测试) 这是最快捷的方式。Apereo官方提供了CAS的Docker镜像。

    # 拉取镜像
    docker pull apereo/cas:latest
    # 运行容器,映射端口并挂载配置文件目录
    docker run -d -p 8443:8443 -p 8080:8080 \
      -v /your/local/config/dir:/etc/cas/config \
      -v /your/local/logs/dir:/var/log/cas \
      --name cas-server apereo/cas:latest
    

    你需要准备 /etc/cas/config 目录下的 cas.properties 等配置文件。对于测试,可以使用内置的HTTPS证书和静态用户认证(在配置文件中写死用户名密码)。这能让你在几分钟内拥有一个可用的CAS Server。

  • 方案二:使用云身份提供商(如Authing、Okta)的CAS服务 正如网络资料中提到的Authing,这类服务将CAS Server作为SaaS提供。你无需关心服务器运维、HTTPS证书、高可用等问题,只需在控制台进行配置。这对于中小型团队或想快速验证原型的情况非常友好。你需要在其控制台创建一个应用,启用CAS协议,并获取关键的端点地址:

    • cas.server-url-prefix : CAS服务器基础地址 (如 https://your-domain.authing.cn/cas-idp/xxx )
    • cas.server-login-url : 登录地址 (如 https://your-domain.authing.cn/cas-idp/xxx/login )
    • serviceValidate 地址:票据验证地址。

实操心得 :对于个人学习和小型项目演示,强烈推荐Docker方案,成本最低,且能接触到最原始的CAS配置。对于正式的生产环境,如果团队没有专门的运维人员来维护CAS Server,使用成熟的云身份服务是更稳妥的选择,它们通常还集成了多因素认证、社会化登录等增值功能。

3.2 Spring Boot后端:CAS Client依赖选型

Spring Boot应用作为CAS Client,我们需要一个库来帮我们实现上述流程中的拦截、重定向和票据验证。主流选择有两个:

  1. org.apereo.cas:cas-client-support-springboot :这是Apereo CAS官方维护的Client库,与CAS Server兼容性最好,功能最全,但配置相对繁琐一些。
  2. net.unicon.cas:cas-client-autoconfig-support :这是一个由社区维护的库,以其简洁的自动配置和注解驱动而闻名,在Spring Boot集成中非常流行。网络资料中使用的正是这个库。

在本指南中,我们选择 net.unicon.cas:cas-client-autoconfig-support ,因为它能极大简化配置,让我们更专注于业务逻辑。

3.3 Vue前端:保持无状态与路由守卫

Vue前端应用在这个架构中是纯粹的客户端,它不直接与CAS Server交互,而是通过后端Spring Boot应用来间接完成认证。因此,前端的关键职责是:

  • 接受后端重定向(包括跳转到CAS登录页和携带ST跳回)。
  • 在登录成功后,从后端API获取用户信息并管理前端登录状态。
  • 实现路由守卫,保护需要认证的页面。

我们将使用Vue 3 + Vue Router + Axios的组合。状态管理可以使用Pinia,也可以直接使用响应式变量,视项目复杂度而定。

4. Spring Boot应用集成CAS Client实战

现在,让我们从零开始构建一个集成了CAS Client的Spring Boot应用。我们将创建一个简单的“员工门户”后端,它有一个主页和一个获取当前用户信息的API。

4.1 项目初始化与依赖引入

首先,使用 Spring Initializr 或IDE创建项目。

  • Project : Maven
  • Language : Java
  • Spring Boot : 选择3.x稳定版(如3.1.x)
  • Dependencies : 选择 Spring Web

创建完成后,在 pom.xml 中添加必要的依赖:

<dependencies>
    <!-- Spring Boot Web Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- CAS Client 自动配置支持 -->
    <dependency>
        <groupId>net.unicon.cas</groupId>
        <artifactId>cas-client-autoconfig-support</artifactId>
        <version>3.0.0</version> <!-- 请检查最新版本 -->
    </dependency>
    <!-- 用于解析CAS Server返回的XML响应 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-oxm</artifactId>
    </dependency>
    <!-- 可选,用于简化HTTP请求,替代资料中的Hutool -->
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

4.2 核心配置详解:application.yml

配置文件是集成的灵魂。在 src/main/resources/application.yml 中,我们需要清晰地定义CAS Server和Client的关系。

server:
  port: 8081 # 我们的Spring Boot应用端口

# CAS 客户端配置
cas:
  # CAS服务器的基础URL前缀,通常以/cas结尾
  server-url-prefix: https://localhost:8443/cas
  # CAS服务器的登录URL
  server-login-url: ${cas.server-url-prefix}/login
  # 当前客户端(本应用)的访问地址,必须与CAS Server中注册的Service URL匹配
  client-host-url: http://localhost:8081
  # 哪些URL模式需要被CAS过滤器保护
  validation-type: CAS3 # 使用CAS 3.0协议进行验证
  validation-url-patterns:
    - /api/** # 保护所有/api开头的接口
    - /user/** # 保护用户相关页面
  # 排除某些路径(如静态资源、登录回调端点本身)不需要CAS过滤
  ignore-url-patterns:
    - /public/**
    - /error
    - /favicon.ico
  # 是否自动重定向到CAS Server登录页。通常为true。
  redirect-after-validation: true
  # 自定义属性,用于指定票据验证后,从CAS响应中提取的属性名,映射到Spring Security的权限
  attribute-authorities-role-attribute: roles

关键配置解析

  • client-host-url :这是最容易出错的地方。它必须与你访问Spring Boot应用的地址完全一致(包括协议、域名、端口)。CAS Server在生成ST和验证ST时,会严格校验Service URL是否与此处匹配。 在开发环境, localhost:8081 127.0.0.1:8081 会被CAS视为不同的服务
  • validation-url-patterns :定义了受保护的资源路径。当用户访问这些路径时,CAS过滤器会介入,检查本地会话,若无则发起CAS认证流程。
  • ignore-url-patterns :用于放行不需要认证的公共资源,避免死循环。

4.3 启用CAS Client与自定义用户信息提取

在Spring Boot的主启动类上,添加 @EnableCasClient 注解来启用自动配置。

import net.unicon.cas.client.configuration.EnableCasClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableCasClient
public class SsoPortalApplication {
    public static void main(String[] args) {
        SpringApplication.run(SsoPortalApplication.class, args);
    }
}

CAS Client库会帮我们自动配置过滤器链,处理重定向和票据验证。验证成功后,它会将CAS Server返回的用户名(principal)设置到Spring Security的上下文中。但通常,我们需要的不仅仅是用户名,还有昵称、邮箱、角色等扩展属性。

我们需要实现一个 AuthenticationSuccessHandler 或使用 CasAuthenticationFilter 的回调,来在认证成功后,执行自定义逻辑,比如根据用户名查询数据库,加载完整的用户信息并设置到会话中。

一个更直接的方式是创建一个Controller,作为CAS验证成功后的回调端点(虽然CAS标准流程是后端验证,但我们可以设计一个前端可访问的端点来获取最终用户信息):

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;

@RestController
@RequestMapping("/api/user")
public class UserController {

    @GetMapping("/current")
    public Map<String, Object> getCurrentUser(@AuthenticationPrincipal UserDetails userDetails, HttpSession session) {
        // userDetails.getUsername() 包含了从CAS返回的用户名
        String username = userDetails.getUsername();
        
        // 这里可以模拟或从数据库查询更详细的用户信息
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("username", username);
        userInfo.put("nickname", "员工_" + username);
        userInfo.put("email", username + "@company.com");
        userInfo.put("roles", Arrays.asList("ROLE_USER", "ROLE_EDITOR"));
        
        // 将用户信息存入session,方便本次会话后续使用
        session.setAttribute("currentUser", userInfo);
        
        return userInfo;
    }
    
    @GetMapping("/logout")
    public String logout(HttpSession session) {
        session.invalidate(); // 销毁本地会话
        // 重定向到CAS Server的单点登出端点,实现全局登出
        // return "redirect:https://localhost:8443/cas/logout?service=http://localhost:8081";
        // 注意:实际项目中,登出URL应可配置
        return "登出成功,请关闭浏览器或重新访问应用以触发CAS登录";
    }
}

这个 /api/user/current 接口将在前端登录成功后被调用,用于获取并展示完整的用户信息。 @AuthenticationPrincipal 注解帮助我们方便地获取到CAS认证后的用户主体。

4.4 处理CAS Server的回调与票据验证

net.unicon.cas 库已经帮我们处理了最复杂的部分:拦截未认证请求、重定向到CAS Server、在回调时(携带ST参数)自动向CAS Server的 /serviceValidate 端点发起验证、并根据验证结果建立安全上下文。这个过程对开发者是透明的。

我们只需要确保 application.yml 中的 client-host-url 和CAS Server上注册的Service URL完全一致,否则在票据验证阶段会失败,CAS Server会返回 INVALID_SERVICE 错误。

5. Vue前端应用与CAS的协同实践

前端应用不直接对接CAS协议,它的工作模式是:被后端保护的重定向“驱动”,并通过调用后端API来获取状态。

5.1 项目初始化与路由配置

使用Vue CLI或Vite创建一个新的Vue 3项目。

npm create vue@latest vue-sso-frontend
# 选择项目特性时,加上 Router, Pinia
cd vue-sso-frontend
npm install

安装axios用于HTTP请求:

npm install axios

修改 src/router/index.js ,配置路由和导航守卫:

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import DashboardView from '../views/DashboardView.vue'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
      meta: { requiresAuth: false } // 首页不需要认证
    },
    {
      path: '/dashboard',
      name: 'dashboard',
      component: DashboardView,
      meta: { requiresAuth: true } // 仪表板需要认证
    },
    // ... 其他路由
  ]
})

// 全局前置导航守卫
router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()
  
  // 如果目标路由需要认证
  if (to.meta.requiresAuth) {
    // 检查Pinia store中是否有用户信息
    if (authStore.user) {
      // 已登录,放行
      next()
    } else {
      // 未登录,尝试从后端获取当前用户信息
      try {
        const userInfo = await authStore.fetchUserInfo()
        if (userInfo) {
          // 获取成功,说明CAS流程已走通,本地会话已建立
          next()
        } else {
          // 获取失败,说明未登录。此时直接访问受保护路由,会被后端CAS过滤器拦截并重定向到CAS登录页。
          // 我们只需让浏览器正常发起请求即可。
          // 注意:不要在这里用window.location.href跳转到登录页,那样会丢失原始路由。
          next() // 放行,让请求到达后端,由后端触发302重定向
        }
      } catch (error) {
        console.error('获取用户信息失败:', error)
        // 同样,放行让后端处理
        next()
      }
    }
  } else {
    // 不需要认证的路由,直接放行
    next()
  }
})

export default router

5.2 状态管理与用户信息获取

创建Pinia store ( src/stores/auth.js ) 来管理登录状态:

import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'

// 创建axios实例,配置基础URL和凭据(用于携带Cookie)
const apiClient = axios.create({
  baseURL: 'http://localhost:8081/api', // 指向Spring Boot后端
  withCredentials: true // 非常重要!允许跨域请求携带Cookie(Session ID)
})

export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const isLoading = ref(false)

  // 从后端获取当前用户信息
  const fetchUserInfo = async () => {
    isLoading.value = true
    try {
      const response = await apiClient.get('/user/current')
      user.value = response.data
      return user.value
    } catch (error) {
      console.error('Failed to fetch user info:', error)
      user.value = null
      return null
    } finally {
      isLoading.value = false
    }
  }

  // 登出
  const logout = async () => {
    try {
      await apiClient.get('/user/logout')
      // 注意:后端/logout可能只是清除本地session,需要前端再跳转到CAS全局登出
      user.value = null
      // 可以在这里重定向到CAS Server的登出页面,或者首页
      window.location.href = 'http://localhost:8081/' // 回到首页会触发重新认证
    } catch (error) {
      console.error('Logout failed:', error)
    }
  }

  // 初始化时尝试获取用户信息(例如页面刷新后)
  const init = () => {
    fetchUserInfo()
  }

  return { user, isLoading, fetchUserInfo, logout, init }
})

5.3 关键页面组件与CAS流程配合

HomeView.vue (首页) : 这是一个公共页面,不需要登录即可访问。可以放一个“前往仪表板”的链接。

<template>
  <div class="home">
    <h1>欢迎来到员工门户</h1>
    <p>这是一个单点登录演示系统。</p>
    <router-link v-if="!authStore.user" to="/dashboard">
      <button>前往仪表板 (需要登录)</button>
    </router-link>
    <div v-else>
      <p>您好,{{ authStore.user.nickname }}!</p>
      <router-link to="/dashboard">
        <button>进入仪表板</button>
      </router-link>
      <button @click="handleLogout">退出登录</button>
    </div>
  </div>
</template>

<script setup>
import { useAuthStore } from '@/stores/auth'
import { onMounted } from 'vue'

const authStore = useAuthStore()

onMounted(() => {
  // 页面加载时,尝试初始化用户信息(如果已通过CAS登录)
  authStore.init()
})

const handleLogout = () => {
  authStore.logout()
}
</script>

DashboardView.vue (仪表板) : 这是一个受保护的路由。当用户未登录时访问,路由守卫会放行请求。请求到达Spring Boot后端,被CAS过滤器拦截。由于没有本地会话,过滤器会构造URL重定向浏览器到CAS Server登录页。用户登录成功后,CAS Server带着ST重定向回 /dashboard 页面。此时请求再次到达后端,CAS过滤器验证ST成功,建立本地会话,最终将仪表板页面内容返回给浏览器。同时,前端在 created mounted 钩子中调用 fetchUserInfo() 成功获取到用户数据。

<template>
  <div v-if="authStore.isLoading">加载中...</div>
  <div v-else-if="authStore.user">
    <h1>个人仪表板</h1>
    <p>用户名: {{ authStore.user.username }}</p>
    <p>邮箱: {{ authStore.user.email }}</p>
    <p>角色: {{ authStore.user.roles.join(', ') }}</p>
    <button @click="handleLogout">退出登录</button>
  </div>
  <div v-else>
    <p>未获取到用户信息,请确保已登录。</p>
    <router-link to="/">返回首页</router-link>
  </div>
</template>

<script setup>
import { useAuthStore } from '@/stores/auth'
import { onMounted } from 'vue'

const authStore = useAuthStore()

onMounted(() => {
  // 确保进入页面时用户信息已加载
  if (!authStore.user) {
    authStore.fetchUserInfo()
  }
})

const handleLogout = () => {
  authStore.logout()
}
</script>

6. 联调测试与核心问题排查实录

将CAS Server(Docker运行在8443端口)、Spring Boot应用(8081端口)、Vue前端(默认5173端口)全部启动,就进入了激动人心又充满挑战的联调阶段。

6.1 完整端到端测试流程

  1. 访问公共页面 :打开浏览器,访问 http://localhost:5173 (Vue首页)。应正常显示,且“前往仪表板”按钮可见。
  2. 触发CAS登录 :点击“前往仪表板”按钮,浏览器地址栏会跳转到 http://localhost:8081/dashboard (向后端发起请求)。
  3. 被重定向到CAS :后端CAS过滤器拦截请求,发现未登录,返回302重定向,地址变为 https://localhost:8443/cas/login?service=http://localhost:8081/dashboard 。浏览器跳转到CAS Server登录页面。
  4. 输入凭证登录 :在CAS登录页输入预设的用户名密码(如 casuser / Mellon ,这是Apereo CAS Docker镜像的默认测试账户)。
  5. 重定向回应用并验证 :登录成功,CAS Server生成ST,并重定向浏览器回 http://localhost:8081/dashboard?ticket=ST-XXXXX 。后端CAS过滤器捕获到 ticket 参数,在后台向CAS Server的 /serviceValidate 发起验证。验证成功,建立Session。
  6. 前端获取用户信息 :后端返回仪表板页面(或API数据)。Vue组件加载,执行 fetchUserInfo() ,调用 /api/user/current 接口成功获取用户信息并显示在页面上。
  7. 访问其他受保护资源 :此时在同一个浏览器中,再打开一个新标签页,访问 http://localhost:8081/api/user/current ,应该能直接拿到用户信息,而不再跳转登录页,因为浏览器已持有TGC Cookie,CAS Server认为用户已登录。
  8. 单点登出测试 :点击前端的“退出登录”按钮,调用后端 /api/user/logout ,销毁本地Session。然后再次访问受保护页面, 应该会再次跳转到CAS登录页,但可能发现CAS登录页显示“您已登录” 。这是因为CAS Server端的全局会话(TGT)依然存在。要实现真正的单点登出,需要重定向到CAS Server的 /logout 端点。

6.2 常见问题与解决方案速查表

在集成过程中,你几乎一定会遇到下面这些问题。别慌,对照表格排查:

问题现象 可能原因 排查步骤与解决方案
无限重定向循环 1. CAS Client配置的 client-host-url 与访问地址或CAS Server中注册的Service不匹配。
2. 前端请求未携带Session Cookie(跨域问题)。
3. CAS Server返回的ST验证失败。
1. 检查URL一致性 :确保 application.yml 中的 client-host-url 、你浏览器访问的地址、CAS Server上配置的Service URL 三者完全一致 ,包括 http vs https localhost vs 127.0.0.1 ,端口号。这是最常见的原因。
2. 检查跨域 :确保前端axios请求配置了 withCredentials: true ,且后端Spring Boot配置了正确的CORS策略,允许前端域名和携带凭证。
3. 查看后端日志 :查看Spring Boot应用日志,看CAS过滤器在验证ST时是否报错。
访问页面直接显示CAS返回的XML CAS Server的票据验证接口 ( /serviceValidate ) 被浏览器直接访问了,或者后端验证ST后,没有正确重定向到前端页面,而是直接将XML响应返回给了浏览器。 1. 检查回调逻辑 :确保CAS验证成功后,你的应用逻辑是重定向到前端主页面或设置好Session后返回视图/API数据,而不是直接输出验证接口的原始响应。
2. 检查过滤器链顺序 :确保CAS过滤器配置在正确的顺序,不会拦截到静态资源或错误端点。
登录成功后,前端调用API返回401或跳转登录 后端本地Session建立成功,但前端请求没有携带Session Cookie(JSESSIONID)。 1. 确认CORS :这是典型的跨域Session丢失问题。确保后端CORS配置包含 allowCredentials(true) 和对应的 allowedOrigins (不能是 * )。
2. 检查前端请求 :确认axios实例或每个请求都设置了 withCredentials: true
CAS登录页提示“无效的Service” CAS Server未识别或未授权当前应用(Service)。 1. 检查CAS Server配置 :在CAS Server的配置文件中(如 services 目录下的JSON注册文件),确保注册了你的Service URL,并且 serviceId 模式匹配正确(例如使用正则 ^https?://localhost:8081/.* )。
2. 检查登录URL :确认重定向到CAS的登录URL中, service 参数的值是经过URL编码的。
票据验证失败 (INVALID_TICKET) ST过期、已被使用过,或者验证请求的Service URL与生成ST时的Service URL不匹配。 1. 检查ST生命周期 :ST默认只有几秒到几分钟的有效期。确保网络没有延迟,验证请求及时发出。
2. 检查Service URL一致性 :同“无限重定向”第一点。
3. 查看CAS Server日志 :获取更详细的错误信息。
登出后,重新登录不需要输密码 浏览器中CAS Server的TGC Cookie未清除,CAS Server认为用户仍然在线。 实现 单点登出(SLO) :在前端或后端登出时,不仅要销毁本地Session,还要重定向浏览器到CAS Server的 /logout 端点(可携带 service 参数指定登出后跳转的页面),CAS Server会清除TGC并通知所有注册的应用进行登出。

6.3 生产环境部署关键考量

当你的集成测试通过,准备上线时,这些点需要特别注意:

  • HTTPS是必须的 :CAS协议设计上要求使用HTTPS来保证TGC和ST传输的安全。生产环境的CAS Server和所有Client应用都必须启用HTTPS。自建CAS Server需要配置有效的SSL证书。
  • Session共享 :如果你的Spring Boot应用是多实例部署,本地HttpSession无法共享。你需要将Session存储外部化,例如使用Spring Session集成Redis。
  • CAS Server高可用 :CAS Server作为认证中心,不能是单点故障。需要部署集群,并共享TGT的存储(如使用Redis或数据库存储TGT)。
  • Service注册管理 :生产环境会有很多应用接入。建议在CAS Server端使用基于JSON或JDBC的Service Registry,实现动态注册和管理,而不是写死在配置文件中。
  • 监控与日志 :为CAS Server和Client应用配置详细的日志和监控,便于故障排查和审计。

7. 进阶:扩展用户属性与集成Spring Security

基础的集成完成了用户身份的认证(Authentication)。但在实际项目中,我们还需要授权(Authorization),即基于角色或权限控制访问。同时,我们可能希望从CAS Server获取更多用户属性(如部门、手机号等)。

7.1 从CAS Server获取扩展属性

CAS 3.0协议在验证响应中支持返回自定义属性。你需要在CAS Server端进行配置,使其在认证时从LDAP、数据库等数据源中获取这些属性并放入响应。

在Spring Boot Client端, net.unicon.cas 库在验证ST成功后,可以将这些属性提取出来。你需要调整配置并自定义一个 AuthenticationSuccessHandler

首先,在 application.yml 中启用属性提取:

cas:
  # ... 其他配置
  validation-type: CAS3
  # 指定从CAS响应中提取属性的配置
  attribute-attributes: nickname,email,department # 你期望的属性名

然后,创建一个Handler来接收这些属性:

@Component
public class CasAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        // authentication.getPrincipal() 可能是一个CasAssertion或包含属性的对象
        // 具体类型取决于client库的实现
        // 例如,你可以在这里将属性存储到Session或自定义的UserDetails对象中
        Map<String, Object> attributes = ((CasAssertion) authentication.getPrincipal()).getAttributes();
        String nickname = (String) attributes.get("nickname");
        // ... 处理属性
        
        // 重定向到前端首页或原始请求的页面
        response.sendRedirect("/");
    }
}

并在Security配置中(如果你集成了Spring Security)设置这个处理器。

7.2 与Spring Security深度集成

我们之前的例子简化了安全配置。在更复杂的场景中,你可能需要细粒度的URL权限控制、方法级安全注解等。这时就需要显式地配置Spring Security。

首先,添加Spring Security依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

然后,创建一个Security配置类,将CAS认证与Spring Security的权限体系连接起来:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import net.unicon.cas.client.configuration.CasClientConfigurerAdapter;
import net.unicon.cas.client.configuration.EnableCasClient;

@Configuration
@EnableWebSecurity
@EnableCasClient
public class SecurityConfig extends CasClientConfigurerAdapter {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/public/**", "/error").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/**").authenticated() // 需要认证
                .anyRequest().authenticated() // 默认所有请求都需要认证
            )
            .csrf(csrf -> csrf.disable()) // 根据情况决定是否禁用CSRF,API项目常禁用
            .logout(logout -> logout
                .logoutUrl("/logout") // 本地登出端点
                .logoutSuccessUrl("/") // 登出成功后跳转
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .addLogoutHandler(new SecurityContextLogoutHandler())
            );
        // CAS Client的过滤器会自动添加进来
        return http.build();
    }
    
    // 可以在这里覆盖CasClientConfigurerAdapter的方法,进行更精细的CAS过滤器配置
    @Override
    public void configureAuthenticationFilter(FilterRegistrationBean authenticationFilter) {
        super.configureAuthenticationFilter(authenticationFilter);
        // 例如,设置认证失败后的跳转地址
        // authenticationFilter.getInitParameters().put("failureUrl", "/casfailed");
    }
}

这样,你就拥有了一个由CAS提供统一认证,由Spring Security管理授权和会话的安全后端。你可以使用 @PreAuthorize("hasRole('ADMIN')") 等注解在方法上进行权限控制。

集成CAS单点登录是一个将分散的认证入口收归统一的过程,初期会有些繁琐,但一旦跑通,对于用户体验和系统管理带来的提升是巨大的。记住,耐心调试配置,尤其是URL和协议的一致性,是成功的关键。希望这篇从原理到Spring Boot+Vue实践的详细指南,能帮你顺利搭建起属于自己的单点登录体系。如果在实践中遇到新的问题,多查看日志,善用搜索引擎和社区,你会发现大部分坑都已经有人踩过并给出了答案。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐