Springboot集成CAS认证客户端(前后端分离)

转载请注明出处

CAS简介

CAS(Central Authentication Service,中央认证服务)是一种单点登录(SSO)协议,允许用户在一个系统中登录后,无需再次登录即可访问其他受信任的系统。以下是CAS认证系统的关键点介绍:

核心概念

  • CAS Server:负责用户认证的中心服务器。用户首次登录时,CAS Server验证用户凭证(如用户名和密码)。
  • CAS Client:集成到各个应用中的客户端库,用于与CAS Server通信,验证用户是否已登录。
  • Ticket:CAS使用两种票据进行认证:
    • TGT(Ticket Granting Ticket):用户登录成功后,CAS Server颁发给用户的长期票据。
    • ST(Service Ticket):用户访问特定服务时,CAS Server颁发的短期票据。

认证流程

  • 1.用户访问应用:用户尝试访问一个受保护的应用。
  • 2.重定向到CAS Server:应用检测到用户未登录,将用户重定向到CAS Server。
  • 3.用户登录:用户在CAS Server输入凭证进行登录。
  • 4.颁发TGT:CAS Server验证凭证后,颁发TGT并存储在用户的浏览器中。
  • 5.颁发ST:CAS Server生成ST并重定向用户回应用,附带ST。
  • 6.验证ST:应用将ST发送到CAS Server进行验证。
  • 7.访问应用:验证通过后,用户被允许访问应用。

优点

  • 单点登录:用户只需登录一次,即可访问多个应用。
  • 安全性:通过票据机制,减少了密码在网络中的传输次数。
  • 集中管理:认证逻辑集中在CAS Server,便于管理和维护。

实现

  • Java:CAS提供了Java客户端库,可以轻松集成到Java应用中。
  • 其他语言:CAS也支持多种编程语言的客户端库,如PHP、Python等。

CAS认证客户端实现

实现思路

关于CAS的客户端实现有很多种方式, 可以自己编写过滤器实现关键步骤的对应跳转. 或者引用一些客户端来实现. 本文中后面的实现方式主要是基于 cas-client-autoconfig-support 来实现对接CAS的服务端, 这个实现中封装了和CAS服务端对接的相关逻辑. 但是也不能直接拿来就用需要稍微改动一下.

为什么说要稍微改动一下, 因为这个登录认证是基于 session 来实现的. 也就是当访问某个资源的时候, 如果没有认证需要在保持在同一个会话的前提下, 客户端重定向到CAS Server进行登录认证操作. cas-client-autoconfig-support 这个实现已经封装了这些处理和操作, 并且默认将拦截全部的资源(/**). 当请求 CAS Client 服务端 的时候, 它会自己验证有没有认证, 没有认证会跳转到服务端, 登录完成后会跳转回来.

但是这里面临一个问题, 现在大多数都是前后端分离的项目, 前端和和后端交互的时候基本都是Ajax的请求. 也就是前端的请求到CAS Client服务端如果没有认证, 后端虽然会返回一个 重定向302 但是前端的页面是不会跳转的. 没办法触发重定向, 所以也就没办法跳转到 CAS Server. 下面来处理一下这个问题.

刚才说 cas-client-autoconfig-support 默认会拦截全部的资源 (/**), 我们就通过配置让他拦截一个固定的路径, 只有请求这个路径才会跳转到 CAS Server 去认证. 其他的接口放行. 对于这部分接口然后我们自己编写一个过滤器, 在过滤器里获取用户信息, 如果获取不到用户信息(未认证的情况), 后端直接返回前端一个状态码(401). 前端接受到这个状态码之后主动通过URL请求后端的一个接口. 这个请求是URL请求方式. 然后后端服务就可以随意进行重定向, 包括跳转到CAS Server等一系列操作, 当CAS Server 认证之后又会重定向到 CAS Client 服务端, 然后我们再把这个请求重定向到用户最开始访问的页面即可.

这里我们主要介绍4个关键接口:

  • /cas-client/call-back 登录回调接口
    • 这个接口就是我们要配置被 cas-client-autoconfig-support 拦截的接口
  • /cas-client/login?redirectUrl=登录成功后回调地址
    • 这个接口是当前端收到401的时候, 需要URL请求的登录接口, 不被 cas-client-autoconfig-support 拦截
  • /cas-client/logout?redirectUrl=登录退出后回调地址
    • 这个接口是前端主动退出登录, 需要 UR请求 的接口, 不被 cas-client-autoconfig-support 拦截
  • /cas-client/get-user
    • 这个接口是供前端通过AJAX请求查询用户的接口 , 不被 cas-client-autoconfig-support 拦截

认证流程

  • 1.用户访问页面, 前端首先会Ajax请求 /get-user 接口, 没有认证, 后端返回401
  • 2.前端URL请求/login?redirectUrl=/aaa/bbb 并设置登录成功的回调地址
  • 3.后端获取到 /login?redirectUrl=/aaa/bbb 中的地址, 在后端服务生成一个随机ID(state), 将这个state/aaa/bbb 建立关系存起来
  • 4.后端将请求重定向至/cas-client/call-back接口, 并将上一步生成的state作为参数携带(/cas-client/call-back?state=123456)
  • 5./cas-client/call-back?state=123456 会被 cas-client-autoconfig-support 拦截, 这个时候就进入了CAS正常的登录流程. 重定向到CAS Server 登录, 并且框架会自动将被拦截的资源(/cas-client/call-back?state=123456)携带.
  • 6.登录成功之后, CAS Server 又会重定向到 /cas-client/call-back?state=123456 这个时候. 因为已经登录认证过了, 请求不再被拦截会正常进入我们的业务逻辑.
  • 7.当已认证的请求进入/cas-client/call-back?state=123456后, 我们查询存储中state=123456 对应的回调地址/aaa/bbb, 然后将请求重定向到(302)/aaa/bbb
  • 8.当页面回到/aaa/bbb 的时候已经被认证过. 前端再次 Ajax请求 /get-user 接口就可以获取到用户信息了.

在上述流程中, 前端仅需要做第 1 2 和两个步骤. 剩下的都由后端来处理
在上述流程中, 第2 3 4 步骤中的传参不是必须的, 也就是state对应回跳地址的这一步,

  • 你可以写成一个固定页面, 比如系统的首页, 那么请求/login?redirectUrl=/aaa/bbb的时候redirectUrl=/aaa/bbb 就可以省略
  • 如果请求的地址比较短, 也可以直接将地址拼. 如/cas-client/call-back?redirectUrl=/aaa/bbb

代码实现

pom.xml依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>

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

        <dependency>
            <groupId>net.unicon.cas</groupId>
            <artifactId>cas-client-autoconfig-support</artifactId>
            <version>2.3.0-GA</version>
        </dependency>

		<!-- hutool 工具类, 非必须. 可以换成你喜欢的 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.35</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
相关配置
# ------------------------------- CAS单点登陆配置  start ---------------
# 本地服务地址
server.host=http://demo.test.cn
# 本地服务端口号
server.port=9090
# 登录成功后默认跳转地址
cas-filter.home-index-url=http://demo.test.cn
# CAS认证服务器前缀
cas.server-url-prefix=http://server.test.cn
# ------------------------------- CAS单点登陆配置  end -----------------


# ------------------------------- CAS单点登陆默认配置  start ------------
# CAS认证服务器登录地址
cas.server-login-url=${cas.server-url-prefix}/login
# CAS认证服务器登出地址
cas-filter.server-logout-url=${cas.server-url-prefix}/logout
# CAS认证客户端(本地)地址前缀
cas.client-host-url=${server.host}:${server.port}
# 协议类型
cas.validation-type=cas
# 鉴权地址
cas.authentication-url-patterns[0]=/cas-client/call-back
# ------------------------------- CAS单点登陆默认配置  end -------------
URL映射常量

我在这里没有写 controller, 上面说的登录、登出获取用户接口,都直接在 Filter中实现了

package com.project.util;

/**
 * Url mapping constant
 *
 * @author Felix
 * @date 2025/02/13
 */
public class UrlMappingConstant {
    /**
     * state
     */
    public static final String CAS_LOGIN_STATE_KEY = "state";

    /**
     * Interceptor prefix
     */
    public static final String INTERCEPTOR_PREFIX = "/cas-client/";
    /**
     * Login url
     */
    public static final String CAS_LOGIN_URL = INTERCEPTOR_PREFIX + "login";
    /**
     * Login callback url
     */
    public static final String CAS_LOGIN_CALLBACK_URL = INTERCEPTOR_PREFIX + "call-back";
    /**
     * Logout url
     */
    public static final String CAS_LOGOUT_URL = INTERCEPTOR_PREFIX + "logout";
    /**
     * Get user url
     */
    public static final String CAS_GET_USER_URL = INTERCEPTOR_PREFIX + "get-user";
}

核心Filter.java
package com.project.filter;

import cn.hutool.core.text.CharSequenceUtil;
import com.project.bean.CasUserInfo;
import com.project.util.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;

import javax.annotation.PostConstruct;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
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;
import java.util.function.BiConsumer;

import static com.project.util.UrlMappingConstant.*;

/**
 * CAS Filter
 *
 * @author Felix
 * @date 2025/02/14
 */
@Slf4j
@WebFilter(filterName = "com.server.CasFilter")
@Configuration
public class CasFilter implements Filter {

    @Value("${cas-filter.home-index-url:}")
    private String homeIndexUrl;

    @Value("${cas-filter.server-logout-url:}")
    private String casServerLogoutUrl;

    @Value("${cas-filter.redirect-url-key:redirectUrl}")
    private String redirectUrlKey;

    /**
     * URL HANDLER
     */
    private final Map<String, BiConsumer<HttpServletRequest, HttpServletResponse>> filterHandlers = new HashMap<>(4);

    @PostConstruct
    private void initHandlers() {
        log.info(">>> Init cas handler.");
        filterHandlers.put(CAS_LOGIN_URL, this::login);
        filterHandlers.put(CAS_LOGIN_CALLBACK_URL, this::loginCallBack);
        filterHandlers.put(CAS_LOGOUT_URL, this::logout);
        filterHandlers.put(CAS_GET_USER_URL, this::getUserInfo);
    }


    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String requestUri = CharSequenceUtil.trim(request.getRequestURI());

        for (Map.Entry<String, BiConsumer<HttpServletRequest, HttpServletResponse>> entry : filterHandlers.entrySet()) {
            String matchUri = entry.getKey();
            if (FilterUtil.urlMatch(matchUri, requestUri)) {
                BiConsumer<HttpServletRequest, HttpServletResponse> handler = entry.getValue();
                handler.accept(request, response);
                return;
            }
        }

        filterChain.doFilter(request, response);
    }


    /**
     * Log in
     *
     * @param request  request
     * @param response response
     */
    private void login(HttpServletRequest request, HttpServletResponse response) {
        String loginSuccessRedirectUrl = this.getWebRedirectUrl(request);
        log.info(">>> Login success redirect url: {}", loginSuccessRedirectUrl);

        String state = PrimaryKeyUtil.nextConfuseId();
        CasCacheUtil.put(state, loginSuccessRedirectUrl);

        String casServerLoginUrl = CAS_LOGIN_CALLBACK_URL + "?" + CAS_LOGIN_STATE_KEY + "=" + state;
        log.info(">>> Login server callback url: {}", casServerLoginUrl);
        FilterUtil.redirectPage(response, casServerLoginUrl);
    }

    /**
     * Login callback
     *
     * @param request  request
     * @param response response
     */
    private void loginCallBack(HttpServletRequest request, HttpServletResponse response) {
        String loginSuccessRedirectUrl = homeIndexUrl;
        String state = request.getParameter(CAS_LOGIN_STATE_KEY);
        if (CharSequenceUtil.isNotBlank(state)) {
            String loginSuccessRedirectUrlCache = CasCacheUtil.get(state);
            if (CharSequenceUtil.isNotBlank(loginSuccessRedirectUrlCache)) {
                loginSuccessRedirectUrl = loginSuccessRedirectUrlCache;
                CasCacheUtil.del(state);
            }
        }

        CasUserInfo userInfo = CasUserUtil.getUserInfoFromCas(request);
        if (null == userInfo) {
            FilterUtil.returnJson(response, HttpStatus.INTERNAL_SERVER_ERROR, ApiResult.error(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), "User resolution exception"));
        } else {
            log.info(">>> Current login user: {}", userInfo.getName());
            log.info(">>> Login callback to url: {}", loginSuccessRedirectUrl);
            FilterUtil.redirectPage(response, loginSuccessRedirectUrl);
        }
    }

    /**
     * logout
     *
     * @param request  request
     * @param response response
     */
    private void logout(HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession();
        if (null != session) {
            session.invalidate();
        }
        String logoutRedirectUrl = this.casServerLogoutUrl + "?service=" + this.getWebRedirectUrl(request);
        log.info(">>> Logout redirect url: {}", logoutRedirectUrl);
        FilterUtil.redirectPage(response, logoutRedirectUrl);
    }

    /**
     * Get user information
     *
     * @param request  request
     * @param response response
     */
    private void getUserInfo(HttpServletRequest request, HttpServletResponse response) {
        CasUserInfo userInfo = CasUserUtil.getUserInfoFromCas(request);
        if (null == userInfo) {
            FilterUtil.returnJson(response, HttpStatus.UNAUTHORIZED, ApiResult.error(String.valueOf(HttpStatus.UNAUTHORIZED.value()), "Unauthorized"));
        } else {
            log.debug(">>> Current query user: {}", userInfo.getName());
            FilterUtil.returnJson(response, HttpStatus.OK, ApiResult.ok(userInfo));
        }
    }

    /**
     * Gets the web redirect url
     *
     * @param request request
     * @return {@link String }
     */
    private String getWebRedirectUrl(HttpServletRequest request) {
        String redirectUrl = request.getParameter(redirectUrlKey);
        if (CharSequenceUtil.isBlank(redirectUrl)) {
            redirectUrl = homeIndexUrl;
        }
        return redirectUrl;
    }
}
CasUserUtil.java 用户解析工具类
package com.project.util;


import com.project.bean.CasUserInfo;
import lombok.extern.slf4j.Slf4j;
import org.jasig.cas.client.validation.Assertion;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;


/**
 * CAS User Util
 *
 * @author Felix
 * @date 2025/02/12
 */
@Slf4j
public class CasUserUtil {

    /**
     * Default session key of the cas client
     */
    public static final String CAS_SESSION_KEY = "_const_cas_assertion_";

    /**
     * Obtain cas user information
     *
     * @param request request
     * @return {@link CasUserInfo }
     */
    public static CasUserInfo getUserInfoFromCas(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }

        Object object = session.getAttribute(CAS_SESSION_KEY);
        if (null == object) {
            return null;
        }

        Assertion assertion = (Assertion) object;
        return buildCasUserInfoByCas(assertion);
    }


    /**
     * Build cas user information
     *
     * @param assertion assertion
     * @return {@link CasUserInfo }
     */
    private static CasUserInfo buildCasUserInfoByCas(Assertion assertion) {
        if (null == assertion) {
            log.error(">>> The Cas does not obtain the user. Procedure ");
            return null;
        }

        try {
            CasUserInfo casUserInfo = new CasUserInfo();
            String name = assertion.getPrincipal().getName();
            Map<String, Object> attributes = assertion.getPrincipal().getAttributes();
            casUserInfo.setName(name);
            casUserInfo.setAttributes(attributes);
            return casUserInfo;
        } catch (Exception exp) {
            log.error(">>> The user conversion error: ", exp);
            return null;
        }
    }
}
FilterUtil.java
package com.project.util;

import cn.hutool.core.text.AntPathMatcher;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * Filter util
 *
 * @author Felix
 * @date 2025/02/12
 */
@Slf4j
public class FilterUtil {

    private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

    /**
     * Url matching
     *
     * @param pattern pattern
     * @param path    path
     * @return boolean
     */
    public static boolean urlMatch(String pattern, String path) {
        return ANT_PATH_MATCHER.match(pattern + "*", path);
    }

    /**
     * Return response message
     *
     * @param response response
     * @param status   status
     * @param result   result
     */
    public static void returnJson(HttpServletResponse response, HttpStatus status, ApiResult<Object> result) {
        response.setStatus(status.value());
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");
        try {
            PrintWriter pw = response.getWriter();
            pw.write(JSONUtil.toJsonStr(result));
            pw.flush();
        } catch (IOException e) {
            log.error(">>> Return json failure", e);
            throw new RuntimeException(e);
        }
    }


    /**
     * Redirect page
     *
     * @param response    response
     * @param redirectUrl redirect url
     */
    public static void redirectPage(HttpServletResponse response, String redirectUrl) {
        try {
            response.setStatus(HttpStatus.SEE_OTHER.value());
            response.sendRedirect(redirectUrl);
        } catch (IOException e) {
            log.error(">>> Redirection failure", e);
            throw new RuntimeException(e);
        }
    }
}

以上便是登录认证所需的关键代码片段了, 涉及的存储部分,请自行替换即可。

关于登出的流程比较简单, 直接重定向即可, 上面的 CasFilter.java 中已经包含了, 这里不再过多解释.

### 回答1: Spring Boot CAS 前后端分离是一种常见的架构模式,它将前端和后端分离开来,使得前端和后端可以独立开发、测试和部署。CAS(Central Authentication Service)是一种单点登录系统,它可以实现用户在多个应用系统中的单点登录和注销。在 Spring Boot CAS 前后端分离架构中,前端通过 AJAX 请求后端的 RESTful API 来获取数据,后端则负责处理业务逻辑和数据存储。CAS 服务器则负责用户认证和授权,通过 CAS 协议来实现单点登录和注销。这种架构模式可以提高系统的可维护性、可扩展性和安全性,同时也可以提高开发效率和用户体验。 ### 回答2: Spring Boot是一个流行的Java框架,用于快速创建基于Spring的应用程序。CAS(Central Authentication Service)是一个单点登录协议,允许用户一次登录即可在多个应用程序中访问受保护的资源。Spring BootCAS可以很好地结合使用,以实现前后端分离的应用程序。 前后端分离是指将应用程序的前端和后端分开发展。前端通常由JavaScript、HTML和CSS构建,使用框架如Vue、React或Angular。后端使用Java或其他语言,通常使用Spring Boot等框架开发。在前后端分离的应用程序中,前端通过RESTful API与后端通信,并且通常由单独部署的服务器提供服务。 像CAS这样的单点登录协议可以很好地支持前后端分离的应用程序。在这种情况下,前端负责在CAS服务器上进行身份验证并获得用户凭证。凭证随后使用AJAX请求发送到后端,以获得对受保护资源的访问权限。后端应用程序需要集成CAS客户端库,以验证请求的凭证并授予访问权限。 使用Spring BootCAS实现前后端分离的应用程序有许多优点。首先,这种方法可以提高应用程序的安全性。将身份验证逻辑集中在CAS服务器上可以确保凭证和用户的私人信息得到妥善保护。此外,前后端分离的应用程序通常更易于维护。由于前端和后端分开开发,因此可以使用不同的框架和语言,使得应用程序更具弹性和可扩展性。此外,在前后端分离的应用程序中,可以更轻松地实现可重复使用的代码,从而节省了时间和资源。 总之,Spring BootCAS是实现前后端分离的强大工具。这种方法可以提高应用程序的安全性、可维护性和可扩展性。如果您正在考虑构建一个前后端分离的应用程序,那么使用Spring BootCAS可能是一个很好的选择。 ### 回答3: SpringBoot是一个开箱即用的框架,可以帮开发者快速搭建Web应用程序。而CAS(Central Authentication Service)则是一个认证系统,用户只需要登录一次CAS系统,就可以在各个不同的应用中共享该登录状态。 在前后端分离的架构中,CAS认证系统可以用于对前后端的用户进行登录认证。在前端页面中,通过用户名和密码登录后,前端会调用CAS的API接口进行登录认证,验证通过后,返回一个Token给前端。前端页面可以使用该Token跨域向后端API发起请求,后端API会通过前端传递的Token进行认证,从而保证API的安全性和可访问性。 在使用SpringBoot构建后端API时,可以使用Spring Security框架集成CAS认证,根据前端传递的Token进行认证。这样,后端API不仅可以保证接口的安全性,还能够避免因为每个应用都需要独立实现登录认证功能而导致的代码冗余。 同时,前后端分离的架构也可以帮助开发者提高开发效率和应用可扩展性。因为前后端可以独立开发、独立部署,开发者可以专注于各自的领域,减少不必要的沟通和协调工作。同时,当应用负载增大时,可以通过扩展前端或后端服务器数量来解决瓶颈,而不用对整个应用进行重构。 总之,前后端分离的架构可以提高开发效率、增强应用可扩展性,而CAS认证系统可以帮助开发者实现安全认证,保障整个应用系统的安全性和可用性。平衡好前后端的工作,使用CAS实现单点登录认证,这对于构建大型Web应用程序非常有帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值