1. Overview
1.概述
In this tutorial, we’ll be looking at the Apereo Central Authentication Service (CAS) and we’ll see how a Spring Boot service can use it for authentication. CAS is an enterprise Single Sign-On (SSO) solution that is also open source.
在本教程中,我们将了解Apereo中央认证服务(CAS),并看看Spring Boot服务如何使用它进行认证。CAS是一个企业单点登录(SSO)解决方案,也是开源的。
2. CAS Server Setup
2.CAS服务器的设置
2.1. CAS Installation and Dependencies
2.1.CAS的安装和依赖性
The server uses the Maven (Gradle) War Overlay style to ease setup and deployment:
该服务器使用Maven(Gradle)的War Overlay风格来简化设置和部署。
git clone https://github.com/apereo/cas-overlay-template.git cas-server
This command will clone the cas-overlay-template into the cas-server directory.
这个命令将把cas-overlay-template克隆到cas-server目录中。
Some of the aspects we’ll be covering include JSON service registration and JDBC database connection. So, we’ll add their modules to the dependencies section of build.gradle file:
我们将涉及的一些方面包括JSON服务注册和JDBC数据库连接。因此,我们将把它们的模块添加到build.gradle文件的dependencies部分。
compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"
Let’s make sure to check the latest version of casServer.
让我们确保检查casServer的最新版本。
2.2. CAS Server Configuration
2.2.CAS服务器配置
Before we can start the CAS server, we need to add some basic configurations. Let’s start by creating a cas-server/src/main/resources folder and in this folder. This will be followed by the creation of application.properties in the folder, too:
在我们启动CAS服务器之前,我们需要添加一些基本配置。让我们首先创建一个cas-server/src/main/resources文件夹,并在这个文件夹中。随后也将在该文件夹中创建application.properties。
server.port=8443
spring.main.allow-bean-definition-overriding=true
server.ssl.key-store=classpath:/etc/cas/thekeystore
server.ssl.key-store-password=changeit
Let’s proceed with the creation of the key-store file referenced in the configuration above. First, we need to create the folders /etc/cas and /etc/cas/config in cas-server/src/main/resources.
让我们继续创建上述配置中提到的钥匙库文件。首先,我们需要在cas-server/src/main/resources中创建文件夹/etc/cas和/etc/cas/config。
Then, we need to change the directory to cas-server/src/main/resources/etc/cas and run the command to generate thekeystore:
然后,我们需要将目录改为cas-server/src/main/resources/etc/cas,并运行命令来生成thekeystore。
keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048
In order for us not to have an SSL handshake error, we should use localhost as the value of first and last name. We should use the same for the organisation name and unit as well. Furthermore, we need to import the thekeystore into the JDK/JRE we’ll be using to run our client app:
为了不出现SSL握手错误,我们应该使用localhost作为名字和姓氏的值。我们也应该对组织名称和单位使用相同的值。此外,我们需要将thekeystore导入我们将用来运行客户端应用程序的JDK/JRE。
keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts
The password for the source and destination keystore is changeit. On Unix systems, we may have to run this command with admin (sudo) privilege. After importing, we should restart all instances of Java that’s running or restart the system.
源和目的密钥库的密码是changeit。在Unix系统上,我们可能需要以管理员(sudo)的权限运行这个命令。导入后,我们应该重新启动所有正在运行的Java实例或重新启动系统。
We’re using JDK11 because it’s required by CAS version 6.1.x. Also, we defined the environment variable $JAVA11_HOME that points to its home directory. We can now start the CAS server:
我们使用JDK11,因为它是CAS 6.1.x版本所要求的。同时,我们定义了环境变量$JAVA11_HOME,指向其主目录。现在我们可以启动CAS服务器了。
./gradlew run -Dorg.gradle.java.home=$JAVA11_HOME
When the application starts, we’ll see “READY” printed on the terminal and the server will be available at https://localhost:8443.
当应用程序启动时,我们将看到终端上打印的 “READY”,服务器将在https://localhost:8443上可用。
2.3. CAS Server User Configuration
2.3.CAS服务器用户配置
We can’t log in yet as we’ve not configured any user. CAS has different methods of managing configuration, including the standalone mode. Let’s create a config folder cas-server/src/main/resources/etc/cas/config in which we’ll create a properties file cas.properties. Now, we can define a static user in the properties file:
我们还不能登录,因为我们还没有配置任何用户。CAS有不同的管理配置的方法,包括独立模式。让我们创建一个配置文件夹cas-server/src/main/resources/etc/cas/config,在其中我们将创建一个属性文件cas.properties。现在,我们可以在属性文件中定义一个静态用户。
cas.authn.accept.users=casuser::Mellon
We have to communicate the location of the config folder to CAS server for the settings to take effect. Let’s update tasks.gradle so we can pass the location as a JVM argument from the command line:
我们必须将配置文件夹的位置传达给CAS服务器,以使设置生效。让我们更新tasks.gradle,这样我们就可以从命令行中传递位置作为JVM参数。
task run(group: "build", description: "Run the CAS web application in embedded container mode") {
dependsOn 'build'
doLast {
def casRunArgs = new ArrayList<>(Arrays.asList(
"-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" ")))
if (project.hasProperty('args')) {
casRunArgs.addAll(project.args.split('\\s+'))
}
javaexec {
main = "-jar"
jvmArgs = casRunArgs
args = ["build/libs/${casWebApplicationBinaryName}"]
logger.info "Started ${commandLine}"
}
}
}
We then save the file and run:
然后我们保存该文件并运行。
./gradlew run
-Dorg.gradle.java.home=$JAVA11_HOME
-Pargs="-Dcas.standalone.configurationDirectory=/cas-server/src/main/resources/etc/cas/config"
Please note that the value of cas.standalone.configurationDirectory is an absolute path. We can now go to https://localhost:8443 and log in with username casuser and password Mellon.
请注意,cas.standalone.configurationDirectory的值是一个绝对路径。我们现在可以到https://localhost:8443,用用户名casuser和密码Mellon登录。
3. CAS Client Setup
3.CAS客户端设置
We’ll use Spring Initializr to generate a Spring Boot client app. It’ll have Web, Security, Freemarker and DevTools dependencies. Besides, we’ll also add the dependency for Spring Security CAS module to its pom.xml:
我们将使用Spring Initializr来生成一个Spring Boot客户端应用程序。它将有Web、Security、Freemarker和DevTools依赖。此外,我们还将把Spring Security CAS模块的依赖性添加到它的pom.xml。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<versionId>5.3.0.RELEASE</versionId>
</dependency>
Finally, let’s add the following Spring Boot properties to configure the app:
最后,让我们添加以下Spring Boot属性来配置该应用程序。
server.port=8900
spring.freemarker.suffix=.ftl
4. CAS Server Service Registration
4.CAS服务器服务注册
Clients applications must register with the CAS server ahead of any authentication. CAS server supports the use of YAML, JSON, MongoDB and LDAP client registries.
客户应用程序必须在任何认证之前向CAS服务器注册。CAS服务器支持使用YAML、JSON、MongoDB和LDAP客户端注册。
In this tutorial, we’ll use the JSON Service Registry method. Let’s create yet another folder cas-server/src/main/resources/etc/cas/services. It’s this folder that’ll house the service registry JSON files.
在本教程中,我们将使用JSON服务注册方法。让我们再创建一个文件夹cas-server/src/main/resources/etc/cas/services。这个文件夹将存放服务注册表的JSON文件。
We’ll create a JSON file that contains the definition of our client application. The name of the file, casSecuredApp-8900.json, follows the pattern serviceName-Id.json:
我们将创建一个JSON文件,其中包含我们客户端应用程序的定义。该文件的名称,casSecuredApp-8900.json,遵循serviceName-Id.json模式。
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "http://localhost:8900/login/cas",
"name" : "casSecuredApp",
"id" : 8900,
"logoutType" : "BACK_CHANNEL",
"logoutUrl" : "http://localhost:8900/exit/cas"
}
The serviceId attribute defines a regex URL pattern for the client application. The pattern should match the URL of the client application.
serviceId属性为客户端应用程序定义了一个regex URL模式。该模式应与客户端应用程序的URL相匹配。
The id attribute should be unique. In other words, there shouldn’t be two or more services with the same id registered to the same CAS server. Having duplicate id will lead to conflicts and overriding of configurations.
id属性应该是唯一的。换句话说,不应该有两个或多个具有相同id的服务注册到同一个CAS服务器。拥有重复的id将导致冲突和配置的覆盖。
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.json.location=classpath:/etc/cas/services
5. CAS Client Single Sign-On Configuration
5.CAS客户端单点登录配置
The next step for us is to configure Spring Security to work with the CAS server. We should also check the full flow of interactions, called a CAS sequence.
我们的下一步是配置Spring Security以与CAS服务器一起工作。我们还应该检查完整的交互流程,称为CAS序列。
Let’s add the following bean configurations to the CasSecuredApplication class of our Spring Boot app:
让我们在Spring Boot应用程序的CasSecuredApplication类中添加以下bean配置。
@Bean
public CasAuthenticationFilter casAuthenticationFilter(
AuthenticationManager authenticationManager,
ServiceProperties serviceProperties) throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager);
filter.setServiceProperties(serviceProperties);
return filter;
}
@Bean
public ServiceProperties serviceProperties() {
logger.info("service properties");
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("http://cas-client:8900/login/cas");
serviceProperties.setSendRenew(false);
return serviceProperties;
}
@Bean
public TicketValidator ticketValidator() {
return new Cas30ServiceTicketValidator("https://localhost:8443");
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider(
TicketValidator ticketValidator,
ServiceProperties serviceProperties) {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties);
provider.setTicketValidator(ticketValidator);
provider.setUserDetailsService(
s -> new User("test@test.com", "Mellon", true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
return provider;
}
The ServiceProperties bean has the same URL as the serviceId in casSecuredApp-8900.json. This is important because it identifies this client to the CAS server.
ServiceProperties Bean的URL与casSecuredApp-8900.json中的serviceId相同。这很重要,因为它可以向 CAS 服务器识别这个客户端。
The sendRenew property of ServiceProperties is set to false. This means a user only needs to present login credentials to the server once.
ServiceProperties的sendRenew属性被设置为false。这意味着用户只需要向服务器出示一次登录凭证。
The AuthenticationEntryPoint bean will handle authentication exceptions. Thus, it’ll redirect the user to the login URL of the CAS server for authentication.
AuthenticationEntryPoint bean将处理认证异常。因此,它将把用户重定向到CAS服务器的登录URL进行认证。
In summary, the authentication flow goes:
总之,认证流程是这样的。
- A user attempts to access a secure page, which triggers an authentication exception
- The exception triggers AuthenticationEntryPoint. In response, the AuthenticationEntryPoint will take the user to the CAS server login page – https://localhost:8443/login
- On successful authentication, the server redirects back to the client with a ticket
- CasAuthenticationFilter will pick up the redirect and call CasAuthenticationProvider
- CasAuthenticationProvider will use TicketValidator to confirm the presented ticket on CAS server
- If the ticket is valid, the user will get a redirection to the requested secure URL
Finally, let’s configure HttpSecurity to secure some routes in WebSecurityConfig. In the process, we’ll also add the authentication entry point for exception handling:
最后,让我们配置HttpSecurity,以确保WebSecurityConfig中一些路由的安全。在这个过程中,我们还将添加异常处理的认证入口点。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers( "/secured", "/login")
.authenticated()
.and().exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
}
6. CAS Client Single Logout Configuration
6.CAS客户端单次注销配置
So far, we’ve dealt with single sign-on; let’s now consider CAS single logout (SLO).
到目前为止,我们已经处理了单点登录;现在让我们考虑CAS单点注销(SLO)。
Applications that use CAS for managing user authentication can log out a user from two places:
使用CAS来管理用户认证的应用程序可以从两个地方注销用户。
- The client application can logout a user from itself locally – this will not affect the user’s login status in other applications using the same CAS server
- The client application can also log out the user from the CAS server – this will cause the user to be logged out from all other client apps connected to the same CAS server.
We’ll first put in place logout on the client application and then extend it to single logout on the CAS server.
我们将首先在客户端应用程序上实施注销,然后将其扩展到CAS服务器上的单一注销。
In order to make obvious what goes on behind the scene, we’ll create a logout() method to handle the local logout. On success, it’ll redirect us to a page with a link for single logout:
为了使幕后发生的事情一目了然,我们将创建一个logout()方法来处理本地注销。一旦成功,它将把我们重定向到一个带有单次注销链接的页面。
@GetMapping("/logout")
public String logout(
HttpServletRequest request,
HttpServletResponse response,
SecurityContextLogoutHandler logoutHandler) {
Authentication auth = SecurityContextHolder
.getContext().getAuthentication();
logoutHandler.logout(request, response, auth );
new CookieClearingLogoutHandler(
AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
.logout(request, response, auth);
return "auth/logout";
}
In the single logout process, the CAS server will first expire the user’s ticket and then send an async request to all registered client apps. Each client app that receives this signal will perform a local logout. Thereby accomplishing the goal of logout once, it will cause a log out everywhere.
在单次注销过程中,CAS服务器将首先过期用户的票据,然后向所有注册的客户端应用程序发送一个异步请求。每个收到这个信号的客户端应用程序将执行一个本地注销。从而完成一次注销的目标,它将引起各地的注销。
Having said that, let’s add some bean configurations to our client app. Specifically, in the CasSecuredApplicaiton:
说到这里,让我们给我们的客户端应用程序添加一些Bean配置。具体来说,在CasSecuredApplicaiton中。
@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
return new SecurityContextLogoutHandler();
}
@Bean
public LogoutFilter logoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter("https://localhost:8443/logout",
securityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl("/logout/cas");
return logoutFilter;
}
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix("https://localhost:8443");
singleSignOutFilter.setLogoutCallbackPath("/exit/cas");
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
The logoutFilter will intercept requests to /logout/cas and redirect the application to the CAS server. The SingleSignOutFilter will intercept requests coming from the CAS server and perform the local logout.
logoutFilter将拦截对/logout/cas的请求并将应用程序重定向到CAS服务器。SingleSignOutFilter将拦截来自CAS服务器的请求并执行本地注销。
7. Connecting the CAS Server to a Database
7.将CAS服务器连接到数据库
We can configure the CAS server to read credentials from a MySQL database. We’ll use the test database of a MySQL server that’s running in a local machine. Let’s update cas-server/src/main/resources/etc/cas/config/cas.properties:
我们可以配置CAS服务器从MySQL数据库中读取凭证。我们将使用运行在本地机器上的MySQL服务器的test数据库。让我们更新cas-server/src/main/resources/etc/cas/config/cas.properties。
cas.authn.accept.users=
cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ?
cas.authn.jdbc.query[0].url=
jdbc:mysql://127.0.0.1:3306/test?
useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].ddlAuto=none
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].passwordEncoder.type=NONE
We set the cas.authn.accept.users to blank. This will deactivate the use of static user repositories by the CAS server.
我们将cas.authn.accept.users设为空白。这将停用CAS服务器对静态用户存储库的使用。
According to the SQL above, users’ credentials are stored in the users table. The email column is what represents the users’ principal (username).
根据上面的SQL,用户的证书被存储在users表中。email列是代表用户的主体(username)。
Please make sure to check the list of supported databases, available drivers and dialects. We also set the password encoder type to NONE. Other encryption mechanisms and their peculiar properties are also available.
请确保检查支持的数据库、可用的驱动程序和方言列表。我们还将密码编码器类型设置为NONE。其他加密机制及其奇特的属性也是可用的。
Note that the principal in the database of the CAS server must be the same as that of the client application.
请注意,CAS服务器数据库中的委托人必须与客户应用程序的委托人相同。
Let’s update CasAuthenticationProvider to have the same username as the CAS server:
让我们更新CasAuthenticationProvider,使其与CAS服务器的用户名相同。
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidator());
provider.setUserDetailsService(
s -> new User("test@test.com", "Mellon", true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
return provider;
}
CasAuthenticationProvider does not use the password for authentication. Nonetheless, its username has to match that of the CAS server for authentication to be successful. CAS server requires a MySQL server to be running on localhost at port 3306. The username and password should be root.
CasAuthenticationProvider不使用密码进行认证。尽管如此,它的用户名必须与CAS服务器的用户名相匹配,才能认证成功。CAS服务器要求MySQL服务器在localhost上运行,端口3306。用户名和密码应该是root。
Restart the CAS server and the Spring Boot app once again. Then use the new credentials for authentication.
再次重启CAS服务器和Spring Boot应用。然后使用新的凭证进行认证。
8. Conclusion
8.结论
We have looked at how to use CAS SSO with Spring Security and many of the configuration files involved. There are many other aspects of CAS SSO that is configurable. Ranging from themes and protocol types to authentication policies.
我们已经研究了如何使用CAS SSO与Spring Security以及许多涉及的配置文件。CAS SSO还有许多其他方面是可以配置的。从主题和协议类型到认证策略,不一而足。
These and others are in the docs. The source code for the CAS server and the Spring Boot app is available over on GitHub.
这些内容和其他内容都在docs中。CAS服务器和Spring Boot应用程序的源代码可以在GitHub上找到。