【spring boot 系列】spring security 实践 + 源码分析

11

前言

本文将从示例、原理、应用3个方面介绍 spring security。

以下分析基于spring boot 2.0 + spring 5.0.4版本源码

示例源码:请戳这里

概述

Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。当前版本为 5.0.5。

Spring Security 5 相比 4,主要有以下几点升级:

  • 支持 OAuth 2.0
  • 支持 Spring WebFlux
  • 可以使用 Reactor 的 StepVerifier 进行测试

示例

pom配置

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

配置非常简单,和 spring security 有关的就是 spring-boot-starter-security,web 和 thymeleaf 的引入是为了构建页面,便于演示

application.properties 配置

spring.thymeleaf.cache=false
spring.security.user.name=user
spring.security.user.password=password
spring.security.user.roles=USER

同样很简单,禁用thymeleaf的缓存功能,另外配置了一个角色为 USER 的用户,用户名/密码:user/password

security config 配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.authorizeRequests()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .anyRequest().fullyAuthenticated()
                .and()
                .formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
                .and()
                .logout().permitAll();
        // @formatter:on
    }
}

security 的配置很简单,可以继承WebSecurityConfigurerAdapterWebSecurityConfigurerAdapter是默认情况下 spring security 的 http 配置。通常情况下,都会存在部分 url 请求不需要过安全验证,此时可以通过configure()方法将不需要进行权限校验的 url 排除掉。上面的例子,指定了 静态资源、login 链接不需要过安全验证,其余 url 均需要

至此,整个 security 最简单的功能就已经实现了,是不是非常简单。下面我们用一个例子来试验下。定义一个 HomeController

@Controller
public class HomeController implements WebMvcConfigurer {

    @GetMapping("/")
    public String home(Map<String, Object> model) {
        model.put("message", "Hello World");
        model.put("title", "Hello Home");
        model.put("date", new Date());
        return "home";
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }
}

Spring 的 WebMvcConfigurer 接口提供了很多方法让我们来定制 SpringMVC 的配置,这里通过 addViewControllers 将 /login 请求映射到了资源 login.html

附上 WebMvcConfigurer 提供的配置方法
图片描述

好了,启动 web 应用,可以体验安全验证的效果了。

如何实现多个用户呢

上面最简单的示例,用户权限信息是直接再配置文件中写死的,那么如何实现多个用户呢?多个角色呢?

通过自定义 UserDetailsService 实现,这里列举使用内存存放用户信息的方式。在上述的SecurityConfig中增加配置:

    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager() throws Exception {
        return new InMemoryUserDetailsManager(
                User.withDefaultPasswordEncoder().username("admin").password("admin")
                        .roles("ADMIN", "USER", "ACTUATOR").build(),
                User.withDefaultPasswordEncoder().username("user").password("user")
                        .roles("USER").build());
    }

上述配置添加了2个用户,admin 和 user

如何实现方法级别的权限控制呢?

答案是也很方便,只要加上一个注解配置即可。在SecurityConfig类上增加如下配置

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)

开启注解配置的方式,开启方法执行前后的安全校验

写个简单的 service 做测试:

@Service
public class SimpleSecureService {
    
    @Secured("ROLE_USER")
    public String secure() {
        return "Hello User Security";
    }

    @PreAuthorize("hasRole('ADMIN')")
    public String authorized() {
        return "Hello Admin Security";
    }
}

通过配置,即实现了方法级别的安全校验,@Secured 和 @PreAuthorize 最大区别是后者支持 spring EL,前者不支持,故后者比前者功能更强大

如何实现权限集成呢?

像上面的例子 admin 只能访问 admin 授权的接口,而不能访问 user 的接口,而我们的业务场景往往是 admin 拥有最高权限,可访问其他所有用户的资源,故这里涉及到一个权限继承的问题(当然你可以在所有方法上都标记 admin 可访问)。
spring 提供了 RoleHierarchy 接口来实现权限的级联。
假设需要的级联关系是

A > B
B > C
C > D
D > E
D > F

那么对应的一级map配置

A --> [B]
B --> [C]
C --> [D]
D --> [E,F]

构造完之后的关系

A --> [B,C,D,E,F]
B --> [C,D,E,F]
C --> [D,E,F]
D --> [E,F]

原理介绍

核心组件

SecurityContextHolder

SecurityContextHolder 用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限,这些都被保存在 SecurityContextHolder 中。SecurityContextHolder 默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security 在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。

如何获取当前用户的信息?
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

getAuthentication()返回了认证信息,getPrincipal()返回了身份信息,UserDetails 便是 Spring 对身份信息封装的一个接口。

Authentication

先看下接口定义

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

Authentication 是 spring security 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。可以见得,Authentication 在 spring security 中是最高级别的身份/认证的抽象。

由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。接口详细解读如下:

  • getAuthorities(),权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系列字符串。
  • getCredentials(),密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
  • getDetails(),细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
  • getPrincipal(),最重要的身份信息,大部分情况下返回的是 UserDetails 接口的实现类,也是框架中的常用接口之一。

AuthenticationManager

初次接触 Spring Securit y的朋友相信会被 AuthenticationManager,ProviderManager ,AuthenticationProvider,这么多相似的 Spring 认证类搞得晕头转向,但只要稍微梳理一下就可以理解清