前后端分离项目集成单点登录项目CAS5.18

发布于:2025-02-11 ⋅ 阅读:(113) ⋅ 点赞:(0)

之前我在项目中使用过CAS作为单点登录服务,不过那些项目,不管是asp.net MVC项目,还是java的spring boot项目,都是前后端不分的,只须使用CAS的客户端(对于asp.net mvc项目来说,cas的客户端就是DotNetCasClient.dll,而java项目,cas客户端就是一些java包),然后配置一下就好了。现在普遍前后端分离,那该如何集成CAS呢?

一、工作原理及流程

前后端分离项目,集成CAS,简单而言,就是:前端不需要关心单点登录,真正处理单点登录的是后端。前端的数据,以及对数据的操作,都源于后端。前端不需要考察自己的登录状态(其实登录的是浏览器),只需访问后端,后端自然会处理,前端仅仅要做的,就是收到后端返回202编码时,跳转到单点登录页面。这应该是一种代理模式。后端是前端的单点登录代理。具体流程如下:

前端向后端请求或提交数据,后端发现前端尚未登录,于是返回一个202状态码;前端接收到此状态码后,转向单点登录,并在地址后面以参数的形式,附上登录后跳转的地址,这个地址是后端一个接口地址;于是登录成功后,跳转到后端这个接口,在这个接口里面,跳转到前端登录返回后页面。
在这里插入图片描述

下面是详细说明。

二、运行环境

我的前后端分离项目,后端是Spring Boot程序,以此为例做说明。

单点登录服务:CAS 5.18
CAS客户端:cas-client-core 3.3.2
后端框架:Spring Boot2
前端框架:VUE 3

三、后端

后端主要是设置一个过滤器,对来自前端的请求进行是否已登录检查。过滤器基本来自于CAS客户端。但这个客户端需要稍为改写。原因据说是CAS无法将来自前端的AJAX请求直接重定向到单点登录服务器,所以改为向前端返回202状态码,然后前端接收到此状态码后,自行重定向到单点登录。这个我目前还不是很理解,需要进一步了解的同学可自行查阅附录参考文章。
在这里插入图片描述

1、pom.xml

<!-- 单点登录相关 -->
<dependency>
    <groupId>org.jasig.cas.client</groupId>
    <artifactId>cas-client-core</artifactId>
    <version>3.3.2</version>
</dependency>
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.10.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
</dependency>

2、application.yml

cas:
  casSigntouServerUrlPrefix: https://192.168.10.250:11443/cas250/logout
  casServerLoginUrl: https://192.168.10.250:11443/cas250/login
  casValidationServerUrlPrefix: https://192.168.10.250:11443/cas250
  clientBackendUrl: http://192.168.10.8:8090 #后端地址
  clientWebUrl: http://192.168.10.8:8080/#/afterLogin #前端地址

3、过滤器1,禁用ssl认证

不知为啥要这么做,照抄参考文章。反正我的前端和后端都不支持https。在实际应用过程中,我发现前后端分离的项目,如果后端为https,则有问题,似乎后端没有认证成功。可能跟这里的改写有关。是不是不实施这个过滤器就能支持https?不知道。

import javax.net.ssl.*;
import javax.servlet.*;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public class IgnoreSSLValidateFilter implements Filter {
    static {
        //执行设置,禁用ssl认证
        try {
            TrustManager[] trustAllCerts = {new X509TrustManager() {
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] arg0, String arg1)
                        throws CertificateException {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] arg0, String arg1)
                        throws CertificateException {
                }
            }};
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

            HostnameVerifier allHostsValid = new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }

}

4、过滤器2,改写重定向为返回202状态码

其实就是String xRequested =request.getHeader(“x-requested-with”); if(“XMLHttpRequest”.equals(xRequested){…}这里做了更改。不明觉厉。

import org.jasig.cas.client.authentication.*;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.ReflectUtils;
import org.jasig.cas.client.validation.Assertion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.FilterConfig;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


public class MyAuthenticationFilter extends AbstractCasFilter {
    private String casServerLoginUrl;
    private boolean renew = false;
    private boolean gateway = false;
    private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
    private AuthenticationRedirectStrategy authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy();
    private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = null;
    private static final Map<String, Class<? extends UrlPatternMatcherStrategy>> PATTERN_MATCHER_TYPES = new HashMap();

    private static final Logger LOGGER = LoggerFactory.getLogger(MyAuthenticationFilter.class);

    public MyAuthenticationFilter() {
    }

    @Override
    protected void initInternal(FilterConfig filterConfig) throws ServletException {
        if (!this.isIgnoreInitConfiguration()) {
            super.initInternal(filterConfig);
            this.setCasServerLoginUrl(this.getPropertyFromInitParams(filterConfig, "casServerLoginUrl", (String)null));
            LOGGER.trace("Loaded CasServerLoginUrl parameter: {}", this.casServerLoginUrl);
            this.setRenew(this.parseBoolean(this.getPropertyFromInitParams(filterConfig, "renew", "false")));
            LOGGER.trace("Loaded renew parameter: {}", this.renew);
            this.setGateway(this.parseBoolean(this.getPropertyFromInitParams(filterConfig, "gateway", "false")));
            LOGGER.trace("Loaded gateway parameter: {}", this.gateway);
            String ignorePattern = this.getPropertyFromInitParams(filterConfig, "ignorePattern", (String)null);
            LOGGER.trace("Loaded ignorePattern parameter: {}", ignorePattern);
            String ignoreUrlPatternType = this.getPropertyFromInitParams(filterConfig, "ignoreUrlPatternType", "REGEX");
            LOGGER.trace("Loaded ignoreUrlPatternType parameter: {}", ignoreUrlPatternType);
            if (ignorePattern != null) {
                Class<? extends UrlPatternMatcherStrategy> ignoreUrlMatcherClass = (Class)PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType);
                if (ignoreUrlMatcherClass != null) {
                    this.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy) ReflectUtils.newInstance(ignoreUrlMatcherClass.getName(), new Object[0]);
                } else {
                    try {
                        LOGGER.trace("Assuming {} is a qualified class name...", ignoreUrlPatternType);
                        this.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy)ReflectUtils.newInstance(ignoreUrlPatternType, new Object[0]);
                    } catch (IllegalArgumentException var6) {
                        LOGGER.error("Could not instantiate class [{}]", ignoreUrlPatternType, var6);
                    }
                }

                if (this.ignoreUrlPatternMatcherStrategyClass != null) {
                    this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern);
                }
            }

            String gatewayStorageClass = this.getPropertyFromInitParams(filterConfig, "gatewayStorageClass", (String)null);
            if (gatewayStorageClass != null) {
                this.gatewayStorage = (GatewayResolver)ReflectUtils.newInstance(gatewayStorageClass, new Object[0]);
            }

            String authenticationRedirectStrategyClass = this.getPropertyFromInitParams(filterConfig, "authenticationRedirectStrategyClass", (String)null);
            if (authenticationRedirectStrategyClass != null) {
                this.authenticationRedirectStrategy = (AuthenticationRedirectStrategy)ReflectUtils.newInstance(authenticationRedirectStrategyClass, new Object[0]);
            }
        }

    }

    @Override
    public void init() {
        super.init();
        CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
    }

    @Override
    public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        if (this.isRequestUrlExcluded(request)) {
            LOGGER.debug("Request is ignored.");
            filterChain.doFilter(request, response);
        } else {
            HttpSession session = request.getSession(false);
            Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
            if (assertion != null) {
                filterChain.doFilter(request, response);
            } else {
                String serviceUrl = this.constructServiceUrl(request, response);
                String ticket = this.retrieveTicketFromRequest(request);
                boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
                if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
                    LOGGER.debug("no ticket and no assertion found");
                    String modifiedServiceUrl;
                    if (this.gateway) {
                        LOGGER.debug("setting gateway attribute in session");
                        modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
                    } else {
                        modifiedServiceUrl = serviceUrl;
                    }

                    LOGGER.debug("Constructed service url: {}", modifiedServiceUrl);

                    String xRequested =request.getHeader("x-requested-with");
                    if("XMLHttpRequest".equals(xRequested)){
                        response.getWriter().write("{\"code\":202, \"msg\":\"no ticket and no assertion found\"}");
                    }else{
                        String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
                        LOGGER.debug("redirecting to \"{}\"", urlToRedirectTo);
                        this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
                    }
                } else {
                    filterChain.doFilter(request, response);
                }
            }
        }
    }

    public final void setRenew(boolean renew) {
        this.renew = renew;
    }

    public final void setGateway(boolean gateway) {
        this.gateway = gateway;
    }

    public final void setCasServerLoginUrl(String casServerLoginUrl) {
        this.casServerLoginUrl = casServerLoginUrl;
    }

    public final void setGatewayStorage(GatewayResolver gatewayStorage) {
        this.gatewayStorage = gatewayStorage;
    }

    private boolean isRequestUrlExcluded(HttpServletRequest request) {
        if (this.ignoreUrlPatternMatcherStrategyClass == null) {
            return false;
        } else {
            StringBuffer urlBuffer = request.getRequestURL();
            if (request.getQueryString() != null) {
                urlBuffer.append("?").append(request.getQueryString());
            }

            String requestUri = urlBuffer.toString();
            return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);
        }
    }

    static {
        PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class);
        PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class);
        PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class);
    }
}

5、注册配置

import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.CharacterEncodingFilter;

import javax.servlet.Filter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Configuration
public class SsoFilterConfig implements Serializable, InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(SsoFilterConfig.class);

    public static final String CAS_SIGNOUT_FILTER_NAME = "CAS Single Sign Out Filter";
    public static final String CAS_AUTH_FILTER_NAME = "CAS Filter";
    public static final String CAS_IGNOREL_SSL_FILTER_NAME = "CAS Ignore SSL Filter";
    public static final String CAS_FILTER_NAME = "CAS Validation Filter";
    public static final String CAS_WRAPPER_NAME = "CAS HttpServletRequest Wrapper Filter";
    public static final String CAS_ASSERTION_NAME = "CAS Assertion Thread Local Filter";
    public static final String CHARACTER_ENCODING_NAME = "Character encoding Filter";

    //CAS服务器退出地址
    @Value("${cas.casSigntouServerUrlPrefix:https://127.0.0.1:8443/cas/logout}")
    String casSigntouServerUrlPrefix;
    //CAS服务器登录地址
    @Value("${cas.casServerLoginUrl:https://127.0.0.1:8443/cas/login}")
    String casServerLoginUrl;
    //CAS服务器地址
    @Value("${cas.casValidationServerUrlPrefix:https://127.0.0.1:8443/cas}")
    String casValidationServerUrlPrefix;
    //客户端地址(即本系统地址?)
    @Value("${cas.clientBackendUrl:http://127.0.0.1:1234}")
    String clientBackendUrl;

    public SsoFilterConfig() {

    }

    /**
     * 单点登出功能,放在其他filter之前
     * casSigntouServerUrlPrefix为登出前缀:https://123.207.122.156:8081/cas/logout
     *
     * @return
     */
    @Bean
    @Order(0)
    public FilterRegistrationBean getCasSignoutFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(getCasSignoutFilter());
        registration.addUrlPatterns("/*", "*.html");
        registration.addInitParameter("casServerUrlPrefix", casSigntouServerUrlPrefix);
        registration.setName(CAS_SIGNOUT_FILTER_NAME);
        registration.setEnabled(true);
        return registration;
    }

    @Bean(name = CAS_SIGNOUT_FILTER_NAME)
    public Filter getCasSignoutFilter() {
        return new SingleSignOutFilter();
    }

    /**
     * 忽略SSL认证
     *
     * @return
     */
    @Bean
    @Order(1)
    public FilterRegistrationBean getCasSkipSSLValidationFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(getCasSkipSSLValidationFilter());
        registration.addUrlPatterns("/*", "*.html");
        registration.setName(CAS_IGNOREL_SSL_FILTER_NAME);
        registration.setEnabled(true);
        return registration;
    }

    @Bean(name = CAS_IGNOREL_SSL_FILTER_NAME)
    public Filter getCasSkipSSLValidationFilter() {
        return new IgnoreSSLValidateFilter();
    }

    /**
     * 负责用户的认证
     * casServerLoginUrl:https://123.207.122.156:8081/cas/login
     * casServerName:https://123.207.122.156:8080/tdw/alerts/
     *
     * @return
     */
    @Bean
    @Order(2)
    public FilterRegistrationBean getCasAuthFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        final Filter casAuthFilter = getCasAuthFilter();
        registration.setFilter(casAuthFilter);
        registration.addUrlPatterns("/*", "*.html");
        registration.addInitParameter("casServerLoginUrl", casServerLoginUrl);
        registration.addInitParameter("serverName", clientBackendUrl);
        registration.setName(CAS_AUTH_FILTER_NAME);
        registration.setEnabled(true);
        return registration;
    }

    @Bean(name = CAS_AUTH_FILTER_NAME)
    public Filter getCasAuthFilter() {
        return new MyAuthenticationFilter();
    }

    /**
     * 对Ticket进行校验
     * casValidationServerUrlPrefix要用内网ip
     * casValidationServerUrlPrefix:https://123.207.122.156:8081/cas
     * casServerName:https://123.207.122.156:8080/tdw/alerts/
     *
     * @return
     */
    @Bean
    @Order(3)
    public FilterRegistrationBean getCasValidationFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        final Filter casValidationFilter = getCasValidationFilter();
        registration.setFilter(casValidationFilter);
        registration.addUrlPatterns("/*", "*.html");
        registration.addInitParameter("casServerUrlPrefix", casValidationServerUrlPrefix);
        registration.addInitParameter("serverName", clientBackendUrl);
        registration.setName(CAS_FILTER_NAME);
        registration.setEnabled(true);
        return registration;
    }

    @Bean(name = CAS_FILTER_NAME)
    public Filter getCasValidationFilter() {
        return new Cas20ProxyReceivingTicketValidationFilter();
    }

    /**
     * 设置response的默认编码方式:UTF-8。
     *
     * @return
     */
    @Bean
    @Order(4)
    public FilterRegistrationBean getCharacterEncodingFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(getCharacterEncodingFilter());
        registration.addUrlPatterns("/*", "*.html");
        registration.setName(CHARACTER_ENCODING_NAME);
        registration.setEnabled(true);
        return registration;
    }

    @Bean(name = CHARACTER_ENCODING_NAME)
    public Filter getCharacterEncodingFilter() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        return characterEncodingFilter;
    }

    @Bean
    public FilterRegistrationBean casHttpServletRequestWrapperFilter(){
        FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();
        authenticationFilter.setFilter(new HttpServletRequestWrapperFilter());
        authenticationFilter.setOrder(6);
        List<String> urlPatterns = new ArrayList<>();
        urlPatterns.add("/*");
        authenticationFilter.setUrlPatterns(urlPatterns);
        return authenticationFilter;
    }

    @Override
    public void afterPropertiesSet() throws Exception {

    }
}

6、给前端和CAS的相关接口

//import 自定义的 巴拉巴拉.server.modules.utils.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Api(tags = "cas单点登录接口")
@Controller
@RequestMapping("/cas")
public class LoginController {

    @Value("${cas.clientWebUrl}")
    String webUrl;

    @ApiOperation(value = "测试接口")
    @GetMapping("/test")
    @ResponseBody
    public String login(HttpServletRequest request) {
        System.out.println(request.getRemoteUser().toString());
        return "success";
    }

    @ApiOperation(value = "票根验证", notes = "验证通过时转发到前端主页")
    @GetMapping("/checkTicket")
    public void index(HttpServletResponse response) throws IOException {
        // 前端页面地址
        response.sendRedirect(webUrl);
    }

    @ApiOperation(value = "获取系统当前登录的用户名与用户名白名单")
    @GetMapping("/getUsername")
    @ResponseBody
    public Result getUsername(HttpServletRequest request) {
        Map<String, String> map = new HashMap<>();
        map.put("currentUser", request.getRemoteUser());

        return Result.ok().put("data",map);
    }
}

四、前端

前端不需要进行登录状态检查。它只在收到后端返回的202状态码时才主动重定向到单点登录。除此之外,它提供了一个登录后返回页面,记录一下登录账号,并凭此到后端获取该账号的详细信息。当然专门设置一个返回页面不是必须的。可见,由始至终,前端并没有校验登录状态,也没有存储什么登录状态,它是否登录,以及是否需要登录,都是由后端控制的。在它向后端请求数据、或提交数据时,由后端的过滤器处理。

前端涉及到处理的模块主要有:

1)请求处理程序
2)路由卫士
3)登录后返回页面
在这里插入图片描述

1、请求处理程序request/index.js

import axios from "axios";

let redirectFlag = false;
const onResponse = (response) => {
  const code = response.data.code;
  if (code === 202 && !redirectFlag) {//转向单点登录
    redirectFlag = true;
    window.location.href = appConfig.cas.casRedirectUrl;
  } else {
    。。。
  }
};

。。。

const getService = (config) => {
  const service = axios.create(config);

  // 添加请求拦截器
  service.interceptors.request.use(onRequest, onRequestError);
  // 添加响应拦截器
  service.interceptors.response.use(onResponse, onResponseError);

  return service;
};

const DEFAULTCONFIG = {
  baseURL: "/api", // 所有的请求地址前缀部分
  timeout: 60000, // 请求超时时间毫秒
  withCredentials: true, // 异步请求携带cookie
  headers: {
    "X-Requested-With": "XMLHttpRequest",
  },
};

const service = getService(DEFAULTCONFIG);

export default service;

2、路由卫士router/index.js

import { createRouter, createWebHashHistory } from "vue-router";

const router = createRouter({
  history: createWebHashHistory(),
  routes,//一些路由规则
});

// 路由守卫
const normalRouter = (to, from, next) => {
  if (to.path === "/afterLogin") {//必须保证登录返回页面顺利加载
    next();
  } else {
    //这部分代码与结合单点登录无关,由前端项目按照自己的逻辑自行定义
    myRouterDo(to, from, next);
  }
};

router.beforeEach(normalRouter);

export default router;

//自定义逻辑
function myRouterDo(to, from, next) {
	。。。
}

3、登录后返回页面

以下代码中,getUserNameCAS()向后端获取登录账号;而在loginDo方法中,我会凭登录后账号向后端请求该账号的详细信息,做一些。

1)页面afterLogin.vue

在这里插入图片描述

<template>
  <div>
    <h1>===== 欢迎使用VUE3 =====</h1>
  </div>

  <div v-if="state.userName === null">
    <span>未登录</span>
  </div>

  <div v-if="state.userName !== null">
    <div class="flex-box navis">
      <div>框架首页</div>
      <div>
        <el-link type="primary" underline="true" href="/#/business">业务首页</el-link>
      </div>
    </div>
    <div class="flex-box user-info">
      <div><span>用户名:</span><span>{{ state.userName }}</span></div>
      <div>
        <el-button type="primary" @click="logout" class="green-button">登出</el-button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { reactive, onMounted, nextTick } from "vue";
import { getUserName as getUserNameApi } from "@/api/cas.js";
import { logout } from "@/utils/auth.js";

const state = reactive({
  userName: null
});

onMounted(() => {
  nextTick(() => {
    getUserInfo();
  });
});

function getUserInfo() {
  getUserNameApi().then((res) => {
    state.userName = res.data.currentUser;
  });
}
</script>

<style scoped>
</style>

2)后端接口api/cas.js

import { request } from "@/request";

const appConfig = require("/public/web-config.json");
const $cas = appConfig.cas;

//目前没有用到
export const login = () => {
  return request({
    url: `/cas/checkTicket`,
    method: "get",
  });
};

export const logout = () => {
  window.location.href = $cas.casLogout;
};

export const getUserName = () => {
  return request({
    url: `/cas/getUsername`,
    method: "get",
  });
};

五、小结

从流程看,跳转到单点登录是前端的主动行为,似乎后端并没有参与单点登录,它是如何代理前端的登录状态事宜的?

首先要厘清的是,所谓的登录,既不是前端登录,也不是后端登录,其实是浏览器登录,或者说,登录的载体是浏览器。其次,所谓的代理前端的登录状态事宜,即是后端考察浏览器的登录状态。当浏览器访问前端,转而访问后端,每一个请求,后端都会向CAS求证,它登录了吗?合法吗?后端是CAS客户端,单点登录后也是先返回后端,这一过程中,后端与CAS之间,早已暗通款曲,建立了秘密联系的通道。因此后端的确可以担负起考察浏览器登录状态的重任。

在这里插入图片描述

CAS作为单点登录服务,应该是很成熟的吧,用起来,只要配置得当,还是很丝滑的。不过,我印象中,CAS部署起来十分麻烦。调试也很麻烦,以前经常遇到重定向次数过多的问题。如果这次不是有现成的部署,我不一定会选用它来做单点服务器。

实际运行过程中,发现如果要集成这个CAS的话,前端和后端必须是同一台机器,否则CAS不认这个登录结果,前端页面于是在不停地狂闪。也不知道是在前端和CAS之间、还是前端和后端之间狂闪,抑或是三者之间踢皮球。猜测是最后第一种,登录成功以后,前端向后端请求数据,后端向CAS求证失败,于是指示前端跳转到CAS,来到CAS后,CAS发现来者已经登录,于是跳回前端,于是新一轮轮回开始了。猜测这其中涉及到cookie。因为我用的是IP作为前后端的地址,对于cookie来说,IP相同,端口不同,仍然算是同域;如果IP不同就不行了。所以如果前后端真的要分开部署在不同机器的话,恐怕只能用域名了。

另外,前后端项目部署于容器,比如docker,似乎也不行。没有时间确认,先这样吧。

六、单点登录与oAuth2

似乎单点登录与oAuth2有许多相同之处。oAuth2,使用第三方系统作为身份识别和授权服务,主要是授权。比如,我们使用微信来授权登录。当用户访问我们系统A时,首先转到微信,微信会询问用户,你同意系统A获取你的微信身份信息吗?用户同意后,微信就将用户的一些身份信息返回到我们系统A,我们就拿到了用户的身份,然后可以认为他登录了。从无须打造自己的登录服务这点来看,单点登录和oAuth2的效果是一样的。

但是,oAuth2并没有所谓一处登录,处处可使用的效果。每个使用微信的应用都是独立的,你登录了系统A,并不能直接访问系统B。同时,单点登录有单点登出,即从一个应用登出以后,其他的应用也都登出了;而oAuth2显然也没有这种功能。

我的感觉是,局域网应用,适合使用单点登录;互联网,适合使用oAuth2。

附录:参考资料

后端代码基本上抄自以下文章。

springboot+vue集成cas单点登录

SpringBoot+Vue+CAS 前后端分离实现单点登录方案


网站公告

今日签到

点亮在社区的每一天
去签到