前言:
SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。CAS是一个开源的企业级SSO具体实现,spring security也提供了和SSO的集成,本文用一个简单示例演示下如何在spring security中利用cas,为了简单起见,本文利用http来访问cas server,关于如何搭建支持http协议的cas server可以参照
[url=http://fengyilin.iteye.com/admin/blogs/2411008]Overlay-Cas server搭建[/url]
环境:
spring boot 版本:1.5.4.RELEASE
1.项目结构
[img]http://dl2.iteye.com/upload/attachment/0128/9200/fec6226a-e703-364b-8ccb-fbf9934b96eb.png[/img]
2.配置类SecurityConfig.java
/**
*
*/
package falcon.chengf.security.samples.javaconfig.cas.client;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.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.User;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* @author: 作者: chengaofeng
* @date: 创建时间:2018-01-05 09:09:21
* @Description: TODO
* @version V1.0
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// cas server默认用户名和密码 casuser::Mellon
manager.createUser(User.withUsername("casuser").password("Mellon").roles("USER").build());
return manager;
}
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
// 路径固定,client端ip:端口/login/cas,是CasAuthenticationFilter默认监听的地址
serviceProperties.setService("http://localhost:8089/login/cas");
serviceProperties.setSendRenew(false);
return serviceProperties;
}
@Bean
public AuthenticationEntryPoint casEntryPoint() {
CasAuthenticationEntryPoint casEntryPoint = new CasAuthenticationEntryPoint();
// cas-server 对应的login地址
casEntryPoint.setLoginUrl("http://localhost:8080/cas-server/cas/login");
casEntryPoint.setServiceProperties(serviceProperties());
return casEntryPoint;
}
@Bean
public CasAuthenticationFilter casFilter() throws Exception {
CasAuthenticationFilter casFilter = new CasAuthenticationFilter();
// casFilter.setFilterProcessesUrl("/**");
casFilter.setAuthenticationManager(authenticationManager());
return casFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().hasRole("USER").and()
.exceptionHandling()
.authenticationEntryPoint(casEntryPoint()).and()
.addFilter(casFilter())
.authenticationProvider(casAuthenticationProvider());
}
@Bean
@Autowired
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(userDetailsByNameServiceWrapper());
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(ticketValidator());
casAuthenticationProvider.setKey("an_id_for_this_auth_provider_only");
return casAuthenticationProvider;
}
@Bean
public Cas20ServiceTicketValidator ticketValidator() {
// cas server 服务的context path
Cas20ServiceTicketValidator ticketValidator = new Cas20ServiceTicketValidator("http://localhost:8080/cas-server");
// ticketValidator.setEncoding("UTF-8");
return ticketValidator;
}
@Bean
public UserDetailsByNameServiceWrapper<CasAssertionAuthenticationToken> userDetailsByNameServiceWrapper() {
UserDetailsByNameServiceWrapper<CasAssertionAuthenticationToken> userDetailsByNameServiceWrapper = new UserDetailsByNameServiceWrapper<CasAssertionAuthenticationToken>(
userDetailsService());
return userDetailsByNameServiceWrapper;
}
}
3.启动类CasClientApp.java
package falcon.chengf.security.samples.javaconfig.cas.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Hello world!
*
*/
@SpringBootApplication
public class CasClientApp
{
public static void main( String[] args )
{
SpringApplication.run(CasClientApp.class, args);
}
}
4.项目配置文件application.yml
server:
port: 8089
[i]因为要启动cas server,用的是8080端口,此处只是将client服务的端口修改成了8089[/i]
5.项目pom文件
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>falcon.chengf</groupId>
<artifactId>security-samples-javaconfig-cas-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>security-samples-javaconfig-cas-client</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
相比单纯的spring security文件,这里只是多追加了spring-security-cas依赖
6.项目默认欢迎页index.html
<!DOCTYPE html>
<html>
<head>
<title>Static</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
hello cas!
</body>
</html>
7.启动项目
选中启动类,选择 Run As->Java application,启动后在浏览器中输入
http://localhost:8089/index.html
[img]http://dl2.iteye.com/upload/attachment/0128/9202/15edf07c-6640-320b-8356-d85fd636cac7.png[/img]
正常情况下,我们会被重定向到cas server的login界面
[img]http://dl2.iteye.com/upload/attachment/0128/9204/0e8b02a9-00fc-3667-9292-59eba796402d.png[/img]
输入用户名:casuser;密码:Mellon点击login
[img]http://dl2.iteye.com/upload/attachment/0128/9206/8e640573-54a9-3dda-b4c5-90cdfc7ac843.png[/img]
认证成功后重定向到我们最初访问的页面
[img]http://dl2.iteye.com/upload/attachment/0128/9208/e6e2239f-6870-3f50-a37b-e3c28d2b1573.png[/img]
8.遇到的问题
8.1 点击login后过一段时间报错[quote]localhost 将您重定向的次数过多。 尝试清除 Cookie. ERR_TOO_MANY_REDIRECTS[/quote]
通过查看浏览器请求记录,程序一直在访问index.html->尝试认证->认证返回->访问index.html 中循环
http://localhost:8080/cas-server/cas/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html 302 GET localhost:8080 /cas-server/cas/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html Thu Feb 01 10:08:25 CST 2018 18 1392 Complete
http://localhost:8080/cas-server/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html 302 GET localhost:8080 /cas-server/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html Thu Feb 01 10:08:25 CST 2018 25 1390 Complete
http://localhost:8089/index.html?ticket=ST-15-TbxurFIR6UZGXCrlmzH5-cas01.example.org 302 GET localhost:8089 /index.html?ticket=ST-15-TbxurFIR6UZGXCrlmzH5-cas01.example.org Thu Feb 01 10:08:25 CST 2018 10 725 Complete
http://localhost:8080/cas-server/cas/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html 302 GET localhost:8080 /cas-server/cas/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html Thu Feb 01 10:08:25 CST 2018 3 1392 Complete
http://localhost:8080/cas-server/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html 302 GET localhost:8080 /cas-server/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html Thu Feb 01 10:08:25 CST 2018 15 1390 Complete
http://localhost:8089/index.html?ticket=ST-16-qXBicEb1u4SX2Nm0W5cg-cas01.example.org 302 GET localhost:8089 /index.html?ticket=ST-16-qXBicEb1u4SX2Nm0W5cg-cas01.example.org Thu Feb 01 10:08:25 CST 2018 7 725 Complete
http://localhost:8080/cas-server/cas/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html 302 GET localhost:8080 /cas-server/cas/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html Thu Feb 01 10:08:25 CST 2018 16 1392 Complete
http://localhost:8080/cas-server/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html 302 GET localhost:8080 /cas-server/login?service=http%3A%2F%2Flocalhost%3A8089%2Findex.html Thu Feb 01 10:08:25 CST 2018 15 1390 Complete
.....
最终发现错误原因是service对应的url和CasAuthenticationFilter对应的监听路径不一致,所以会一直尝试认证,CasAuthenticationFilter默认监听的是/login/cas,一开始不明白设置成了/index.html,所以一直不能认证成功
8.2 点击login后,客户端控制台出现如下错误
[quote]org.jasig.cas.client.util.XmlUtils : 元素类型 "img" 必须由匹配的结束标记 "</img>" 终止[/quote]
原因是 spring security官网上的cas-server是3.4的,ticketValidator路基估计和4.2不一样,在4.2时,validator路径不需要 /cas了,路径前缀直接是
http://casServerIp:casServerPort/casServerContext
在本例子中就是http://localhost:8080/cas-server,而不是http://localhost:8080/cas-server/cas
至此,spring security结合cas的例子就结束了
9.最后再说一点在实际应用中,用户信息存储的问题
因为sso在实际应用中都是会有好多系统共同应用,如果我们能保证所有系统中权限、角色、组织、菜单等的定义保持一致,我们可以把就可以把权限系统对应的用户信息保存在server端(通常情况下这很难做到,因为提供sso的团队一般并不能完全左右使用sso的应用),如果不能保证一致,或是为了扩展需要,我们可在sso server端只是单纯的创建一个user表,其他的权限信息让各个应用自己维护,就如同我们在本例中采用的形式。
[url=https://github.com/fengyilin/spring-security-sample/tree/master]下载源码[/url]