背景需求:
现在大多数系统 都是基于若伊等前后端分离脚手架开发的,有时会遇到需要把多个子系统进行整合,实现统一的登录入口的情况。就是要求从一个单独部署的登录认证中心:cas-server 登录以后,就直接可以访问需要进行整合的子系统,无需在子系统这边再次进行登录。
概念:
CAS英文全称Central Authentication Service) 是 Yale 大学发起的一个开源项目
CAS Server
CAS Server 负责完成对用户的认证工作, CAS Server 需要独立部署。
CAS Client
CAS Client 负责部署在客户端,就是在需要整合的子系统中引入相应jar包,并配置过滤器(即需要整合单点登录的第三方 Web 应用系统)
目前, CAS Client 支持非常多的客户端,包括 Java 、 .Net 、 Php 等客户端,几乎可以这样说, CAS 协议能够适合任何语言编写的客户端应用,这里以通用spring boot前后端分离管理系统实践。
实现思路:
统一登录认证服务使用cas-server单独部署,然后各子系统引入cas-client 客户端,在子系统中配置cas 过滤器,在子系统中配置好cas过滤器后,访问子系统的url都会被cas过滤器拦截,通过访问cas-server认证中心的接口进行认证校验,认证失败则跳转到cas-server的登录页面,成功则放行。
子系统配置cas客户端,以若伊前后端分离管理系统为例:
第一步:引入cas客户端maven依赖:
<!-- 单点登录客户端 -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-support-springboot</artifactId>
<version>3.6.4</version>
</dependency>
第二步:配置cas请求过滤器:
application.yml 配置如下:
# SSO 单点登录客户端配置
cas:
loginType: cas
urlPattern: /cas/login
server-url-prefix: http://ip:port/cas
#cas客户端地址
#client-host-url: ip:port
authentication-url-patterns: http://ip:port/cas/
#前端跳转页面
web-home-url: http://ip:port/login-cas
package com.ysgz.framework.config;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
/**
* CAS集成核心配置类
*/
@Configuration
@ConditionalOnProperty(value = "cas.loginType", havingValue = "cas")
public class CasFilterConfig {
private static final Logger log = LoggerFactory.getLogger(CasFilterConfig.class);
/**
* 需要走cas拦截的地址(/* 所有地址都拦截)
*/
@Value("${cas.urlPattern:/*}")
private String filterUrl;
/**
* 默认的cas地址,防止通过 配置信息获取不到
*/
@Value("${cas.server-url-prefix:http://ip:prot/cas}")
private String casServerUrl;
/**
* 认证地址(这个地址需要在cas服务端进行配置)
*/
@Value("${cas.authentication-url-patterns:http://ip:prot/cas/}")
private String authenticationUrl;
/**
* 应用访问地址(这个地址需要在cas服务端进行配置)
*/
@Value("${cas.client-host-url:http://ip:prot}")
private String appServerUrl;
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
log.info(" \n cas 单点登录配置 \n appServerUrl = " + appServerUrl + "\n casServerUrl = " + casServerUrl);
log.info(" servletListenerRegistrationBean ");
ServletListenerRegistrationBean listenerRegistrationBean = new ServletListenerRegistrationBean();
listenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
listenerRegistrationBean.setOrder(HIGHEST_PRECEDENCE);
return listenerRegistrationBean;
}
/**
* 单点登录退出
*
* @return
*/
@Bean
public FilterRegistrationBean singleSignOutFilter() {
log.info(" servletListenerRegistrationBean ");
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new SingleSignOutFilter());
registrationBean.addUrlPatterns(filterUrl);
registrationBean.addInitParameter("casServerUrlPrefix", casServerUrl);
registrationBean.setName("CAS Single Sign Out Filter");
registrationBean.setOrder(HIGHEST_PRECEDENCE);
return registrationBean;
}
/**
* 单点登录认证
*
* @return
*/
@Bean
public FilterRegistrationBean AuthenticationFilter() {
log.info(" AuthenticationFilter ");
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new AuthenticationFilter());
registrationBean.addUrlPatterns(filterUrl);
registrationBean.setName("CAS Filter");
registrationBean.addInitParameter("casServerLoginUrl", casServerUrl);
registrationBean.addInitParameter("serverName", appServerUrl);
registrationBean.setOrder(HIGHEST_PRECEDENCE);
return registrationBean;
}
/**
* 单点登录校验
*
* @return
*/
@Bean
public FilterRegistrationBean Cas30ProxyReceivingTicketValidationFilter() {
log.info(" Cas30ProxyReceivingTicketValidationFilter ");
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
registrationBean.addUrlPatterns(filterUrl);
registrationBean.setName("CAS Validation Filter");
registrationBean.addInitParameter("casServerUrlPrefix", authenticationUrl);
registrationBean.addInitParameter("serverName", appServerUrl);
registrationBean.setOrder(HIGHEST_PRECEDENCE);
return registrationBean;
}
/**
* 单点登录请求包装
*
* @return
*/
@Bean
public FilterRegistrationBean httpServletRequestWrapperFilter() {
log.info(" httpServletRequestWrapperFilter ");
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new HttpServletRequestWrapperFilter());
registrationBean.addUrlPatterns(filterUrl);
registrationBean.setName("CAS HttpServletRequest Wrapper Filter");
registrationBean.setOrder(Integer.MAX_VALUE);
return registrationBean;
}
}
上述过滤器中,最后一个过滤器:HttpServletRequestWrapperFilter 用于装饰request请求,将从cas-server认证中心获取到的用户信息添加到request请求中,但原子系统也会对request进行装饰,所以有可能会被原子系统装饰后拿不到从cas-server获取的用户信息,这时候只需要配置HttpServletRequestWrapperFilter的顺序在原子系统装饰过滤器的后面才可以。
第三步:从request请求中获取cas过滤器从获取cas-server认证中心得到的用户信息,并通过该用户信息获取它所关联的本子系统的用户的用户名和密码,是用该用户名和密码执行本子系统的登录流程,将登录成功后的token返回到本子系统的前端页面中。
package com.ysgz.web.controller.system;
import com.ysgz.common.core.domain.AjaxResult;
import com.ysgz.common.exception.ServiceException;
import com.ysgz.framework.web.service.SysLoginService;
import com.ysgz.system.domain.SysCasUser;
import com.ysgz.system.service.ISysCasUserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.Principal;
/**
* CAS单点登录
*
* @author YinHeng
* @date 2023/10/17 10:42
*/
@RestController
@RequestMapping("/cas")
public class SysCasLoginController {
@Resource
private SysLoginService loginService;
@Resource
private ISysCasUserService casUserService;
@Value("${cas.web-home-url}")
private String webHomeUrl;
/**
* 单点登录
*
* @param request
* @param response
*/
@RequestMapping("/login")
public void casLogin(HttpServletRequest request, HttpServletResponse response, String jsessionid) {
Principal userPrincipal = request.getUserPrincipal();
String userName = userPrincipal.getName();
SysCasUser casUser = casUserService.findByUserName(userName);
if (casUser == null) {
throw new ServiceException("单点登录用户不存在");
}
String token = loginService.casLogin(casUser.getUserName(), casUser.getPassword());
response.setStatus(HttpServletResponse.SC_FOUND);
response.setHeader("Location", webHomeUrl + "?token=" + token);
}
}
第四步:上述代码中通过 request.getUserPrincipal() 方法获取到 cas-server 认证中心登录的用户的信息,里面包含了cas-server认证中心登录的用户关联的本子系统的用户的用户名,通过该用户名获取该用户的登录密码,然后调用本子系统的登录方法进行登录,将登录成功后的token返回给本子系统的指定的前端页面,在本系统的指定的前端页面中将token设置好,然后跳转本子系统的前端欢迎页面,因为已经有了token,前端会自动调用后端接口获取token对应的用户的动态路由、用户信息等。
后端返回token时重定向到该页面,该页面将设置token,然后跳转到欢迎页面,这里使用的时Vue3的语法,Vue2需进行适配一下。下图为login-cas.vue 页面
<template>login-cas.vue 页面</template>
<script setup>
import useUserStore from "@/store/modules/user";
// debugger
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
// 在组件挂载后执行初始化逻辑
const token = route.query.token;
userStore.setToken(token).then(() => {
router.push({ path: "/" });
});
</script>
前端使用的时若伊框架,其他框架大体思路相同,
前端路由配置该vue页面路由:
{
path: '/login-cas',
component: () => import('@/views/login-cas'),
hidden: true,
},
路由守卫设置该页面路由为白名单:
定义一个全局函数用于设置token ,该函数在上面 login-cas.vue 页面被调用:
这里设置好token后跳转欢迎页面,路由守卫发现没有用户信息,会自动访问本子系统后端接口获取用户信息及动态路由等,前后端分离的子系统CAS客户端集成完成。
中间会涉及到较多的细节没有写,