前后端分离后台管理系统如何兼容CAS单点登录

背景需求:

        现在大多数系统 都是基于若伊等前后端分离脚手架开发的,有时会遇到需要把多个子系统进行整合,实现统一的登录入口的情况。就是要求从一个单独部署的登录认证中心: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客户端集成完成。

中间会涉及到较多的细节没有写,

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值