SpringSecurity系列之SpringBoot+CAS单点登录

某一个 Client 需要接入 CAS Server 进行验证,则该 Client 必须提前在 CAS Server 上配置其信息。这个信息既可以动态添加,也可以通过 JSON 来配置,后面松哥会教搭建如何动态添加,这里方便起见,我们还是通过 JSON 来进行配置。
在这里插入图片描述

1.准备工作

准备工作主要做两件事。

1.1 服务记录

某一个 Client 需要接入 CAS Server 进行验证,则该 Client 必须提前在 CAS Server 上配置其信息。

这个信息既可以动态添加,也可以通过 JSON 来配置,后面松哥会教搭建如何动态添加,这里方便起见,我们还是通过 JSON 来进行配置。

具体配置方式如下,在 CAS Server 中创建如下目录:

src/main/resources/services 

在该目录下创建一个名为 client1-99.json 的文件,client1 表示要接入的 client 的名字,99 表示要接入的 client 的 id,json 文件内容如下(这个配置可以参考官方给出的模版:overlays/org.apereo.cas.cas-server-webapp-tomcat-5.3.14/WEB-INF/classes/services/Apereo-10000002.json):

{
    
     
  "@class": "org.apereo.cas.services.RegexRegisteredService", 
  "serviceId": "^(https|http)://.*", 
  "name": "client1", 
  "id": 99, 
  "description": "应用1 的定义信息", 
  "evaluationOrder": 1 
} 

这段 JSON 配置含义如下:

  1. @calss 指定注册服务类,这个是固定的org.apereo.cas.services.RegexRegisteredService。
  2. serviceId 则通过正则表达式用来匹配具体的请求。
  3. name 是接入的 client 的名称。
  4. id 是接入的 client 的 id。
  5. description 是接入的 client 的描述信息。
  6. evaluationOrder 则指定了执行的优先级。

接下来再在 src/main/resources/application.properties 文件中配置刚刚 json 的信息,如下:

cas.serviceRegistry.json.location=classpath:/services 
cas.serviceRegistry.initFromJson=true 

这里有两行配置:

  1. 指定配置 JSON 文件的位置。
  2. 开启 JSON 识别。

OK,配置完成后,重启 CAS Server。

CAS Server 启动成功后,我们在控制台看到如下日志,表示 JSON 配置已经加载成功了:

在这里插入图片描述

1.2 JDK 证书

第二个要提前准备的东西就是 JDK 证书。

在实际开发中,这一步可以忽略,但是因为我们现在用的自己生成的 SSL 证书,所以我们要将自己生成的证书导入到 JDK 中,否则在使用 Spring Security 接入 CAS 单点登录时,会抛出如下错误:

在这里插入图片描述
将 SSL 证书导入 JDK 中的命令其实也很简单,两个步骤,第一个导出 .cer 文件,第二步,导入 JDK,命令如下:

keytool -export -trustcacerts -alias casserver -file ./cas.cer -keystore ./keystore 
sudo keytool -import -trustcacerts -alias casserver -file ./cas.cer -keystore /Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home/lib/security/cacerts 

注意,在执行 export 导出命令时,需要输入密钥口令,这个口令就是自己一开始创建 SSL 证书时设置的。在执行 import 导入命令时,也需要输入口令,这个口令是 changeit,注意,不是自己一开始设置的。

密钥库的位置在 JDK 目录下的 /lib/security/cacerts,小伙伴们根据自己实际情况来修改(在 JDK9 之前,位置在 jre/lib/security/cacerts)。

我们在本地测试一定要导入证书到 JDK 证书库中,否则后面的测试会出现上图中的错误,证书导入 JDK 证书库之后,要确保之后的开发中,使用的是本地的 JDK。

注意,JDK 证书导入之后,CASServer 需要重启一下。

1.3 修改 hosts

另外,我们还需要修改电脑 hosts 文件,因为前面关于 CAS Server,关于 SSL 证书的配置都涉及到域名,所以后面的访问我们将通过域名的形式访问,hosts 文件中添加如下两条记录:

在这里插入图片描述
第一个是 CAS Server 的请求域名,第二个是 CAS Client 的请求域名。

2.开发 Client

在使用 Spring Security 开发 CAS Client 之前,有一个基本问题需要先和小伙伴们捋清楚:用户登录是在 CAS Server 上登录,所以 Spring Security 中虽然依旧存在用户的概念,但是对于用户的处理逻辑会和前面的有所不同。

好了,接下来我们来看下具体步骤。

首先我们来创建一个普通的 Spring Boot 项目,加入 Web 依赖 和 Spring Security 依赖,如下:

在这里插入图片描述
项目创建成功后,我们再来手动加入 cas 依赖:

<dependency> 
    <groupId>org.springframework.security</groupId> 
    <artifactId>spring-security-cas</artifactId> 
</dependency> 

接下来,在 application.properties 中配置 CAS Server 和 CAS Client 的请求地址信息:

cas.server.prefix=https://cas.javaboy.org:8443/cas 
cas.server.login=${cas.server.prefix}/login 
cas.server.logout=${cas.server.prefix}/logout 
 
cas.client.prefix=http://client1.cas.javaboy.org:8080 
cas.client.login=${cas.client.prefix}/login/cas 
cas.client.logoutRelative=/logout/cas 
cas.client.logout=${cas.client.prefix}${cas.client.logoutRelative} 

这些配置都是自定义配置,所以配置的 key 可以自己随意定义。至于配置的含义都好理解,分别配置了 CAS Server 和 CAS Client 的登录和注销地址。

配置好之后,我们需要将这些配置注入到实体类中使用,这里就用到了类型安全的属性绑定。

这里我创建两个类分别用来接收 CAS Server 和 CAS Client 的配置文件:

@ConfigurationProperties(prefix = "cas.server") 
public class CASServerProperties {
    
     
    private String prefix; 
    private String login; 
    private String logout; 
    //省略 getter/setter 
} 
@ConfigurationProperties(prefix = "cas.client") 
public class CASClientProperties {
    
     
    private String prefix; 
    private String login; 
    private String logoutRelative; 
    private String logout; 
    //省略 getter/setter 
} 

另外记得在启动类上面添加 @ConfigurationPropertiesScan 注解来扫描这两个配置类:

@SpringBootApplication 
@ConfigurationPropertiesScan 
public class Client1Application {
    
     
 
    public static void main(String[] args) {
    
     
        SpringApplication.run(Client1Application.class, args); 
    } 
} 

这里配置完成后,我们一会将在配置文件中来使用。

接下来创建 CAS 的配置文件,略长:

@Configuration 
public class CasSecurityConfig {
    
     
    @Autowired 
    CASClientProperties casClientProperties; 
    @Autowired 
    CASServerProperties casServerProperties; 
    @Autowired 
    UserDetailsService userDetailService; 
 
    @Bean 
    ServiceProperties serviceProperties() {
    
     
        ServiceProperties serviceProperties = new ServiceProperties(); 
        serviceProperties.setService(casClientProperties.getLogin()); 
        return serviceProperties; 
    } 
 
    @Bean 
    @Primary 
    AuthenticationEntryPoint authenticationEntryPoint() {
    
     
        CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint(); 
        entryPoint.setLoginUrl(casServerProperties.getLogin()); 
        entryPoint.setServiceProperties(serviceProperties()); 
        return entryPoint; 
    } 
 
    @Bean 
    TicketValidator ticketValidator() {
    
     
        return new Cas20ProxyTicketValidator(casServerProperties.getPrefix()); 
    } 
 
    @Bean 
    CasAuthenticationProvider casAuthenticationProvider() {
    
     
        CasAuthenticationProvider provider = new CasAuthenticationProvider(); 
        provider.setServiceProperties(serviceProperties()); 
        provider.setTicketValidator(ticketValidator()); 
        provider.setUserDetailsService(userDetailService); 
        provider.setKey("javaboy"); 
        return provider; 
    } 
 
    @Bean 
    CasAuthenticationFilter casAuthenticationFilter(AuthenticationProvider authenticationProvider) {
    
     
        CasAuthenticationFilter filter = new CasAuthenticationFilter(); 
        filter.setServiceProperties(serviceProperties()); 
        filter.setAuthenticationManager(new ProviderManager(authenticationProvider)); 
        return filter; 
    } 
 
    @Bean 
    SingleSignOutFilter singleSignOutFilter() {
    
     
        SingleSignOutFilter sign = new SingleSignOutFilter(); 
        sign.setIgnoreInitConfiguration(true); 
        return sign; 
    } 
    @Bean 
    LogoutFilter logoutFilter() {
    
     
        LogoutFilter filter = new LogoutFilter(casServerProperties.getLogout(), new SecurityContextLogoutHandler()); 
        filter.setFilterProcessesUrl(casClientProperties.getLogoutRelative()); 
        return filter; 
    } 
} 

这个配置文件略长,但是并不难,我来和大家挨个解释:

  1. 首先一进来注入三个对象,这三个中,有两个是我们前面写的配置类的实例,另外一个则是 UserDetailsService,关于 UserDetailsService,我想我也不必多做解释,大家参考本系列前面的文章就知道 UserDetailsService 的作用,一会我会给出 UserDetailsService 的实现。
  2. 接下来配置 ServiceProperties,ServiceProperties 中主要配置一下 Client 的登录地址即可,这个地址就是在 CAS Server 上登录成功后,重定向的地址。
  3. CasAuthenticationEntryPoint 则是 CAS 验证的入口,这里首先设置 CAS Server 的登录地址,同时将前面的 ServiceProperties 设置进去,这样当它登录成功后,就知道往哪里跳转了。
  4. TicketValidator 这是配置 ticket 校验地址,CAS Client 拿到 ticket 要去 CAS Server 上校验,默认校验地址是:https://cas.javaboy.org:8443/cas/proxyValidate?ticket=xxx
  5. CasAuthenticationProvider 主要用来处理 CAS 验证逻辑,关于 AuthenticationProvider 松哥在前面的文章中和大家分享过(SpringSecurity 自定义认证逻辑的两种方式(高级玩法)),当时就说,想要自定义认证逻辑,如短信登录等,都可以通过扩展 AuthenticationProvider 来实现,这里的 CAS 登录当然也不例外,这里虽然设置了一个 userDetailService,但是目的不是为了从数据库中查询数据做校验,因为登录是在 CAS Server 中进行的,这个的作用,我在后面会做介绍。
  6. CasAuthenticationFilter 则是 CAS 认证的过滤器,过滤器将请求拦截下来之后,交由 CasAuthenticationProvider 来做具体处理。
  7. SingleSignOutFilter 表示接受 CAS Server 发出的注销请求,所有的注销请求都将从 CAS Client 转发到 CAS Server,CAS Server 处理完后,会通知所有的 CAS Client 注销登录。
  8. LogoutFilter 则是配置将注销请求转发到 CAS Server。

接下来我再来给大家看下我定义的 UserDetailsService:

@Component 
@Primary 
public class UserDetailsServiceImpl implements UserDetailsService{
    
     
 
    @Override 
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    
     
        return new User(s, "123", true, true, true, true, 
                AuthorityUtils.createAuthorityList("ROLE_user")); 
    } 
} 

既然是单点登录,也就是用户是在 CAS Server 上登录的,这里的 UserDetailsService 意义在哪里呢?

用户虽然在 CAS Server 上登录,但是,登录成功之后,CAS Client 还是要获取用户的基本信息、角色等,以便做进一步的权限控制,所以,这里的 loadUserByUsername 方法中的参数,实际上就是你从 CAS Server 上登录成功后获取到的用户名,拿着这个用户名,去数据库中查询用户的相关信心并返回,方便 CAS Client 在后续的鉴权中做进一步的使用,这里我为了方便,就没有去数据库中查询了,而是直接创建了一个 User 对象返回。

接下来,我们再来看看 Spring Security 的配置:

@Configuration 
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
     
    @Autowired 
    AuthenticationEntryPoint authenticationEntryPoint; 
    @Autowired 
    AuthenticationProvider authenticationProvider; 
    @Autowired 
    SingleSignOutFilter singleSignOutFilter; 
    @Autowired 
    LogoutFilter logoutFilter; 
    @Autowired 
    CasAuthenticationFilter casAuthenticationFilter; 
 
    @Override 
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
     
        auth.authenticationProvider(authenticationProvider); 
    } 
 
    @Override 
    protected void configure(HttpSecurity http) throws Exception {
    
     
        http.authorizeRequests().antMatchers("/user/**") 
                .hasRole("user") 
                .antMatchers("/login/cas").permitAll() 
                .anyRequest().authenticated() 
                .and() 
                .exceptionHandling() 
                .authenticationEntryPoint(authenticationEntryPoint) 
                .and() 
                .addFilter(casAuthenticationFilter) 
                .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class) 
                .addFilterBefore(logoutFilter, LogoutFilter.class); 
    } 
} 

这里的配置就简单很多了:

  1. 首先配置 authenticationProvider,这个 authenticationProvider 实际上就是一开始配置的 CasAuthenticationProvider。
  2. 接下来配置 /user/** 格式的路径需要有 user 角色才能访问,登录路径 /login/cas 可以直接访问,剩余接口都是登录成功之后才能访问。
  3. 最后把 authenticationEntryPoint 配置进来,再把自定义的过滤器加进来,这些都比较容易我就不多说了。

最后,再提供两个测试接口:

@RestController 
public class HelloController {
    
     
    @GetMapping("/hello") 
    public String hello() {
    
     
        return "hello"; 
    } 
    @GetMapping("/user/hello") 
    public String user() {
    
     
        return "user"; 
    } 
} 

OK ,如此之后,我们的 CAS Client 现在就开发完成了,接下来启动 CAS Client,启动成功后,浏览器输入 http://client1.cas.javaboy.org:8080/user/hello 访问 hello 接口,此时会自动跳转到 CAS Server 上登录,登录成功之后,经过两个重定向,会重新回到 hello 接口。

3.小结

OK,这就是和大家介绍的 Spring Security + CAS 单点登录

猜你喜欢

转载自blog.csdn.net/Monsterof/article/details/120847446