Spring Boot 快速接入 Cas 认证

cas-client-support-springboot 是 Cas 官方提供的客户端 SDK,可快速的集成 Spring Boot 实现统一身份认证平台的集成!

一、快速接入

1、引入依赖

在项目依赖管理中引入 cas-client-support-springboot 依赖 :

Maven 依赖
<!-- https://mvnrepository.com/artifact/org.jasig.cas.client/cas-client-support-springboot -->
<dependency>
    <groupId>org.jasig.cas.client</groupId>
    <artifactId>cas-client-support-springboot</artifactId>
    <version>3.6.4</version>
</dependency>
Gradle 依赖
// https://mvnrepository.com/artifact/org.jasig.cas.client/cas-client-support-springboot
implementation group: 'org.jasig.cas.client', name: 'cas-client-support-springboot', version: '3.6.4'
2、功能扩展
2.1、自定义 AntUrlPatternMatcherStrategy 使用基于Ant表达式的匹配策略
import org.jasig.cas.client.authentication.UrlPatternMatcherStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;

public class AntUrlPatternMatcherStrategy implements UrlPatternMatcherStrategy {

    /**
     * Any number of these characters are considered delimiters between multiple
     * context config paths in a single String value.
     */
    public static String CONFIG_LOCATION_DELIMITERS = ",; \t\n";
    private AntPathMatcher matcher = new AntPathMatcher();
    private String[] patterns;

    @Override
    public boolean matches(String url) {
        for (String pattern : patterns) {
            if (matcher.match(pattern, url)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void setPattern(String pattern) {
        this.patterns = StringUtils.tokenizeToStringArray(pattern, CONFIG_LOCATION_DELIMITERS);
    }

}
2.2、如果遇到SSL证书问题,需要需要信任请求域名,可自定义 HostnameVerifier(可选)
public class TrustAllHostnameVerifier implements HostnameVerifier {

    public final static TrustAllHostnameVerifier DEFAULT = new TrustAllHostnameVerifier();

    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}
2.3、自定义Cas客户端配置器,CustomCasClientConfigurer,解决配置项扩展问题
import org.jasig.cas.client.boot.configuration.CasClientConfigurer;
import org.jasig.cas.client.configuration.ConfigurationKeys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * cas-client-support-springboot 依赖提供了CAS客户端的自动配置,
 * 当自动配置不满足需要时,可通过实现{@link CasClientConfigurer}接口来重写需要自定义的逻辑
 */
@Component
public class CustomCasClientConfigurer implements CasClientConfigurer {

    /**
     * CAS-protected client Ignore Pattern Path, E.g: /api/*,/auth/*
     */
    @Value("${cas-ignore-pattern-path:}")
    private String ignorePatternPath;

    /**
     * 配置认证过滤器,添加忽略参数,使 /cas/logout 登出提示页免登录
     */
    @Override
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void configureAuthenticationFilter(final FilterRegistrationBean authenticationFilter) {
        Map initParameters = authenticationFilter.getInitParameters();
        initParameters.put(ConfigurationKeys.IGNORE_URL_PATTERN_TYPE.getName(), AntUrlPatternMatcherStrategy.class.getName());
        initParameters.put(ConfigurationKeys.IGNORE_PATTERN.getName(), ignorePatternPath);
    }

    @Override
    public void configureValidationFilter(FilterRegistrationBean validationFilter) {
        Map initParameters = validationFilter.getInitParameters();
        initParameters.put(ConfigurationKeys.HOSTNAME_VERIFIER.getName(), TrustAllHostnameVerifier.class.getName());
        initParameters.put(ConfigurationKeys.IGNORE_PATTERN.getName(), ignorePatternPath);
    }

}

二、Cas 对接示例(前后端不分离)

为完整是的演示Cas对接过程,这里会给出对接示例演示 Cas对接过程。

1、创建 CasAuthController 控制器,并编写 /cas/index/cas/login/cas/logout/cas/logoutPage 4个接口,用来调试 Cas 对接逻辑
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.jasig.cas.client.Protocol;
import org.jasig.cas.client.boot.configuration.CasClientConfigurationProperties;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

@Api(tags = "Cas 单点登录模拟")
@RestController
@RequestMapping("cas")
@Slf4j
public class CasAuthController {

    @Autowired
    private CasClientConfigurationProperties casProperties;

    /**
     * 首页,需要登录
     */
    @GetMapping("/index")
    @ResponseBody
    public Map<String, Object> index(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 1、从上下文中获取CAS认证信息
        HttpSession session = request.getSession(false);
        Assertion assertion = (Assertion) (session == null ? request
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
        // 2、如果没有认证信息,重定向到登录接口
        if (Objects.isNull(assertion)) {
            // 2.1、重定向到登录接口
            response.sendRedirect(casProperties.getClientHostUrl() + "/cas/login");
            return Collections.emptyMap();
        }
        // 3、如果有认证信息,获取用户信息
        Map<String, Object> userInfo = CasUtil.getUserAttributes();
        Map<String, Object> result =  new HashMap<>(3);
        result.put("code", 200);
        result.put("msg", MapUtils.getString(userInfo, "nickname", "xx") + ", 您已登录成功。");
        result.put("data", "<a href=\"" + casProperties.getClientHostUrl() + "/cas/logout\">退出登录</a>");
        return result;
    }

    /**
     * 登录接口(未登录时会被重定向到CAS Server进行认证)
     */
    @GetMapping("login")
    @ApiOperation("Cas 单点登录")
    public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 1、从上下文中获取CAS认证信息
        HttpSession session = request.getSession(false);
        Assertion assertion = (Assertion) (session == null ? request
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
        // 2、如果没有认证信息,重定向到CAS Server进行认证
        if (Objects.isNull(assertion)) {
            // 2.1、重定向到登录页面
            String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerLoginUrl(),
                    Protocol.CAS2.getServiceParameterName(),
                    casProperties.getClientHostUrl() + "/cas/login",
                    Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
            response.sendRedirect(casSingleLoginUrl);
            return;
        }
        // 3、重定向到主页
        final String urlToRedirectTo =  casProperties.getClientHostUrl() + "/cas/index";
        if (log.isDebugEnabled()) {
            log.debug("redirecting to \"" + urlToRedirectTo + "\"");
        }
        response.sendRedirect(urlToRedirectTo);
    }

    /**
     * 退出登录,跳转登出提示页
     */
    @GetMapping("/logout")
    @ApiOperation("cas 单点登出")
    public void logout(HttpServletRequest request, HttpServletResponse response, @RequestParam(required = false) String url) throws IOException {
        // 1、获取当前本地会话
        HttpSession session = request.getSession(false);
        if (Objects.nonNull(session)) {
            // 1.1、本地会话过期
            session.invalidate();
        }
        // 2、构造CAS Server登出URL,其中service参数为本地登出回调地址
        String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerUrlPrefix() + "/logout",
                Protocol.CAS2.getServiceParameterName(),
                casProperties.getClientHostUrl() + "/cas/logoutPage",
                Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
        // 3、重定向到CAS Server登出
        response.sendRedirect(casSingleLoginUrl);
    }

    /**
     * 登出提示页,免登录(生产中应该是项目的登录页面地址)
     */
    @GetMapping("/logoutPage")
    @ResponseBody
    public Map<String, Object> logoutPage(HttpServletResponse response) {
        // The URL to the CAS Server Single login.
        String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerLoginUrl(),
                Protocol.CAS2.getServiceParameterName(),
                casProperties.getClientHostUrl() + "/cas/login",
                Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
        // The URL to the Server Single logout.
        String casSingleLogoutUrl = CommonUtils.constructRedirectUrl(casProperties.getServerUrlPrefix() + "/logout",
                Protocol.CAS2.getServiceParameterName(),
                casSingleLoginUrl,
                Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));

        Map<String, Object> result =  new HashMap<>(3);
        result.put("code", 200);
        result.put("msg", "您已退出登录成功。");
        result.put("data", "<a href=\"" + casSingleLoginUrl + "\">去登录</a><br><br>"
                + "<a href=\"" + casSingleLogoutUrl + "\">全局退出登录</a>");
        return result;
    }

}
  • /cas/index:模拟主页,该接口模拟登录主页,返回登出地址
  • /cas/login:登录接口,常用于与Cas服务认证交互以及提供给应用中心配置功能入口(这里地址也可以是前端地址,那就需要前端实现单点出逻辑)
  • /cas/logout:本地登出接口,该接口销毁本地会话后,会重定向到登出模拟页面接口;如果业务需要直接全局登出,可以直接重定向Cas服务的登出地址
  • /cas/logoutPage:登出模拟页面,使用接口的方式模拟登录页面,接口返回登录地址全局登出地址
2、项目配置
Yaml 配置
############################################################################################
###Cas 认证(CasClientConfigurationProperties)配置:
############################################################################################
# CAS-protected client Ignore Pattern Path E.g. /api/cas/**. Optional.
cas-ignore-pattern-path: '**/cas/index,**/cas/logout,**/cas/logoutPage,**/swagger-ui/*,**/webjars/*,**/swagger-resources/*,**/v2/api-docs'
# Cas Configuration
cas:
  # CAS server URL E.g. https://example.com/cas or https://cas.example. Required.
  server-url-prefix: https://example.com
  # CAS server login URL E.g. https://example.com/cas/login or https://cas.example/login. Required.
  server-login-url: ${cas.server-url-prefix}/login
  # CAS-protected client application host URL E.g. https://myclient.example.com Required.
  client-host-url: https://myclient.example.com
  # Hostname Verifier to use for HTTPS connections.  Defaults to DefaultHostnameVerifier.
  #hostname-verifier: DefaultHostnameVerifier
  # SslConfig for HTTPS connections.
  #ssl-config-file: classpath:conf/cas/ssl.properties
  # ValidationType the CAS protocol validation type. Defaults to CAS3 if not explicitly set.
  validation-type: CAS3
  # Validation filter redirectAfterValidation. Defaults to true.
  redirect-after-validation: false
  # Whether to receive the single logout request from cas server.
  single-logout:
    enabled: true
Properties 配置
# CAS-protected client Ignore Pattern Path E.g. /api/cas/**. Optional.
cas-ignore-pattern-path=**/cas/index,**/cas/logout,**/cas/logoutPage,**/swagger-ui/*,**/webjars/*,**/swagger-resources/*,**/v2/api-docs
# Cas Configuration
cas.server-url-prefix=https://example.com
cas.server-login-url=${cas.server-url-prefix}/login
cas.client-host-url=https://myclient.example.com
cas.validation-type=CAS3
cas.single-logout.enabled=true
cas.redirect-after-validation=false

这里特别注意 redirect-after-validation 需要设置为 false, 否则会造成第4步中的示例代码,在登录之后无法正常登录,总会被重定向到 service 参数所指向的地址。

3、启动对象添加 @EnableCasClient 注解,访问调试接口验证对接情况

应用入口代码:

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

@SpringBootApplication
@EnableCasClient
public class CasAutoConfigApp {

    public static void main(String[] args) {
        SpringApplication.run(CasAutoConfigApp.class, args);
    }

}

EnableCasClient 注解类引入了配置类 CasClientConfiguration, 配置类中做了以下几件事:

  • casAuthenticationFilter() 创建了 认证过滤器
  • casValidationFilter() 创建了 验证票据过滤器
  • casHttpServletRequestWrapperFilter() 创建了请求对象的包装类
  • casAssertionThreadLocalFilter() 创建了将 Assertion 放到 ThreadLocal 的过滤器,对于获取不到HttpRequest 请求对象的情况这很有用
  • casSingleSignOutFilter() 创建了单点登出的过滤器
  • casSingleSignOutListener() 创建单点登出的Listener,用于监听登出事件,清理内存中单点登录会话缓存
  • SpringSecurityAssertionAutoConfiguration 兼容Spring Security的配置类
4、验证Cas认证接入情况

认证验证流程:

浏览器浏览器业务系统/平台业务系统/平台统一身份认证平台统一身份认证平台Step 1、访问 /cas/index “模拟主页”1.1、接口不被拦截,登录信息不存在,则重定向到本地登录接口 /cas/login1.2、/cas/login 检查登录状态,构建重定向地址1.3、浏览器重定向:http://cas_server_url/login?service=http%3A%2F%2Fclient_host_url%3A8081%2Fcas%2FloginStep 2、在统一身份认证平台完成登录验证2.1、账号密码登录或其他方式登录,登录成功,重定向 `/cas/login` 接口Step 3、“业务系统/平台”内部对接逻辑处理3.1、获取认证信息失败:跳转到 “统一身份认证平台” 的登录页面3.2、获取认证信息成功:关联本地用户、生成Token、写Cookie信息3.3、重定向到 /cas/index “模拟主页”Step 4、“模拟主页”,验证 “退出登录”4.1、在 “模拟主页” 访问 “退出登录” 地址:http://client_host_url/cas/logout4.2、销毁本地会话,重定向到 /cas/logoutPage “登出模拟页”Step 5、“登出模拟页”,验证 “全局登出”5.1、在“登出模拟页” 访问 “全局登出地址”,会重新跳转到 “统一身份认证平台” 的登录页面5.2、再次输入账号密码或使用其他方式登录登录成功,重定向回三方系统 `/cas/login` 接口5.3、已经关联本地用户,重定向到 /cas/index “模拟主页”Step 6、“业务系统/平台” 本地账号、用户信息建立关联,统一身份认证接入完成!
4.1、首先访问 /cas/index , 比如:http://192.168.0.20:8081/cas/index
  • /cas/index 接口不被拦截,登录信息不存在,则重定向到本地登录接口 /cas/login
  • /cas/login 接口是被拦截的,因当前未进行Cas登录,访问接口会进入 DefaultAuthenticationRedirectStrategyredirect 方法,被重定向到认证中心登录界面
public final class DefaultAuthenticationRedirectStrategy implements AuthenticationRedirectStrategy {

    @Override
    public void redirect(final HttpServletRequest request, final HttpServletResponse response,
                         final String potentialRedirectUrl) throws IOException {
        response.sendRedirect(potentialRedirectUrl);
    }
}
4.2、在统一身份认证平台完成登录验证
  • 账号密码登录或其他方式登录,登录成功,重定向 /cas/login 接口
  • /cas/login 接口获取认证信息失败:跳转到 “统一身份认证平台” 的登录页面;获取认证信息成功:关联本地用户、生成Token、写 Cookie 信息
  • 完成本地用户关联后,重定向 /cas/index “模拟主页”
4.3、“模拟主页”,返回如下内容:
{
  "code": 200,
  "msg": "您已登录成功。",
  "data": "<a href=\"http://192.168.0.20:8081/cas/logout\">退出登录</a>"
}

内容中包含了登录成功提示登出地址,访问 登出地址即可实现本地登出。

  • 在 “模拟主页” 访问 “退出登录” 地址:http://client_host_url/cas/logout
  • “业务系统/平台”会销毁本地会话,重定向到 /cas/logoutPage “登出模拟页”
4.4、“登出模拟页”,返回如下内容:
{
  "code": 200,
  "msg": "您已退出登录成功。",
  "data": "<a href=\"http://192.168.3.27:31495/login?service=http%3A%2F%2F192.168.0.20%3A8081%2Fcas%2Flogin\">去登录</a><br><br><a href=\"http://192.168.3.27:31495/logout?service=http%3A%2F%2F192.168.3.27%3A31495%2Flogin%3Fservice%3Dhttp%253A%252F%252F192.168.0.20%253A8081%252Fcas%252Flogin\">全局退出登录</a>"
}

内容中包含了登出成功提示登录地址全局退出登录地址

  • 在“登出模拟页” 访问 “全局登出地址”,会重新跳转到 “统一身份认证平台” 的登录页面
  • 再次输入账号密码或使用其他方式登录登录成功,重定向回三方系统 /cas/login 接口
  • 已经关联本地用户,重定向到 /cas/index “模拟主页”

三、Cas 对接示例(前后端分离)

在前后端分离的项目里面,需要对上面已经完成的对接逻辑进行一些调整。cas/indexcas/logoutPage 替换为前端地址,并去除模拟方法。

1、自定义重定向策略 CustomAuthRedirectStrategy ,解决前后端分离项目,Ajax 请求无法处理重定向问题

前后端分离项目,Cas的重定向无法跳转到 cas登录网址去,需要前端进行跳转,所以要重写AuthenticationRedirectStrategy,直接给前端返回401错误码.

CustomAuthRedirectStrategy.java

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import lombok.extern.slf4j.Slf4j;
import org.jasig.cas.client.Protocol;
import org.jasig.cas.client.authentication.AuthenticationRedirectStrategy;
import org.jasig.cas.client.boot.configuration.CasClientConfigurationProperties;
import org.jasig.cas.client.util.CommonUtils;
import org.springframework.http.HttpStatus;

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

/**
 * 自定义跳转策略
 */
@Slf4j
public class CustomAuthRedirectStrategy implements AuthenticationRedirectStrategy {

    public static final String DATE_LONGFORMAT = "yyyy-MM-dd HH:mm:ss";
    private ObjectMapper objectMapper = JsonMapper.builder()
            // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
            //.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL)
            .defaultDateFormat(new SimpleDateFormat(DATE_LONGFORMAT))
            // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
            .visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
            .serializationInclusion(JsonInclude.Include.NON_NULL).build();

    @Override
    public void redirect(HttpServletRequest request, HttpServletResponse response, String urlToRedirectTo) throws IOException {

        CasClientConfigurationProperties casProperties = SpringBeanUtils.getApplicationContext().getBean(CasClientConfigurationProperties.class);

        String origin = request.getHeader("Origin");
        // 简单请求跨域,如果是跨域请求在响应头里面添加对应的Origin
        if (StringUtils.hasText(origin)) {
            response.addHeader("Access-Control-Allow-Origin", origin);
        } else {
            response.addHeader("Access-Control-Allow-Origin", "*");
        }
        // 非简单请求跨域
        response.addHeader("Access-Control-Allow-Headers", "content-type");
        // 允许跨域请求的方法
        response.addHeader("Access-Control-Allow-Methods", "*");
        // 携带cookie的跨域
        response.addHeader("Access-Control-Allow-Credentials", "true");


        if(WebUtils.isAjaxRequest(request)){

            /**
             * The URL to the CAS Server Single login.
             */
            String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerLoginUrl(),
                    Protocol.CAS2.getServiceParameterName(),
                    casProperties.getClientHostUrl() + "/cas/login",
                    Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
            /**
             * The URL to the CAS Server Single logout.
             */
            String casSingleLogoutUrl = CommonUtils.constructRedirectUrl(casProperties.getServerUrlPrefix() + "/logout",
                    Protocol.CAS2.getServiceParameterName(),
                    casSingleLoginUrl,
                    Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
            if (log.isDebugEnabled()) {
                log.debug("casSingleLoginUrl to \"" + casSingleLoginUrl + "\"");
                log.debug("casSingleLogoutUrl to \"" + casSingleLogoutUrl + "\"");
            }

            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setHeader("content-type", "application/json;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");

            Map<String, Object> result = new HashMap<>();
            result.put("code", HttpStatus.UNAUTHORIZED.value());
            result.put("msg", "未授权,请先登录。");
            Map<String, Object> data = new HashMap<>();
            data.put("loginUrl", casSingleLoginUrl);
            data.put("logoutUrl", casSingleLogoutUrl);
            result.put("data", data);
            objectMapper.writeValue(response.getOutputStream(), result);

        } else {
            response.sendRedirect(urlToRedirectTo);
        }

    }
}
2、修改 CustomCasClientConfigurer,添加自定义重定向策略配置
import org.jasig.cas.client.boot.configuration.CasClientConfigurer;
import org.jasig.cas.client.configuration.ConfigurationKeys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * cas-client-support-springboot 依赖提供了CAS客户端的自动配置,
 * 当自动配置不满足需要时,可通过实现{@link CasClientConfigurer}接口来重写需要自定义的逻辑
 */
@Component
public class CustomCasClientConfigurer implements CasClientConfigurer {

    /**
     * CAS-protected client Ignore Pattern Path, E.g: /api/*,/auth/*
     */
    @Value("${cas-ignore-pattern-path:}")
    private String ignorePatternPath;

    /**
     * 配置认证过滤器,添加忽略参数,使 /cas/logout 登出提示页免登录
     */
    @Override
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void configureAuthenticationFilter(final FilterRegistrationBean authenticationFilter) {
        Map initParameters = authenticationFilter.getInitParameters();
        initParameters.put(ConfigurationKeys.IGNORE_URL_PATTERN_TYPE.getName(), AntUrlPatternMatcherStrategy.class.getName());
        initParameters.put(ConfigurationKeys.AUTHENTICATION_REDIRECT_STRATEGY_CLASS.getName(), CustomAuthRedirectStrategy.class.getName());
        initParameters.put(ConfigurationKeys.IGNORE_PATTERN.getName(), ignorePatternPath);
    }

    @Override
    public void configureValidationFilter(FilterRegistrationBean validationFilter) {
        Map initParameters = validationFilter.getInitParameters();
        initParameters.put(ConfigurationKeys.HOSTNAME_VERIFIER.getName(), TrustAllHostnameVerifier.class.getName());
        initParameters.put(ConfigurationKeys.IGNORE_PATTERN.getName(), ignorePatternPath);
        initParameters.put(ConfigurationKeys.EXCEPTION_ON_VALIDATION_FAILURE.getName(), Boolean.FALSE.toString());
    }

}
3、在 CasAuthController 控制器,并添加 /cas/address 接口,用来前端获取跳转地址
/**
 * 登录、注销地址
 */
@GetMapping("address")
@ApiOperation("获取登录配置")
public Map<String, Object> getCasAddress() {
    Map<String, Object> result = new HashMap<>(2);
    // The URL to the CAS Server Single login.
    String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerLoginUrl(),
            Protocol.CAS2.getServiceParameterName(),
            casProperties.getClientHostUrl() + "/cas/login",
            Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
    result.put("loginUrl", casSingleLoginUrl);
    result.put("logoutUrl", casProperties.getClientHostUrl() + "/cas/logout");
    return result;
}
4、修改 cas/login 认证成功后的重定向地址 :

添加 cas-client-front-url 配置参数:

Yaml 配置
# CAS-protected client front url E.g. http://localhost:8080. Required.
cas-client-front-url: http://192.168.0.20:8080
Properties 配置
# CAS-protected client front url E.g. http://localhost:8080. Required.
cas-client-front-url=http://192.168.0.20:8080

这里特别注意 cas-client-front-url 值前后端分离项目中的前端访问地址, 不要使用 cas.client-front-url, 否则会导致错误。

修改 CasAuthController 控制器代码, 添加 casClientFrontUrl 属性,修改 login 重定向逻辑

/**
 * CAS-protected client front url E.g. http://localhost:8080. Required.
 */
@Value("${cas-client-front-url:}")
private String casClientFrontUrl;

@GetMapping("login")
@ApiOperation("Cas 单点登录")
public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {

    String origin = request.getHeader("Origin");
    // 简单请求跨域,如果是跨域请求在响应头里面添加对应的Origin
    if (StringUtils.hasText(origin)) {
        response.addHeader("Access-Control-Allow-Origin", origin);
    } else {
        response.addHeader("Access-Control-Allow-Origin", "*");
    }
    // 非简单请求跨域
    response.addHeader("Access-Control-Allow-Headers", "content-type");
    // 允许跨域请求的方法
    response.addHeader("Access-Control-Allow-Methods", "*");
    // 携带cookie的跨域
    response.addHeader("Access-Control-Allow-Credentials", "true");

    // 1、从上下文中获取CAS认证信息
    HttpSession session = request.getSession(false);
    Assertion assertion = (Assertion) (session == null ? request
            .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session
            .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
    // 2、如果没有认证信息,重定向到CAS Server进行认证
    if (Objects.isNull(assertion)) {
        // 2.1、重定向到登录页面
        String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerLoginUrl(),
                Protocol.CAS2.getServiceParameterName(),
                casProperties.getClientHostUrl() + "/cas/login",
                Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
        response.sendRedirect(casSingleLoginUrl);
        return;
    }

    // 3、如果有认证信息,获取用户信息
    Long userId = CasUtil.getUserAccountId();
    Map<String, Object> userInfo = CasUtil.getUserAttributes();
    // 4、手动设置Cookie(前端地址与后端接口在一个域名下有效)
    if (Objects.nonNull(session)) {
        // 5、重定向到主页
        final String urlToRedirectTo =  casClientFrontUrl + "/home/index?jsessionid=" + session.getId();
        if (log.isDebugEnabled()) {
            log.debug("redirecting to \"" + urlToRedirectTo + "\"");
        }
        response.sendRedirect(urlToRedirectTo);
    }

}

前端需要新增全局拦截器,未登录状态下一律拦截到 cas/login 接口,登录成功后会将会重定向至配置的前端页面,并追加面jsessionid参数,需要将jsessionid写入cookie,后续所有请求保持和后端jsessionid一致(注意跨域)

认证中心登录成功,重定向到前端项目指定地址,去除 cas/index 模拟方法。

5、修改 cas/logout 认证登出逻辑,重定向到认证中心登录界面 :
    @GetMapping("/logout")
    @ApiOperation("cas 单点登出")
    public void logout(HttpServletRequest request, HttpServletResponse response, @RequestParam(required = false) String url) throws IOException {
        // 1、获取当前本地会话
        HttpSession session = request.getSession(false);
        if (Objects.nonNull(session)) {
            // 1.1、本地会话过期
            session.invalidate();
        }
        // 2、构造CAS Server登出URL,其中service参数为本地登出回调地址
        // The URL to the CAS Server Single login.
        String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerLoginUrl(),
                Protocol.CAS2.getServiceParameterName(),
                casProperties.getClientHostUrl() + "/cas/login",
                Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
        // The URL to the Server Single logout.
        String casSingleLogoutUrl = CommonUtils.constructRedirectUrl(casProperties.getServerUrlPrefix() + "/logout",
                Protocol.CAS2.getServiceParameterName(),
                casSingleLoginUrl,
                Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
        // 3、重定向到CAS Server登出
        response.sendRedirect(casSingleLogoutUrl);
    }

本地登出成功,重定向到认证中心的登录界面,去除 cas/logoutPage 模拟方法。

6、项目配置
Yaml 配置
############################################################################################
###Cas 认证(CasClientConfigurationProperties)配置:
############################################################################################
# CAS-protected client Ignore Pattern Path E.g. /api/cas/**. Optional.
cas-ignore-pattern-path: '**/cas/address,**/cas/logout,**/swagger-ui/*,**/webjars/*,**/swagger-resources/*,**/v2/api-docs'
# Cas Configuration
cas:
  # CAS server URL E.g. https://example.com/cas or https://cas.example. Required.
  server-url-prefix: https://example.com
  # CAS server login URL E.g. https://example.com/cas/login or https://cas.example/login. Required.
  server-login-url: ${cas.server-url-prefix}/login
  # CAS-protected client application host URL E.g. https://myclient.example.com Required.
  client-host-url: https://myclient.example.com
  # Hostname Verifier to use for HTTPS connections.  Defaults to DefaultHostnameVerifier.
  #hostname-verifier: DefaultHostnameVerifier
  # SslConfig for HTTPS connections.
  #ssl-config-file: classpath:conf/cas/ssl.properties
  # ValidationType the CAS protocol validation type. Defaults to CAS3 if not explicitly set.
  validation-type: CAS3
  # Validation filter redirectAfterValidation. Defaults to true.
  redirect-after-validation: false
  # Whether to receive the single logout request from cas server.
  single-logout:
    enabled: true
Properties 配置
# CAS-protected client Ignore Pattern Path E.g. /api/cas/**. Optional.
cas-ignore-pattern-path=**/cas/address,**/cas/logout,**/swagger-ui/*,**/webjars/*,**/swagger-resources/*,**/v2/api-docs
# Cas Configuration
cas.server-url-prefix=https://example.com
cas.server-login-url=${cas.server-url-prefix}/login
cas.client-host-url=https://myclient.example.com
cas.validation-type=CAS3
cas.single-logout.enabled=true
cas.redirect-after-validation=false
7、前端Vue项目调整
7.1、VueRouter 添加 路由拦截逻辑,处理重定向携带 JSESSIONID:
import Router from 'vue-router';
Vue.use(Router);
const vueRouter = new Router({
  mode: 'history',
  base: process.env.VUE_APP_PATH,
  routes: []
});
const beforeEachRoute = async (to, _, next) => {
  const { path, name, query } = to;
  // 1、对url带jsessionid路由的处理
  if (query.jsessionid) {
    // 清除cookie
    const cookiesList = cookies.keys();
    cookiesList.forEach((item) => {
      cookies.remove(item);
    });
    cookies.set('JSESSIONID', query.jsessionid);
    let _query = { ...query };
    delete _query.jsessionid;
    next({
      path,
      query: _query
    })
    return;
  } else {
    // 2、url不带jsessionid,说明是其他路由页面访问,需要判断登录状态
    const oldJessionid = cookies.get('JSESSIONID');
    console.log('oldJessionid', oldJessionid);
    // 2.1、如果cookie里有JSESSIONID,说明已经登录,直接跳转
    if (oldJessionid) {
      next();
      return;
    }
    // 2.2、如果cookie里没有JSESSIONID,说明没有登录,跳转到登录页面
    getLoginAddress().then((res) => {
      // 清除cookie和localStorage
      const cookiesList = cookies.keys();
      cookiesList.forEach((item) => {
        cookies.remove(item);
      });
      localStorage.removeItem("JSESSIONID");
      // 浏览器在当前窗口,跳转到认证中心登录页面
      console.log('res', res);
      if (res && res.data && res.data.loginUrl) {
        window.open(res.data.loginUrl, '_self');
      }
    })
    .catch(() => {
      location.reload();
    }).finally(() => {

    });
    }
};
vueRouter.beforeEach(beforeEachRoute);
7.2、Axios 添加 HTTP 请求参数
axios.defaults.timeout = 30000;
// 返回其他状态吗
axios.defaults.validateStatus = function (status) {
  return status >= 200 && status <= 500; // 默认的
};
// 设置X-Requested-With请求头默认值
axios.defaults.headers["X-Requested-With"] = 'XMLHttpRequest';
// 跨域请求,允许保存cookie
axios.defaults.withCredentials = true;
7.3、Axios 添加 HTTP 响应拦截逻辑,处理 401 异常
import cookies from 'vue-cookies';
import axios from 'axios';

// HTTP 响应拦截
axios.interceptors.response.use((config) => {
    const res = config.data;
    // 授权失败
    if (config.status === 401 || res.code === 401) {
      // 清理 Cookie:该短代码很重要,防止跳转认证平台后,再次跳转到本系统时,Cookie 仍然存在,存在多个JSESSIONID 导致Session无法保持
      const cookiesList = cookies.keys();
      cookiesList.forEach((item) => {
        cookies.remove(item);
      });
      // 重定向到登录页面
      if(res.data && res.data.loginUrl){
        window.open(res.data.loginUrl, '_self');
        return;
      }
    }
    // 其他逻辑
}
8、验证Cas认证接入情况

认证验证流程:

浏览器浏览器业务系统/平台业务系统/平台统一身份认证平台统一身份认证平台Step 1、访问前端主页1.1、路由守卫 vueRouter.beforeEach() ,检查登录状态1.2、已登录(JSESSIONID 有效),则进入主页1.3、未登录(JSESSIONID 失效),调用 cas/address 接口获取 loginUrl 地址Step 2、在统一身份认证平台完成登录验证2.1、账号密码登录或其他方式登录,登录成功,重定向 `/cas/login` 接口Step 3、“业务系统/平台”内部对接逻辑处理3.1、获取认证信息失败:跳转到 “统一身份认证平台” 的登录页面3.2、获取认证信息成功:关联本地用户、生成Token、写Cookie信息3.3、重定向到前端主页Step 4、路由鉴权处理4.1、路由守卫 vueRouter.beforeEach() ,jsessionid 参数处理,保存 Cookie4.2、路由重定向到主页Step 5、接口鉴权处理5.1、接口后端拦截,已登录(JSESSIONID 有效),执行接口逻辑5.2、未登录(JSESSIONID 失效),返回 401 状态码 和 登录地址5.3、接口响应拦截器,拦截401,重新跳转到 “统一身份认证平台” 的登录页面5.2、再次输入账号密码或使用其他方式登录登录成功,重定向回三方系统 `/cas/login` 接口5.3、已经关联本地用户,重定向到前端主页Step 6、“业务系统/平台” 本地账号、用户信息建立关联,统一身份认证接入完成!
8.1、首先访问 前端主页 , 比如:http://192.168.0.20:8080

前端Vue路由守卫,执行登录状态检查逻辑

  • 路由守卫 vueRouter.beforeEach() ,检查登录状态
  • 已登录(JSESSIONID 有效),则进入主页
  • 未登录(JSESSIONID 失效),调用 cas/address 接口获取 loginUrl 地址
8.2、在统一身份认证平台完成登录验证
  • 账号密码登录或其他方式登录,登录成功,重定向 /cas/login 接口
  • /cas/login 接口获取认证信息失败:跳转到 “统一身份认证平台” 的登录页面;获取认证信息成功:关联本地用户、生成Token、写 Cookie 信息
  • 完成本地用户关联后,携带 jsessionid 参数,重定向到前端主页
8.3、前端主页,执行路由鉴权逻辑
  • 路由守卫 vueRouter.beforeEach() ,jsessionid 参数处理,保存 Cookie
  • 路由重定向到主页
  • 主页正常调用后端接口
8.4、后端服务,执行接口鉴权拦截
  • 接口后端拦截,已登录(JSESSIONID 有效),执行接口逻辑
  • 未登录(JSESSIONID 失效),返回 401 状态码 和 登录地址
  • 接口响应拦截器,拦截401,重新跳转到 “统一身份认证平台” 的登录页面
  • 再次输入账号密码或使用其他方式登录登录成功,重定向回三方系统 /cas/login 接口
  • 已经关联本地用户,重定向到前端主页
9、本地账号关联

假设有用户表

CREATE TABLE `ag_user` (
  `id` bigint(19) NOT NULL COMMENT '主键',
  `account` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '账户名称',
  `is_admin` tinyint(4) DEFAULT NULL COMMENT '是否是超管',
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `cas_user_id` bigint(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户';

定义 Service 接口

import com.baomidou.mybatisplus.extension.service.IService;
import org.jasig.cas.client.validation.Assertion;

public interface IUserService extends IService<User> {

    /**
     * 关联Cas用户
     * @param assertion
     * @return
     */
    boolean saveOrUpdate(Assertion assertion);

}

实现 Service 接口

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.collections4.MapUtils;
import org.jasig.cas.client.validation.Assertion;

import java.util.Map;
import java.util.Objects;

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public boolean saveOrUpdate(Assertion assertion) {
        // 1、如果有认证信息,获取用户信息
        Long casUserId = CasUtil.getUserAccountId(assertion);
        Map<String, Object> userInfo = CasUtil.getUserAttributes(assertion);
        // 2、根据casUserId查询用户信息
        User entity = getBaseMapper().selectOne(new LambdaQueryWrapper<User>().eq(User::getCasUserId, casUserId));
        if(Objects.nonNull(entity)){
            entity = new User()
                    .setId(IdWorker.getId())
                    .setName(MapUtils.getString(userInfo, "nickname"))
                    .setAccount(CasUtil.getUserAccount())
                    .setCasUserId(casUserId);
            return this.update(entity, new LambdaUpdateWrapper<User>().eq(User::getCasUserId, entity.getCasUserId()));
        } else {
            entity = new User()
                    .setId(IdWorker.getId())
                    .setName(MapUtils.getString(userInfo, "nickname"))
                    .setAccount(CasUtil.getUserAccount())
                    .setCasUserId(casUserId)
                    .setIsAdmin(Boolean.FALSE);
            return this.save(entity);
        }
    }
}

定义 Mapper 接口

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

定义 User 实体

@Data
@Accessors(chain = true)
@FieldNameConstants
@TableName("ag_user")
public class User {
    @TableId
    private Long id;
    private String account;
    private String name;
    private Boolean isAdmin;
    private Long casUserId;
}

在 /cas/login 接口添加关联用户逻辑

    @GetMapping("login")
    @ApiOperation("Cas 单点登录")
    public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {

        String origin = request.getHeader("Origin");
        // 简单请求跨域,如果是跨域请求在响应头里面添加对应的Origin
        if (StringUtils.hasText(origin)) {
            response.addHeader("Access-Control-Allow-Origin", origin);
        } else {
            response.addHeader("Access-Control-Allow-Origin", "*");
        }
        // 非简单请求跨域
        response.addHeader("Access-Control-Allow-Headers", "*");
        // 允许跨域请求的方法
        response.addHeader("Access-Control-Allow-Methods", "*");
        // 携带cookie的跨域
        response.addHeader("Access-Control-Allow-Credentials", "true");

        // 1、从上下文中获取CAS认证信息
        HttpSession session = request.getSession(false);
        Assertion assertion = (Assertion) (session == null ? request
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
        // 2、如果没有认证信息,重定向到CAS Server进行认证
        if (Objects.isNull(assertion)) {
            // 2.1、重定向到登录页面
            String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerLoginUrl(),
                    Protocol.CAS2.getServiceParameterName(),
                    casProperties.getClientHostUrl() + "/cas/login",
                    Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
            response.sendRedirect(casSingleLoginUrl);
            return;
        }

        // 3、关联Cas用户
        userService.saveOrUpdate(assertion);

        // 4、手动设置Cookie(前端地址与后端接口在一个域名下有效)
        if (Objects.nonNull(session)) {
            // 5、重定向到主页
            final String urlToRedirectTo =  casClientFrontUrl + "/home/index?jsessionid=" + session.getId();
            if (log.isDebugEnabled()) {
                log.debug("redirecting to \"" + urlToRedirectTo + "\"");
            }
            response.sendRedirect(urlToRedirectTo);
        }

    }

四、涉及的工具

WebUtils.java

@Slf4j
public class WebUtils extends org.springframework.web.util.WebUtils {

    private static final String XML_HTTP_REQUEST = "XMLHttpRequest";
    private static final String X_REQUESTED_WITH = "X-Requested-With";
    private static final String CONTENT_TYPE_JSON = "application/json";

    public static boolean isAjaxResponse(HttpServletRequest request ) {
        return isAjaxRequest(request) || isContentTypeJson(request) || isPostRequest(request);
    }

    public static boolean isObjectRequest(HttpServletRequest request ) {
        return isPostRequest(request) && isContentTypeJson(request);
    }

    public static boolean isAjaxRequest(HttpServletRequest request ) {
        return XML_HTTP_REQUEST.equals(request.getHeader(X_REQUESTED_WITH));
    }


    public static boolean isContentTypeJson(HttpServletRequest request ) {
        return request.getHeader(HttpHeaders.CONTENT_TYPE).contains(CONTENT_TYPE_JSON);
    }

    public static boolean isPostRequest(HttpServletRequest request ) {
        return HttpMethod.POST.compareTo(HttpMethod.resolve(request.getMethod()) ) == 0;
    }

    public static boolean isPostRequest(HttpRequest request ) {
        return HttpMethod.POST.compareTo(request.getMethod()) == 0;
    }

}

CasUtil.java

import lombok.extern.slf4j.Slf4j;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.util.AssertionHolder;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

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

/**
 * Cas 工具类
 */
@Slf4j
public class CasUtil {

    /**
     * 获取 Assertion
     *
     * @param request request
     * @return Assertion
     */
    public static Assertion getAssertion(HttpServletRequest request) {
        HttpSession session = request.getSession();
        return (Assertion) session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
    }

    /**
     * 获取 HttpServletRequest
     *
     * @return HttpServletRequest
     */
    public static HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert attributes != null;
        return attributes.getRequest();
    }

    /**
     * 获取 account
     *
     * @return account
     */
    public static String getUserAccount() {
        log.info("当前用户:{}", getRequest().getRemoteUser());
        return getRequest().getRemoteUser();
    }

    /**
     * 判断当前用户是否是超级管理员
     *
     * @return 是或不是
     */
    public static Boolean isAdmin() {
        // 获取配置文件中的超级管理员集合
        List<String> adminAccounts = getAdminAccountListDateBase();
        // 若是超级管理员
        return adminAccounts.contains(getUserAccount());
    }

    public static List<String> getAdminAccountListDateBase() {
        return CasUserUtil.getAllAdminAccount();
    }

    /**
     * 获取 accountId
     *
     * @return accountId
     */
    public static Long getUserAccountId() {
        return Long.valueOf(String.valueOf(Optional.ofNullable(getUserAttributes().get("accountId")).orElse("0")));
    }

    /**
     * 获取 accountId
     *
     * @return accountId
     */
    public static Long getUserAccountId(Assertion assertion) {
        return Long.valueOf(String.valueOf(Optional.ofNullable(getUserAttributes(assertion).get("accountId")).orElse("0")));
    }

    /**
     * 获取用户属性
     *
     * @return 用户属性
     */
    public static Map<String, Object> getUserAttributes() {
        Assertion assertion = AssertionHolder.getAssertion();
        return Objects.isNull(assertion) ? Collections.emptyMap() : getUserAttributes(assertion);
    }

    /**
     * 获取用户属性
     *
     * @return 用户属性
     */
    public static Map<String, Object> getUserAttributes(Assertion assertion) {
        AttributePrincipal principal = assertion.getPrincipal();
        return Objects.isNull(principal) ? new HashMap<>() : principal.getAttributes();
    }

    /**
     * 获取 accountId
     *
     * @return accountId
     */
    public static Long getUserAccountIdIgnoreEx() {
        return Long.valueOf(String.valueOf(Optional.ofNullable(getUserAttributesIgnoreEx().get("accountId")).orElse("0")));
    }

    /**
     * 获取用户属性
     *
     * @return 用户属性
     */
    public static Map<String, Object> getUserAttributesIgnoreEx() {
        try {
            Assertion assertion = AssertionHolder.getAssertion();
            AttributePrincipal principal = assertion.getPrincipal();
            log.info("Principal Name : {} , Attributes: {}", principal.getName(), principal.getAttributes());
            return Objects.isNull(principal) ? Collections.EMPTY_MAP : principal.getAttributes();
        } catch (Exception e){
            log.error(e.getMessage());
            return Collections.EMPTY_MAP;
        }
    }
}

SpringBeanUtils.java

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringBeanUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext){
        SpringBeanUtils.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

}

五、常见问题

1、Cas登录成功后重定向 /cas/login ,登录状态失效

原因:

CustomAuthRedirectStrategyredirect 方法中,我们通过下面的代码生成了单点登录、单点登出地址

/**
* The URL to the CAS Server Single login.
*/
String casSingleLoginUrl = CommonUtils.constructRedirectUrl(casProperties.getServerLoginUrl(),
Protocol.CAS2.getServiceParameterName(),
casProperties.getClientHostUrl() + "/cas/login",
Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));
/**
* The URL to the CAS Server Single logout.
*/
String casSingleLogoutUrl = CommonUtils.constructRedirectUrl(casProperties.getServerUrlPrefix() + "/logout",
Protocol.CAS2.getServiceParameterName(),
casProperties.getClientHostUrl() + "/cas/logoutPage",
Boolean.FALSE, Optional.ofNullable(casProperties.getGateway()).orElse(Boolean.FALSE));

其中,单点登出回调地址 /cas/logoutPage 是不被拦截的,这是一个模拟单点登出页面的接口,接口返回内容包含了一个全局登出地址,用于演示全局登出逻辑

AbstractTicketValidationFilterdoFilter 方法中,在完成ticket票据校验逻辑后,将验证结果放入 RquestSession 后,会重定向到 constructServiceUrl 方法构造的地址,解决这个问题的方法有2个,

http://192.168.3.27:31495/login?service=http%3A%2F%2F192.168.0.20%3A8081%2Fcas%2Flogin

doFilter 代码片段:

final Assertion assertion = this.ticketValidator.validate(ticket,
                        constructServiceUrl(request, response));

logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());

request.setAttribute(CONST_CAS_ASSERTION, assertion);

if (this.useSession) {
    request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
}
onSuccessfulValidation(request, response, assertion);

if (this.redirectAfterValidation) {
    logger.debug("Redirecting after successful ticket validation.");
    response.sendRedirect(constructServiceUrl(request, response));
    return;
}

constructServiceUrl 代码片段:

protected final String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
    return CommonUtils.constructServiceUrl(request, response, this.service, this.serverName,
    this.protocol.getServiceParameterName(),
    this.protocol.getArtifactParameterName(), this.encodeServiceUrl);
}
2、登录状态下获取用户信息

直接获取登录用户名

request.getRemoteUser();

获取详细用户信息

AttributePrincipal principal = (AttributePrincipal)request.getUserPrincipal();
Map attributes = principal.getAttributes();
Object moblie=attributes .get("moblie");

参考资料:
https://blog.csdn.net/qq2456939181/article/details/127935967

作者:Jeebiz  创建时间:2023-09-26 13:06
最后编辑:Jeebiz  更新时间:2024-05-07 20:29