目录
前言
Spring Security 的配置核心就是配置一系列的 SecurityFilterChain,之前的文章已经简单讨论过 Spring Boot 的自动装配机制,给引入 Spring Security 依赖后,提供了简单的默认配置。
本文简述如何自定义配置。
WebSecurityConfigurerAdapter
在 Spring Security 老版本(5.x)之前,使用 WebSecurityConfigurerAdapter 来配置(需要一个配置类继承该类),但该类在新版本已经废弃,新版本使用配置类和 @Bean 来进行自定义配置。
参考官方博客
WebSecurity 和 HttpSecurity
废弃掉了 WebSecurityConfigurerAdapter 后,官方推荐的配置类似:
@Configuration
public class SecurityConfiguration {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
}
简单来讲:
WebSecurity:控制整个 Spring Security 的全局行为。HttpSecurity:控制某一条 Security Filter Chain 如何处理 HTTP 请求。WebSecurity负责“是否进入安全体系”,HttpSecurity负责“进入之后如何进行安全处理”。
如果在 WebSecurity 配置 ignoring,那么符合忽略规则的请求,会直接跳过 Spring Security 过滤器体系,直接进入 Servlet。
官方并不推荐滥用 WebSecurity 的 ignoring 来略过 security,更加推荐配置 HttpSecurity 的 authorizeHttpRequests 来精细控制需要不同权限的访问。
HttpSecurity
很多默认配置都是 Spring Boot 通过 @ConditionalOnMissingBean 提供的,如果我们自己提供了自定的 Bean,那么相对应的默认配置 Bean 就会失效。
我们想要自定义 Spring Security 的行为,首先就是配置 SecurityFilterChain:
@Configuration
public class CustomSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
return http.build();
}
}
我们依然使用了 Spring Boot 提供的默认的 HttpSecurity,但覆盖了 defaultSecurityFilterChain:
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityFilterProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
}
此时我们没有做任何配置,我们可以开启 formLogin:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http.formLogin(Customizer.withDefaults());
return http.build();
}
Customizer.withDefaults() 可以理解成:我启用这个功能,但不做任何额外配置,全部采用 Spring Security 的默认配置。
Customizer 是个函数式接口:
@FunctionalInterface
public interface Customizer<T> {
/**
* Performs the customizations on the input argument.
* @param t the input argument
*/
void customize(T t);
/**
* Returns a {@link Customizer} that does not alter the input argument.
* @return a {@link Customizer} that does not alter the input argument.
*/
static <T> Customizer<T> withDefaults() {
return (t) -> {
};
}
}
之所以这么写,是因为 HttpSecurity 的 formLogin 函数如下,接受的是 Customizer:
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) {
formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
return HttpSecurity.this;
}
如果什么都不配置,就是传递一个空的 lambda 函数:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http.formLogin(form -> {});
return http.build();
}
Customizer.withDefaults() 就是 form -> {},只不过都使用前者的话,看起来更整齐统一。
那么如何知道对应的默认行为?答案在对应的 Configurer 中,比如 formLogin 就去 FormLoginConfigurer 以及它继承的父类 AbstractAuthenticationFilterConfigurer 中找:
HttpSecurity
↓
formLogin()
↓
FormLoginConfigurer
↓
AbstractAuthenticationFilterConfigurer
↓
init()
↓
configure()
所以想要知道可以配置哪些内容,查看对应的 Configurer,比如 formLogin 对应的 FormLoginConfigurer。
此时,我们仅仅配置了 formLogin,并没有显示启用任何保护,所以访问接口不会跳转到登陆页面。
但是相关的登录页都已准备好,请求 http://localhost:8080/login 可以看到 Spring Security 提供的默认登录页。
securityMatcher
HttpSecurity 的 securityMatcher 是用来决定请求是否会进入到当前配置的 SecurityFilterChain,如果不配置,这条 SecurityFilterChain 默认匹配所有请求,相当于:
http.securityMatcher("/**")
大部分简单的应用只需要配置一条 SecurityFilterChain,所以不需要配置 securityMatcher,但是如果是复杂的应用,需要 FilterChainProxy 使用不同规则匹配应用不用的 SecurityFilterChain 的时候,就需要配置 securityMatcher 了。
例如,对于 /API/ 和除了 /API/ 之外,需要应用两个不同的 SecurityFilterChain,可以这么写:
@Configuration
public class SecurityConfig {
@Bean
@Order(1)
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 只处理 /api/** 的请求
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
// 未写 securityMatcher:可匹配其余任意请求
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/css/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
系统仅会调用第一个匹配的 SecurityFilterChain,比如 /API/message 就会匹配到 apiSecurity 这个 SecurityFilterChain,而 /other 无法匹配 /API/** 的规则,FilterChainProxy 会按照 @Order 的顺序继续往下找,这里就匹配到了 webSecurity。
关于多个 SecurityFilterChain 的解释可以参考文档。
最佳时间是,如果有多个 SecurityFilterChain,那么 @Order 的顺序应该是从小范围的具体匹配到最后的最大范围的兜底。
因为 @Order 越小优先级越高,匹配到了第一个,就不会执行剩余的,所以越具体的匹配要放到最前面,最终一定要有一个兜底的匹配(就是不配置 securityMatcher),来匹配除了前面所有条件之外的剩余的请求:
/api/**
↓
/admin/**
↓
/actuator/**
↓
最后要有个兜底
/**
@Bean
@Order(100) // 放在最后,兜底用
SecurityFilterChain defaultChain(HttpSecurity http) {
http
.authorizeHttpRequests(auth ->
auth.anyRequest().authenticated());
return http.build();
}
一旦使用了 securityMatcher,就一定要划分清楚每个 SecurityFilterChain 的匹配范围。
authorizeHttpRequests
如果说 securityMatcher 是决定一个请求是否进入当前 SecurityFilterChain 的,那么 authorizeHttpRequests 就是决定进入了当前 Chain 以后,这个请求是否允许访问。
一个最简单的例子,我们在上面仅仅配置了 formLogin 的基础上,再配置 authorizeHttpRequests:
@Configuration
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
http.formLogin(Customizer.withDefaults());
return http.build();
}
}
重启应用,访问任意接口,发现会跳转到登录页,因为这一行的配置意思是:所有请求必须登录。
我们可以通过一些方式来配置哪些 URL 需要哪些权限才可以访问或者拒绝访问,HttpSecurity 的 authorizeHttpRequests 如下:
public HttpSecurity authorizeHttpRequests(
Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer) {
ApplicationContext context = getContext();
authorizeHttpRequestsCustomizer
.customize(getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry());
return HttpSecurity.this;
}
AuthorizationManagerRequestMatcherRegistry 继承自 AbstractRequestMatcherRegistry,AbstractRequestMatcherRegistry 中有个方法:
public C requestMatchers(String... patterns) {
return requestMatchers(null, patterns);
}
还有其他几个重载方法:
requestMatchers(String... patterns) // 只按路径匹配
requestMatchers(HttpMethod method, String... patterns) // HTTP 方法 + 路径
requestMatchers(HttpMethod method) // 只按 HTTP 方法,不限制路径
requestMatchers(RequestMatcher... requestMatchers) // 你自己提供匹配器,规则可任意复杂
本是就是一个匹配器,匹配到的请求进行下一步判定——AuthorizeHttpRequestsConfigurer 中的各种权限规则方法:
permitAll():无条件允许,不要求认证denyAll():无条件拒绝hasRole("ADMIN"):必须拥有ROLE_ADMINhasAnyRole("ADMIN", "USER"):拥有ROLE_ADMIN或ROLE_USER中任意一个hasAllRoles("ADMIN", "USER"):必须同时拥有ROLE_ADMIN和ROLE_USERhasAuthority("article:read"):必须拥有精确权限 article:readhasAnyAuthority("read", "write"):拥有 read 或 write 中任意一个hasAllAuthorities("read", "write"):必须同时拥有 read 和 write- authenticated():只要已认证即可,包括 remember-me 认证
fullyAuthenticated():必须完整认证,不接受 remember-me 认证rememberMe():只允许通过 remember-me 认证的用户- anonymous():只允许匿名用户,已登录用户不符合
- access():支持自定义的
AuthorizationManager
一个简单的示例如下:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http.authorizeHttpRequests(
auth ->
auth
.requestMatchers("/open/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated());
http.formLogin(Customizer.withDefaults());
return http.build();
}
写法就是指定一个匹配器再指定匹配的权限判定规则。
上面的示例所作出的权限限制:
- 匹配到 /open/** 的请求,全部放行,无需登录。
- 匹配到 /admin/** 的请求,需要登陆且用户需要具有
ROLE_ADMIN角色,才能访问。 - 匹配到 /user/** 的请求,需要登录且用户需要具有
ROLE_ADMIN或者ROLE_USER角色之一,才能访问。 - 除以上匹配条件之外的请求,需要登录才能访问
用户管理
为了测试刚刚的 HttpSecurity 配置,这里简单提下 Spring Security 的用户管理部分。
Spring Boot 自动配置用户的配置类是:UserDetailsServiceAutoConfiguration:
@Bean
InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(User.withUsername(user.getName())
.password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles))
.build());
}
默认使用的 UserDetailsManager 是 InMemoryUserDetailsManager 即将用户存储在内存中,可以在 SecurityProperties 中看到默认的用户名是 user,密码是一个随机生成的 UUID 值:
public static class User {
/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
/**
* Granted roles for the default user name.
*/
private List<String> roles = new ArrayList<>();
}
我们可以通过自己提供 UserDetailsService 来覆盖 Spring 的默认配置,出于演示的目的,为了不引入额外的数据库依赖,依然使用 InMemoryUserDetailsManager,往里面加入三个自定义的用户:
@Configuration
public class CustomUserConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
UserDetails alice = User.withUsername("Alice").password(passwordEncoder.encode("123")).roles("ADMIN").build();
UserDetails bob = User.withUsername("Bob").password(passwordEncoder.encode("123")).roles("USER").build();
UserDetails cindy = User.withUsername("Cindy").password(passwordEncoder.encode("123")).build();
manager.createUser(alice);
manager.createUser(bob);
manager.createUser(cindy);
return manager;
}
}
三个用户主要区别在于角色,Alice 具有 ROLE_ADMIN 角色,Bob 具有 ROLE_USER 角色,Cindy 没有任何角色。
对应的测试 controller:
@RestController
public class HomeController {
@GetMapping("/")
public String home(Authentication authentication) {
return "Hello World: " + authentication.getName() + "\n";
}
@GetMapping("/open")
public String open(Authentication authentication) {
String username = Objects.nonNull(authentication) ? authentication.getName() : "Anonymous";
return "Open API: " + username + "\n";
}
@GetMapping("/admin")
public String admin(Authentication authentication) {
return "Hello Admin: " + authentication.getName() + "\n";
}
@GetMapping("/user")
public String user(Authentication authentication) {
return "Hello User: " + authentication.getName() + "\n";
}
}
- / 匹配到的规则是
.anyRequest().authenticated()),只有登陆的用户才可以访问 / - /open 匹配到的规则是
.requestMatchers("/open/**").permitAll(),登陆或者不登陆都可以访问 /open - /admin 匹配到的规则是
.requestMatchers("/admin/**").hasRole("ADMIN"),只有拥有角色ROLE_ADMIN的用户能访问 /admin - /user 匹配到的规则是
.requestMatchers("/user/**").hasAnyRole("ADMIN", "USER")拥有角色ROLE_ADMIN或者ROLE_USER之一的角色能访问 /user
最终呈现的结果是
- Alice 能访问所有接口
- Bob 能访问 /, /open, /user 三个接口
- Cindy 能访问 /, /open 两个接口
- 未登录用户仅能访问 /open 一个接口