一、cas server
搭建 cas server
1.拉取镜像
docker pull apereo/cas:6.6.13
2.配置文件
在宿主机,创建文件 /data/etc/cas/config/cas.properties
cas.server.name=https://172.17.0.2:8443
cas.server.prefix=${cas.server.name}/cas
logging.config=file:/etc/cas/config/log4j2.xml
cas.service-registry.core.init-from-json=true
cas.service-registry.json.location=file:/etc/cas/services
# 配置允许登出后跳转到指定页面
cas.logout.follow-service-redirects=true
3.配置客户端支持 http
在宿主机,创建文件 /data/etc/cas/services/web-10000001.json
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|imaps|http)://.*",
"name" : "web",
"id" : 10000001,
"evaluationOrder" : 10
}
4.制作 ssl 证书
1) 使用 keytool 创建本地密钥库
keytool -genkey -alias cas.server.com -keyalg RSA -storepass changeit -keystore cas.keystore -dname "CN=cas.server.com, OU=cas, O=cas, L=hangzhou, ST=zhejiang, C=zh"
2) 把密钥库导出成证书文件
keytool -export -alias cas.server.com -keystore cas.keystore -file cas.crt -storepass changeit
查看证书
keytool -printcert -file cas.crt
3) 将创建的证书导入到客户端 java 证书库
keytool -import -keystore "/opt/jdk1.8.0_341/jre/lib/security/cacerts" -file "cas.crt" -alias cas.server.com -storepass changeit
查看 jdk 证书内容
keytool -list -v -keystore /opt/jdk1.8.0_341/jre/lib/security/cacerts -alias cas.server.com -storepass changeit
删除客户端 java 证书
keytool -delete -keystore /opt/jdk1.8.0_341/jre/lib/security/cacerts -alias cas.server.com -storepass changeit
4) 把密钥复制成服务端 thekeystore
docker 容器创建的时候,宿主机的 /data/etc/cas/cas.keystore,映射容器的 /etc/cas/thekeystore
5.启动容器
docker run -itd --name cas_1 -v /data/etc/cas/config/cas.properties:/etc/cas/config/cas.properties -v /data/etc/cas/services:/etc/cas/services -v /data/etc/cas/cas.keystore:/etc/cas/thekeystore apereo/cas:6.6.13
6.配置 hosts
宿主机地址为:172.17.0.1
server 容器地址为:172.17.0.2
配置宿主机 /etc/hosts 文件
172.17.0.2 cas.server.com
127.0.0.1 cas.client1.com
127.0.0.1 cas.client2.com
配置 server 容器 /etc/hosts 文件
(如果不配置,server 无法根据域名找到 client,无法实现单点登出)
172.17.0.1 cas.client1.com
172.17.0.1 cas.client2.com
7.验证
访问地址:https://cas.server.com:8443/cas/login
初始用户名:casuser
初始密码:Mellon
二、cas manage
(可选)
搭建 cas manage
1.拉取镜像
docker pull apereo/cas-management:6.3.1
2.配置文件
在宿主机,创建文件 /data/etc/cas/config/manager.properties
cas.server.name=https://cas.server.com:8443
cas.server.prefix=${cas.server.name}/cas
mgmt.serverName=http://172.17.0.3:8091
mgmt.adminRoles[0]=ROLE_ADMIN
mgmt.userPropertiesFile=file:/etc/cas/config/users.json
server.port=8091
server.ssl.enabled=false
logging.config=file:/etc/cas/config/log4j2-management.xml
3.启动容器
docker run -itd --name cas_manager_1 -v /data/etc/cas/config/management.properties:/etc/cas/config/management.properties apereo/cas-management:6.3.1
4.导入证书
把 cas.crt 复制到 cas manager 容器
docker cp cas.crt cas_manager_1:/root
cas manager 容器,导入证书,信任 https 服务端
keytool -import -keystore "/opt/java/openjdk/lib/security/cacerts" -file "cas.crt" -alias cas.server.com -storepass changeit
配置 cas manager 容器 /etc/hosts
172.17.0.3 localhost
172.17.0.2 cas.server.com
5.验证
访问地址:http://172.17.0.3:8091/cas-management/
三、cas client
1.maven
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-support-springboot</artifactId>
<version>3.6.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
2.配置
application.properties 文件
# application.properties
server.port=9003
server.servlet.context-path=/
cas.server-url-prefix=https://cas.server.com:8443/cas
cas.server-login-url=https://cas.server.com:8443/cas/login
cas.client-host-url=http://cas.client1.com:9003
cas.single-logout.enabled=true
3.过滤器
3.1 自动配置
在启动类增加 @EnableCasClient
的注解,实现自动配置过滤器
@SpringBootApplication
@EnableCasClient
public class CasApplication3 {
public static void main(String[] args) {
SpringApplication.run(CasApplication3.class, args);
}
}
认证过滤器,配置忽略的 url
/**
* cas-client-support-springboot 依赖提供了CAS客户端的自动配置,
* 当自动配置不满足需要时,可通过实现 {@link CasClientConfigurer} 接口来重写需要自定义的逻辑
*/
@Component
public class CasClientConfigurerImpl implements CasClientConfigurer {
/**
* 配置认证过滤器,添加忽略参数,使 /index 和 /logout 免登录
*/
@Override
public void configureAuthenticationFilter(final FilterRegistrationBean authenticationFilter) {
Map<String, String> initParameters = authenticationFilter.getInitParameters();
initParameters.put("ignorePattern", "(/index)|(/logout)");
}
}
3.2 手动配置
// CASProperties.java
@ConfigurationProperties(prefix = "cas")
@Component
@Getter
@Setter
public class CASProperties {
private String serverUrlPrefix;
private String serverLoginUrl;
private String clientHostUrl;
}
// CASConfiguration.java
@Configuration
public class CASConfiguration {
@Autowired
private CASProperties casProperties;
/**
* 登出过滤器
* SingleSignOutFilter 会拦截所有请求,而不是仅仅拦截 logout 时的请求
* 登陆时,SingleSignOutFilter 会记录 token,session
* 单点登出时,server 请求每个 client,SingleSignOutFilter 会移除 token,session
*/
@Bean
public FilterRegistrationBean filterSingleRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new SingleSignOutFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
Map<String, String> initParameters = new HashMap<>();
initParameters.put("casServerUrlPrefix", casProperties.getServerUrlPrefix());
registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(1);
return registration;
}
/**
* ticket 验证器
*/
@Bean
public FilterRegistrationBean filterValidationRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
Map<String, String> initParameters = new HashMap<String, String>();
initParameters.put("casServerUrlPrefix", casProperties.getServerUrlPrefix());
initParameters.put("serverName", casProperties.getClientHostUrl());
initParameters.put("useSession", "true");
registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(1);
return registration;
}
/**
* 授权过滤器,用于登录
*/
@Bean
public FilterRegistrationBean filterAuthenticationRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new AuthenticationFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
Map<String, String> initParameters = new HashMap<>();
initParameters.put("casServerLoginUrl", casProperties.getServerLoginUrl());
initParameters.put("serverName", casProperties.getClientHostUrl());
// 忽略 url
initParameters.put("ignorePattern", "/logout/success");
initParameters.put("ignoreUrlPatternType", "CONTAINS");
registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(1);
return registration;
}
/**
* wraper 过滤器
* 负责包装HttpServletRequest,从而可通过HttpServletRequest的getRemoteUser()方法获取登录用户的登录名
*/
@Bean
public FilterRegistrationBean filterWrapperRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new HttpServletRequestWrapperFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
// 设定加载的顺序
registration.setOrder(1);
return registration;
}
/**
* 添加监听器
*/
@Bean
public ServletListenerRegistrationBean<EventListener> singleSignOutListenerRegistration() {
ServletListenerRegistrationBean<EventListener> registrationBean = new ServletListenerRegistrationBean<EventListener>();
registrationBean.setListener(new SingleSignOutHttpSessionListener());
registrationBean.setOrder(1);
return registrationBean;
}
}
4.测试
// CASController.java
@Controller
public class CASController {
@ResponseBody
@RequestMapping("/index")
public String index() {
return "index";
}
@ResponseBody
@RequestMapping("/getUsers")
public String getUsers() {
return "name: client C";
}
@RequestMapping("/logout")
public String logout() {
// 调用 https://cas.server.com:8443/cas/logout 后,server 会向每个 client,发送请求,
// SingleSignOutFilter 会注销 session
// 可跳转 https://cas.server.com:8443/cas/logout
// 也可跳转指定页 https://cas.server.com:8443/cas/logout?service=http://cas.client1.com:9003/logout/success,
// 会重定向指定的 service
return "redirect:https://cas.server.com:8443/cas/logout";
}
}
// CasApplication3.java
@SpringBootApplication
// @EnableCasClient 启用cas client 自动配置
public class CasApplication3 {
public static void main(String[] args) {
SpringApplication.run(CasApplication3.class, args);
}
}
四、过滤器
- CAS Single Sign Out Filter——SingleSignOutFilter
实现单点登出,放在首个位置; - CAS Validation Filter——Cas30ProxyReceivingTicketValidationFilter
负责对Ticket的校验
需要指定服务端地址:casServerUrlPrefix
需要指定客户端地址:serverName - CAS Authentication Filter——AuthenticationFilter
负责用户的鉴权
需要指定服务端登录地址:casServerLoginUrl
需要指定客户端地址:serverName - CAS HttpServletRequest Wrapper Filter——HttpServletRequestWrapperFilter
负责包装HttpServletRequest,从而可通过HttpServletRequest的getRemoteUser()方法获取登录用户的登录名
五、流程
- 用户访问
http://cas.client1.com:9003
,过滤器判断用户是否登录,没有登录,则重定向(302)到https://cas.server.com:8443
- 重定向到
https://cas.server.com:8443
,到登陆页面,登陆成功后,https://cas.server.com:8443
将用户登录的信息记录到服务器的 session 中 https://cas.server.com:8443
重定向到cas.client1.com:9003
,并携带凭证,cas.client1.com:9003
拿着凭证去cas.server.com
验证凭证是否有效,从而判断用户是否登录成功- 登录成功,浏览器与网站之间进行正常的访问
1.访问客户端
1) 访问 http://cas.client1.com:9003/getUsers
经过 Cas30ProxyReceivingTicketValidationFilter 过滤器,取出 ticket,若无ticket,进入下个过滤器,即进入 AuthenticationFilter 过滤器。
AuthenticationFilter 过滤器,判断是否有session,若无session,判断是否有 ticket,若无ticket,重定向到 https://cas.server.com:8443/cas/login?service=http://cas.client1.com:9003/getUsers
(重定向生成 302 响应码和 Location 响应头,从而通知浏览器客户端重新访问 Location 响应头中指定的URL)
请求头:
GET /getUsers HTTP/1.1
Host: cas.client1.com:9003
响应头:
HTTP/1.1 302
Location: https://cas.server.com:8443/cas/login?service=http%3A%2F%2Fcas.client1.com%3A9003%2FgetUsers
// Cas30ProxyReceivingTicketValidationFilter.java
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
if (!preFilter(servletRequest, servletResponse, filterChain)) {
return;
}
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String ticket = retrieveTicketFromRequest(request);
if (CommonUtils.isNotBlank(ticket)) {
logger.debug("Attempting to validate ticket: {}", ticket);
try {
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;
}
} catch (final TicketValidationException e) {
logger.debug(e.getMessage(), e);
onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(e);
}
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
// AuthenticationFilter.java
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
if (isRequestUrlExcluded(request)) {
logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
return;
}
final HttpSession session = request.getSession(false);
final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
if (assertion != null) {
filterChain.doFilter(request, response);
return;
}
final String serviceUrl = constructServiceUrl(request, response);
final String ticket = retrieveTicketFromRequest(request);
final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
filterChain.doFilter(request, response);
return;
}
final String modifiedServiceUrl;
logger.debug("no ticket and no assertion found");
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);
final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
logger.debug("redirecting to "{}"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}
2) 访问 https://cas.server.com:8443/cas/login?service=http://cas.client1.com:9003/getUsers
,进行登陆成功后,重新定向 http://cas.client1.com:9003/getUsers?ticket=ST-1-kQdfseIgdkZbIfsMWtFQXhmM-2kzxm-pc
,并携带 Set-Cookie
这个 ticket 就是 ST,同时会在Cookie中设置一个 TGC,该 cookie 是网站 cas.server.com 的 cookie ,只有访问这个网站才会携带这个 cookie 过去
Cookie 中的 TGC:向 cookie 中添加该值的目的是当下次访问 cas.server.com 时,浏览器将 Cookie 中的 TGC 携带到服务器,服务器根据这个 TGC,查找与之对应的 TGT。从而判断用户是否登录过了,是否需要展示登录页面。TGT 与 TGC 的关系就像 SESSION 与 Cookie 中 SESSIONID 的关系
- TGT:Ticket Granted Ticket(俗称大令牌,或者说票根,他可以签发ST)
- TGC:Ticket Granted Cookie(cookie中的value),存在Cookie中,根据他可以找到TGT
- ST:Service Ticket (小令牌),是TGT生成的,默认是用一次就生效了。也就是 ticket 值
请求头:
POST /cas/login?service=http%3A%2F%2Fcas.client1.com%3A9003%2FgetUsers HTTP/1.1
Host: cas.server.com:8443
Origin: http://cas.server.com:8443
Referer: http://cas.server.com:8443/cas/login?service=http%3A%2F%2Fcas.client1.com%3A9003%2FgetUsers
响应头:
HTTP/1.1 302
Set-Cookie: TGC=eyJhbGciOiJIUzUxMiJ9.ZXlKNmFYQWlPaUpFUlVZaUxDSmhiR2NpT2lKa2FYSWlMQ0psYm1NaU9pSkJNVEk0UTBKRExVaFRNalUySW4wLi5lYUxnUk5FZ2NrcE5TZDd6STNwTHBnLnRuM2ZQS0tZZnRjM3I1RWdxZkJUbzZqT2xvTlFpQWRTay1OT2JHalhMd3ZsRlYyMk52NGNTd3JKNVhHSW1nSFVlN3NZalNrbDBOSUxmdVZmX0c5dE1wdXpMRG9RNEhua0hiMlBvSDNUU1Qxa2tDTl9tV1l2UWlLejl0REt2Y1N5Qk5RT2s4WTJESkNvY2pzZjRFMWotRDVCQmtxUXlka0VXMDZ5U3NydHMtZEJaWE92NXUyMkZ1UUxFdUhKanVyVGNvckpJLXVZUUFNMkkzWGJtVElleHBNb19wM2ZMY1l3cXFuS29aV3ZHMmsuRDlvWFNGWTBiTFliem5wa0FiQ1ExQQ==.qeJmVbtNlVkbMFoFL1XjFD8o4BY8EtbZym10dAJoVqeHvy4miF9QB6pfurvR0iaBlgOcXTaJfYRhH3sSQM5Ysw; Path=/cas/; HttpOnly
Location: http://cas.client1.com:9003/getUsers?ticket=ST-1-kQdfseIgdkZbIfsMWtFQXhmM-2kzxm-pc
3) 访问 http://cas.client1.com:9003/getUsers?ticket=ST-1-FFtKZrDi1Fs-r4azaIkaXvDHIc4zxm-pc
,进入 Cas30ProxyReceivingTicketValidationFilter 过滤器,取出 ticket,使用 Cas30ServiceTicketValidator 验证。
Cas30ServiceTicketValidator 进行 java 请求 https://cas.server.com:8443/cas/p3/serviceValidate?ticket=ST-1-kQdfseIgdkZbIfsMWtFQXhmM-2kzxm-pc&service=http://cas.client1.com:9003/getUsers
,返回验证信息,封装到 Assertion,把其放入 request 和 session,后重定向到 http://cas.client1.com:9003/getUsers;jsessionid=518E600320516FA8EA8BC9C603E7198E
请求头:
GET /getUsers?ticket=ST-1-xiFOAQsTeydCG6y67-mxTnPZx6Uzxm-pc HTTP/1.1
Host: cas.client1.com:9003
Referer: https://cas.server.com:8443/
响应头:
HTTP/1.1 302
Set-Cookie: JSESSIONID=518E600320516FA8EA8BC9C603E7198E; Path=/; HttpOnly
Location: http://cas.client1.com:9003/getUsers;jsessionid=518E600320516FA8EA8BC9C603E7198E
4) 访问 http://cas.client1.com:9003/getUsers;jsessionid=518E600320516FA8EA8BC9C603E7198E
,进入 Cas30ProxyReceivingTicketValidationFilter 过滤器,取出 ticket,无 ticket,进入 AuthenticationFilter 过滤器,得到 session,从 session 得到 Assertion,若 Assertion 不为空,进入下个过滤器,即 HttpServletRequestWrapperFilter 过滤器
ST 的默认过期策略,st.timeToKillInSeconds=10 当你访问一个应用系统时,cas server 签发了一张票据,你需要在十秒钟之内拿着这种 ST 去 cas server 进行校验,过了10秒钟就过期了。
请求头:
GET /getUsers;jsessionid=518E600320516FA8EA8BC9C603E7198E HTTP/1.1
Cookie: JSESSIONID=518E600320516FA8EA8BC9C603E7198E
Host: cas.client1.com:9003
Referer: https://cas.server.com:8443/
5) 用户第二次访问
因为 http://cas.client1.com:9003
保存了 session,所以验证通过
2.访问其他客户端
1) 用户访问 http://cas.client2.com:9004/getUsers
,重定向到 https://cas.server.com:8443/cas/login?service=http://cas.client2.com:9004/getUsers
(重定向生成 302 响应码和 Location 响应头,从而通知浏览器客户端重新访问 Location 响应头中指定的URL)
请求头中携带 Cookie
请求头:
GET /getUsers HTTP/1.1
Cookie: JSESSIONID=518E600320516FA8EA8BC9C603E7198E
Host: cas.client2.com:9004
响应头:
HTTP/1.1 302
Location: https://cas.server.com:8443/cas/login?service=http%3A%2F%2Fcas.client2.com%3A9004%2FgetUsers
2) 重定向 https://cas.server.com:8443/cas/login?service=http%3A%2F%2Fcas.client2.com%3A9004%2FgetUsers
后,cas.server.com 进行处理,验证登陆过,不跳转登陆页面,重定向到 http://cas.client2.com:9004/getUsers?ticket=ST-2-dzyqpzz68wmHgbToR5tFtY0W8Oczxm-pc
请求头:
GET /cas/login?service=http%3A%2F%2Fcas.client2.com%3A9004%2FgetUsers HTTP/1.1
Cookie: TGC=eyJhbGciOiJIUzUxMiJ9.ZXlKNmFYQWlPaUpFUlVZaUxDSmhiR2NpT2lKa2FYSWlMQ0psYm1NaU9pSkJNVEk0UTBKRExVaFRNalUySW4wLi5lYUxnUk5FZ2NrcE5TZDd6STNwTHBnLnRuM2ZQS0tZZnRjM3I1RWdxZkJUbzZqT2xvTlFpQWRTay1OT2JHalhMd3ZsRlYyMk52NGNTd3JKNVhHSW1nSFVlN3NZalNrbDBOSUxmdVZmX0c5dE1wdXpMRG9RNEhua0hiMlBvSDNUU1Qxa2tDTl9tV1l2UWlLejl0REt2Y1N5Qk5RT2s4WTJESkNvY2pzZjRFMWotRDVCQmtxUXlka0VXMDZ5U3NydHMtZEJaWE92NXUyMkZ1UUxFdUhKanVyVGNvckpJLXVZUUFNMkkzWGJtVElleHBNb19wM2ZMY1l3cXFuS29aV3ZHMmsuRDlvWFNGWTBiTFliem5wa0FiQ1ExQQ==.qeJmVbtNlVkbMFoFL1XjFD8o4BY8EtbZym10dAJoVqeHvy4miF9QB6pfurvR0iaBlgOcXTaJfYRhH3sSQM5Ysw
Host: cas.server.com:8443
响应头:
HTTP/1.1 302
Location: http://cas.client2.com:9004/getUsers?ticket=ST-2-dzyqpzz68wmHgbToR5tFtY0W8Oczxm-pc
3) 重定向到 http://cas.client2.com:9004/getUsers?ticket=ST-2-dzyqpzz68wmHgbToR5tFtY0W8Oczxm-pc
,经过过滤器验证后,重定向到 http://cas.client2.com:9004/getUsers
,
请求头:
GET /getUsers?ticket=ST-2-dzyqpzz68wmHgbToR5tFtY0W8Oczxm-pc HTTP/1.1
Cookie: JSESSIONID=518E600320516FA8EA8BC9C603E7198E
响应头:
HTTP/1.1 302
Set-Cookie: JSESSIONID=BE8C4E61A1749069899253B36144E496; Path=/; HttpOnly
Location: http://cas.client2.com:9004/getUsers
4) 重定向到 http://cas.client2.com:9004/getUsers
,判断 session,成功访问
请求头:
GET /getUsers HTTP/1.1
Cookie: JSESSIONID=BE8C4E61A1749069899253B36144E496
3.登出
// SingleSignOutFilter.java
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
/**
* <p>Workaround for now for the fact that Spring Security will fail since it doesn't call {@link #init(javax.servlet.FilterConfig)}.</p>
* <p>Ultimately we need to allow deployers to actually inject their fully-initialized {@link org.jasig.cas.client.session.SingleSignOutHandler}.</p>
*/
if (!this.handlerInitialized.getAndSet(true)) {
HANDLER.init();
}
if (HANDLER.process(request, response)) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
登陆操作:cas client 应用把 session 和 ticket 关系保存到 SessionMappingStorage 中。
退出操作:cas server 接收请求后,获取浏览器的 cookie 中的 tgc 信息,对 tgc 信息进行解密,解密后将获取到 TGT 的 ID,注销该 TGT。cas server 除了自己销毁 TGT 外,还需要通知 cas client 销毁 ticket。cas server 发送带 logoutRequest 参数的 http 请求到每一个 cas client。
logoutRequest 的值为:
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-12-fuDolbm4i66mhG28CYqrw8cd" Version="2.0" IssueInstant="2023-12-15T03:21:24Z">
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">casuser</saml:NameID>
<samlp:SessionIndex>ST-11-8xVp0mCsxaLnAKWO-yxWGGuZNxw-36c3c2e505d2</samlp:SessionIndex>
</samlp:LogoutRequest>"
其他:若要实现 session 共享,可 cas 通过 redis 覆写可以实现多节点 cas 的集群(都是用 redis 存储 session,不再使用 HashMapBackedSessionMappingStorage 这个内部的 map 存)
// SingleSignOutHandler.java
public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
if (isTokenRequest(request)) {
logger.trace("Received a token request");
recordSession(request);
return true;
} else if (isBackChannelLogoutRequest(request)) {
logger.trace("Received a back channel logout request");
destroySession(request);
return false;
} else if (isFrontChannelLogoutRequest(request)) {
logger.trace("Received a front channel logout request");
destroySession(request);
// redirection url to the CAS server
final String redirectionUrl = computeRedirectionToServer(request);
if (redirectionUrl != null) {
CommonUtils.sendRedirect(response, redirectionUrl);
}
return false;
} else {
logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
return true;
}
}
private void recordSession(final HttpServletRequest request) {
final HttpSession session = request.getSession(this.eagerlyCreateSessions);
if (session == null) {
logger.debug("No session currently exists (and none created). Cannot record session information for single sign out.");
return;
}
final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);
logger.debug("Recording session for token {}", token);
try {
this.sessionMappingStorage.removeBySessionById(session.getId());
} catch (final Exception e) {
// ignore if the session is already marked as invalid. Nothing we can do!
}
sessionMappingStorage.addSessionById(token, session);
}
private void destroySession(final HttpServletRequest request) {
final String logoutMessage;
// front channel logout -> the message needs to be base64 decoded + decompressed
if (isFrontChannelLogoutRequest(request)) {
logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request,
this.frontLogoutParameterName));
} else {
logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
}
logger.trace("Logout request:n{}", logoutMessage);
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
if (CommonUtils.isNotBlank(token)) {
final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
if (session != null) {
final String sessionID = session.getId();
logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);
try {
session.invalidate();
} catch (final IllegalStateException e) {
logger.debug("Error invalidating session.", e);
}
this.logoutStrategy.logout(request);
}
}
}