# CAS SSO Demo
**Repository Path**: lxiaomin/CAS-SSO-Demo
## Basic Information
- **Project Name**: CAS SSO Demo
- **Description**: CAS
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 5
- **Forks**: 1
- **Created**: 2018-09-03
- **Last Updated**: 2025-11-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# CAS SSO 安装部署文档
## 1 CAS 简介
### 1.1 CAS的工作流程
参考以下两篇博客,讲得非常详细,本文就不复制粘贴了。
- CAS单点登录原理解析:https://www.cnblogs.com/lihuidu/p/6495247.html
- CAS实现SSO单点登录原理:https://blog.csdn.net/cruise_h/article/details/51013597
- CAS 比较通俗易懂的原理解释:https://blog.csdn.net/wang379275614/article/details/46337529
### 1.2 CAS官方文档(5.3.X)
- CAS 5.3.x:https://apereo.github.io/cas/5.3.x/index.html
- CAS Quick Start:https://apereo.github.io/cas/5.3.x/planning/Getting-Started.html
### 1.3 重要术语
CAS系统中设计了5中票据:**TGC、ST、PGT、PGTIOU、PT**。
1. **Ticket-granting cookie(TGC)**:存放用户身份认证凭证的cookie,在浏览器和CAS Server间通讯时使用,并且只能基于安全通道传输(Https),是CAS Server用来明确用户身份的凭证;
2. **Service ticket(ST):服务票据**,服务的惟一标识码,由CAS Server发出(Http传送),通过客户端浏览器到达业务服务器端;一个特定的服务只能有一个惟一的ST;
3. **Proxy-Granting ticket(PGT)**:由CAS Server颁发给拥有ST凭证的服务,PGT绑定一个用户的特定服务,使其拥有向CAS Server申请,获得PT的能力;
4. **Proxy-Granting Ticket I Owe You(PGTIOU)**:作用是将通过凭证校验时的应答信息由CAS Server 返回给CAS Client,同时,与该PGTIOU对应的PGT将通过回调链接传给Web应用。Web应用负责维护PGTIOU与PGT之间映射关系的内容表;
5. **Proxy Ticket (PT)**:是应用程序代理用户身份对目标程序进行访问的凭证;
## 2 CAS 服务端安装
###2.1 War包部署
####2.1.1 项目源码下载
1. 官方推荐的部署方式是在Tomcat下进行部署,首先在以下链接下载CAS Server项目:[CAS 5.3版本](https://github.com/apereo/cas-overlay-template/tree/5.3)
2. 导入IDEA中
3. 若无需特殊配置,即可在项目根目录下,使用以下命令直接打出war包。
```shell
mvn install
```
#### 2.1.2 Tomcat配置
> CAS server认证的时候需要使用到HTTPS协议,所以需要申请证书并配置Tomcat。若服务器已有域名并支持了HTTPS协议,则忽略这一步。直接将2.1.1步骤的war包部署到Tomcat webapps目录下即可。
1. 使用Keytool工具生成证书。
```powershell
#/Users/tomcat.keystore中,/Users是目录名称,tomcat.keystore是存储秘钥和证书的文件。
keytool -genkey -alias tomcat -keyalg RSA -keystore /Users/tomcat.keystore
#注意:生成证书的时候,会提示您的姓名和姓氏之类的问题,请输入你的域名,如:
#您的姓名和姓氏是什么?
#mydomin.com
#导出证书到指定目录
keytool -export -trustcacerts -alias tomcat -file /Users/tomcat.cer -keystore /Users/tomcat.keystore
#在JDK中导入证书,在root权限下执行,默认密码为:changeit,其中的字符串参数为jdk的目录,应改为本机JDK目录。
keytool -import -trustcacerts -alias tomcat -file /Users/tomcat.cer -keystore "/Java/jdk1.8.0_151/jre/lib/security/cacerts"
#查看证书(只是确认是否导入)
keytool -list -v -keystore "/Java/jdk1.8.0_151/jre/lib/security/cacerts"
```
2. Tomcat 的配置
配置文件目录:**conf->server.xml**
增加以下标签:**keystoreFile**属性为上述生成证书和秘钥的文件的位置,**keystorePass**是生成证书时候设置的密码。
```xml
```
3. 将cas.war放入tomcat下的webapp目录中。
4. 启动tomcat:
```powershell
#更改权限
chmod 755 startup.sh
#启动tomcat
./startup.sh
```
### 2.2 Spring boot启动
### 2.3 数据库认证配置
1. 修改pom.xml文件如下。
```xml
5.1.46
org.apereo.cas
cas-server-support-jdbc
${cas.version}
org.apereo.cas
cas-server-support-jdbc-drivers
${cas.version}
mysql
mysql-connector-java
${mysql.driver.version}
```
2. 重新打包,打出war包。
```shell
#clean
mvn clean
#重新下载新的依赖并打包
mvn install
```
3. 将war包部署到tomcat webapp的目录下,并启动项目。
```shell
./startup.sh
```
4. 打开目录:**WEB-INF -> classes -> application.properties**
```properties
#注释以下的配置,下面是静态的密码账号。
# cas.authn.accept.users=casuser::Mellon
#新增如下数据库配置,按需修改,不能复制全部
cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/videomeeting?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].sql=select * from t_user_info where id=?
cas.authn.jdbc.query[0].fieldPassword=passwd
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
```
5. 重新启动Tomcat,执行目录为 Tomcat下的bin目录。
```
#关闭
killall java
#重启
./startup.sh
```
6. 重新打开并校验:https://mydomin.cn:8443/cas/login
### 2.4 自定义UI说明
####2.4.1 war包
> 在war包下的以下目录:WEB-INF->classes->templates
>
> 可以看到一系列的HTML模板信息,可根据需求进行自定义。
## 3 CAS客户端在Spring Boot的集成
###3.1 测试环境说明
- Maven 3
- Spring Boot 2.0.4
- JDK 1.8
### 3.2 Maven 配置
```xml
4.0.0
cn.lxm.cas
client-2
0.0.1-SNAPSHOT
jar
client-2
Demo project for Spring Boot
cn.lxm
cas
0.0.1-SNAPSHOT
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.thymeleaf.extras
thymeleaf-extras-springsecurity4
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.security
spring-security-cas
org.springframework.boot
spring-boot-starter-test
com.google.code.gson
gson
2.8.5
org.springframework.boot
spring-boot-maven-plugin
```
### 3.3 配置项
在spring boot默认目录下新增一个config的目录config,存放Spring Boot的Java配置。
1. CasConfiguration.java
```java
package cn.lxm.cas.client2.config;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true)
public class CasConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private CasProperties casProperties;
/*定义认证用户信息获取来源,密码校验规则等*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider());
}
/*定义安全策略*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//配置安全策略
.antMatchers("/","/index").permitAll()//定义/请求不需要验证
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated()//其余的所有请求都需要验证
.and()
.logout().permitAll()//定义logout不需要验证
.and()
.formLogin();//使用form表单登录
http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint())
.and()
.addFilter(casAuthenticationFilter())
.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
//http.csrf().disable(); //禁用CSRF
}
/*认证的入口*/
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
/*指定service相关信息*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casProperties.getAppUrl() + casProperties.getAppLoginUrl());
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/*CAS认证过滤器*/
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
return casAuthenticationFilter;
}
/*cas 认证 Provider*/
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(casUserDetailsService());
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey("casAuthenticationProviderKey");
return casAuthenticationProvider;
}
/*用户自定义的AuthenticationUserDetailsService*/
@Bean
public AuthenticationUserDetailsService casUserDetailsService() {
return new CasUserDetailsService();
}
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl());
}
/*单点登出过滤器*/
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(casProperties.getCasServerUrl());
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
/*请求单点退出过滤器*/
@Bean
public LogoutFilter casLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(),
new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
return logoutFilter;
}
}
```
2. CasProperties.java. 用于读取配置项。
```java
package cn.lxm.cas.client2.config;
/**
* @author LXM
* @Title: cas
* @Description:
* @date 2018/8/30下午1:31
*/
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class CasProperties {
@Value("${cas.server.url}")
private String casServerUrl;
@Value("${cas.server.login_url}")
private String casServerLoginUrl;
@Value("${cas.server.logout_url}")
private String casServerLogoutUrl;
@Value("${app.url}")
private String appUrl;
@Value("${app.login_url}")
private String appLoginUrl;
@Value("${app.logout_url}")
private String appLogoutUrl;
public String getCasServerUrl() {
return casServerUrl;
}
public void setCasServerUrl(String casServerUrl) {
this.casServerUrl = casServerUrl;
}
public String getCasServerLoginUrl() {
return casServerLoginUrl;
}
public void setCasServerLoginUrl(String casServerLoginUrl) {
this.casServerLoginUrl = casServerLoginUrl;
}
public String getCasServerLogoutUrl() {
return casServerLogoutUrl;
}
public void setCasServerLogoutUrl(String casServerLogoutUrl) {
this.casServerLogoutUrl = casServerLogoutUrl;
}
public String getAppUrl() {
return appUrl;
}
public void setAppUrl(String appUrl) {
this.appUrl = appUrl;
}
public String getAppLoginUrl() {
return appLoginUrl;
}
public void setAppLoginUrl(String appLoginUrl) {
this.appLoginUrl = appLoginUrl;
}
public String getAppLogoutUrl() {
return appLogoutUrl;
}
public void setAppLogoutUrl(String appLogoutUrl) {
this.appLogoutUrl = appLogoutUrl;
}
@Override
public String toString() {
return "CasProperties{" +
"casServerUrl='" + casServerUrl + '\'' +
", casServerLoginUrl='" + casServerLoginUrl + '\'' +
", casServerLogoutUrl='" + casServerLogoutUrl + '\'' +
", appUrl='" + appUrl + '\'' +
", appLoginUrl='" + appLoginUrl + '\'' +
", appLogoutUrl='" + appLogoutUrl + '\'' +
'}';
}
}
```
3. CasUserDetailService.java
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author LXM
* @Title: cas
* @Description: * 用于加载用户信息
* 实现UserDetailsService接口,或者实现AuthenticationUserDetailsService接口
* @date 2018/8/30下午1:32
*/
public class CasUserDetailsService implements AuthenticationUserDetailsService {
private static final Logger log = LoggerFactory.getLogger(CasUserDetailsService.class);
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
String username = token.getName();
log.debug("current username [{}]", username);
// 这里应该查询数据库获取具体的用户信息和权限信息,这里是开始授权的地方。
Collection authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User(username, username, authorities);
}
}
```
### 3.4 视图控制器(前端保护)
1. HomeController.java 做测试的控制器
```java
import com.google.gson.Gson;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
@CrossOrigin
@Controller
public class HomeController {
@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
public String index(ModelMap modelMap, HttpSession session) {
modelMap.addAttribute("msg", "welcome to spring-boot-cas-client!");
modelMap.addAttribute("sessionId", session.getId());
return "index";
}
@GetMapping(value = "/user")
@PreAuthorize("hasRole('ROLE_USER')")
public String user(ModelMap modelMap) {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
modelMap.addAttribute("user", user);
return "user";
}
```
### 3.5 资源控制器(后端保护)
####3.5.1 本域下访问
本域名下访问没有跨域问题,常规写法即可。
```java
package cn.lxm.cas.client2.controller;
import cn.lxm.cas.client2.config.CasProperties;
import com.google.gson.Gson;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author LXM
* @Title: cas
* @Description:
* @date 2018/9/1下午4:42
*/
@Controller
@CrossOrigin
public class ResourceController {
@GetMapping("/resource")
@PreAuthorize("hasAnyRole('ROLE_USER')")
public CasProperties test(HttpServletRequest reques, HttpServletResponse response) {
CasProperties properties = new CasProperties();
properties.setAppLoginUrl("12313133");
properties.setCasServerLogoutUrl("33333");
return properties;
}
}
```
#### 3.5.2 AJAX跨域访问
对于ajax跨域访问的请求,请求的流程一般如下:

为了解决跨域请求问题,同时不影响同域下访问,需要**对提供RESTAPI资源的Client-2**做以下修改:
1. **Client-2**增加对jsonp的支持。JSONPConfiguration.java
```java
package cn.lxm.cas.client2.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;
/**
* @author LXM
* @Title: cas
* @Description:
* @date 2018/9/3上午11:20
*/
@ControllerAdvice(basePackages = {"cn.lxm.cas.client2.controller"})
public class JSONPConfiguration extends AbstractJsonpResponseBodyAdvice {
public JSONPConfiguration() {
super("callback", "jsonp");
}
}
```
2. **Client-2**增加一个处理跨域请求的filter:SimpleCORSFilter.java
```java
package cn.lxm.cas.client2.filters;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author LXM
* @Title: videoconferencing
* @Description:
* @date 2018/8/6上午10:57
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCORSFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
String originHeader = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", originHeader);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("XDomainRequestAllowed", "1");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Key, Authorization");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
public void init(FilterConfig filterConfig) {
}
public void destroy() {
}
}
```
3. **Client-2**示例的Controller: ResourceController.java
```Java
/**
* @author LXM
* @Title: cas
* @Description:
* @date 2018/9/1下午4:42
*/
@RestController
@CrossOrigin
public class ResourceController {
@RequestMapping(value = "test", produces = "application/json; charset=UTF-8", method = {RequestMethod.GET, RequestMethod.POST})
@PreAuthorize("hasAnyRole('ROLE_USER')")
public CasProperties initDbDatasC() {
CasProperties properties = new CasProperties();
properties.setAppLoginUrl("12313133");
properties.setCasServerLogoutUrl("33333");
return properties;
}
}
```
4. **Client-1** 请求**Client-2**资源的Ajax示例:
```javascript
$.ajax({
type: "get",
async: false,
url: "http://mydomin.cn:9091/test/",
dataType: "jsonp",
jsonp: "callback",//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(默认为:callback)
jsonpCallback: "success_jsonpCallback",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名
success: function (json) {
alert(json);
},
error: function () {
alert('fail');
}
});
```
### 3.6 客户端-2
第二个客户端的代码与第一个客户端的基础部分代码可以完全一样,在Client-1中访问到了受保护的资源,经过CAS登录后,便可以在Client-2中访问,不会被过滤器拦截。
##4 常见问题
###4.1 未认证授权服务错误提示
1. 打开war包解压的cas目录下的:**WEB-INF -> classes -> services -> HTTPSandIMAPS-10000001.json**
2. 将其中的这一句修改:
```json
"serviceId" : "^(https|imaps)://.*",
//修改为:
"serviceId" : "^(https|imaps|http)://.*",
```
3. 在 **WEB-INF -> classes -> application.properties**中增加一行:
```properties
cas.serviceRegistry.initFromJson=true
```
## 5 demo
说明:
- Client-1的端口为9090,Client-2的端口为9091
- 在本机测试环境中,我在host文件中加入了以下一行,假装自己有域名。
```
127.0.0.1 mydomin.cn
```
- Client-1与Client-2之间单点登录后可以相互访问
- Client-1中在*mydomin.cn/function* 页面中可以对Client-2中的资源进行跨域调用。