我是用官方文档学习的
孩子还是大一的小凳,对很多底层理解的不深,博客主要是记录学习日志,可能讲得不够好,望海涵
概述
Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。
Servlet的应用使用SpringSecurity
入门
在maven中使用SpringSecurity首先肯定是要引入依赖
<dependencies>
<!-- ... 其他依赖元素 ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
由于Spring Boot提供了一个Maven BOM来管理依赖版本,所以你不需要指定一个版本。
如果你想覆盖Spring Security的版本,你可以通过提供一个Maven属性来实现
<properties>
<!-- ... -->
<spring-security.version>6.2.0-SNAPSHOT</spring-security.version>
</properties>
如果使用较新的SS的版本,可能springboot也得改成新版本
<properties>
<!-- ... -->
<spring.version>6.1.0-M2</spring.version>
</properties>
当引入好依赖后,启动springboot项目
如果日志有出现
$ ./mvnw spring-boot:run
...
INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
...
那么说明SpringSecurity启动成功了
这个时候如果去访问该项目的某个endpoint时,就会401 unauthorized了
如果在浏览器里面访问那就会重定向到一个默认的登录页面,这个登录页面的信息在静态资源那里
就好像
这个是现在在做的项目里面的实际的一种情况
SpringSecurity在启动时默认会启动以下的一些功能
- 任何端点(包括 Boot 的
/error
端点)都需要一个认证的用户。 - 在启动时用生成的密码 注册一个默认用户(密码被记录到控制台;在前面的例子中,密码是
8e557245-73e2-4286-969a-ff57fe326336
)。 - 用 BCrypt 以及其他方式保护密码存储。
- 提供基于表单的 登录 和 注销 流程。
- 对 基于表单 的登录以及 HTTP Basic 进行认证。
- 提供内容协商;对于web请求,重定向到登录页面;对于服务请求,返回
401 Unauthorized
。 - 减缓 CSRF 攻击。
- 减缓 Session Fixation 攻击。
- 写入 Strict-Transport-Security,以 确保HTTPS。
- 写入 X-Content-Type-Options 以减缓 嗅探攻击。
- 写入保护认证资源的 Cache Control header。
- 写入 X-Frame-Options,以缓解 点击劫持 的情况。
- 与 HttpServletRequest的认证方法 整合。
- 发布 认证成功和失败的事件。
Springboot在对SpringSecurity的启动时会有很多自动配置
在我们自己没有去配置SpringSecurtiy的配置类的时候,会启动其内部自定义的SS配置
@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {
@Bean
@ConditionalOnMissingBean(UserDetailsService.class)
InMemoryUserDetailsManager inMemoryUserDetailsManager() {
String generatedPassword = // ...;
return new InMemoryUserDetailsManager(User.withUsername("user")
.password(generatedPassword).roles("ROLE_USER").build());
}
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher delegate) {
return new DefaultAuthenticationEventPublisher(delegate);
}
}
实际开发中使用SpringSecurity的代码例子
package com.example.securitydemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults; // for httpBasic and formLogin defaults
@Configuration
@EnableWebSecurity // 显式启用 Web 安全性,尽管在 Spring Boot 中定义 SecurityFilterChain bean 时通常会自动应用
public class WebSecurityConfig {
// 3.1 定义密码编码器 (非常重要!)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 3.2 使用内存中的用户进行认证 (用于演示)
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password")) // 密码必须加密
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("adminpassword"))
.roles("ADMIN", "USER") // admin 拥有 ADMIN 和 USER 角色
.build();
return new InMemoryUserDetailsManager(user, admin);
}
//UserDetails是spring security用来对用户进行身份验证和授权的接口,
//可以在里面定义用户授权验证的相关信息,也可以从数据库里面获取用户信息封装在这里面
//为后续的验证授权功能服务
// 3.3 配置 HTTP 安全规则
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/home", "/css/**", "/js/**", "/images/**", "/public/**").permitAll() // 3.3.1 允许公共访问的路径
.requestMatchers("/admin/**").hasRole("ADMIN") // 3.3.2 只有 ADMIN 角色的用户才能访问 /admin/**
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // USER 或 ADMIN 角色可以访问 /user/**
.anyRequest().authenticated() // 3.3.3 其他所有请求都需要认证
)
.formLogin(formLogin -> formLogin // 3.3.4 配置表单登录
.loginPage("/login") // 3.3.5 指定自定义登录页面的路径 (如果需要)
.permitAll() // 登录页面本身需要允许所有用户访问
.defaultSuccessUrl("/hello", true) // 登录成功后的默认跳转页面
)
.logout(logout -> logout // 3.3.6 配置登出
.logoutUrl("/perform_logout") // 自定义登出URL
.logoutSuccessUrl("/login?logout") // 登出成功后跳转的页面
.permitAll()
)
.httpBasic(withDefaults()); // 启用 HTTP Basic 认证,使用默认配置
return http.build();
}
}
实际开发中肯定是从数据库查询数据来进行认证的
架构
从web请求到servlet之前通常需要经过一系列的filter的处理
而Servlet和Filter是Web容器如Tomcat直接管理的对象,他们不直接感知Spring容器的存在。而他们的生命周期是通过Web容器控制的。
如果希望在Filter里面使用Spring提供的服务,如使用bean,那就需要借助DelegatingFilterProxy来解决了。
当请求符合DelegatingFilterProxy时,它本身不执行过滤逻辑,相反地它会从Spring的WebApplicationContext中寻找DelegatingFilterProxy声明的filter-name对应的Filter Bean,然后执行do Filter操作
FilterChainProxy简单说就是Security所有Filter的链总入口,同时作为一个Bean Filter存在
而SecurityFilterChain是实现SpringSecurity一系列功能的链,FilterChainProxy会把任务委托给一个特定的SecurityFilterChain
进一步的,SecurityFilterChain被FilterChainProxy用来确定当前请求应该调用哪些SecurityFilter实例
如果单单使用DelegatingFilterProxy来调用SecurityFilter,那么可以要有很多的DelegatingFilterProxy来满足需求,所有可以用FilterChainProxy来放多个SecurityFilterChain,以FilterChainProxy为入口,并且通过它来选择用哪个SecurityFilterChain
Security Filter是通过SecurityFilterChain的API来插入到FilterChainProxy中间的
而这些Filter可以有多种不同的功能要求,如认证授权漏洞保护什么的,可以按照特定的顺序执行
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())//CSRFFIlter
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)//UsernamePasswordAUthenticationFilter
.httpBasic(Customizer.withDefaults())//BasicAuthenticationFilter
.formLogin(Customizer.withDefaults());//AuthorizationFilter
return http.build();
}
}
那么如何添加自定义的Filter到FilterChain呢
在官方文档里面说默认的securityFilter足以满足应用程序的安全,但是也可以自定义一个filter去实现如
想添加一个 Filter
,获得一个租户 id header 并检查当前用户是否有访问该租户的权限。
前面的描述已经给了我们一个添加 filter 的线索,因为我们需要知道当前的用户,所以我们需要在认证 filter 之后添加它
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
//也可也从OncePerRequestFilter中继承
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id");
boolean hasAccess = isUserAllowed(tenantId);
if (hasAccess) {
filterChain.doFilter(request, response);
return;
}
throw new AccessDeniedException("Access denied");
}
}
//接下来需要将其添加到securityfilterchain中
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
return http.build();
}
进一步地,由于把filter声明为bean,并且在springsecurity中也有调用,难免会有一些问题,可以通过依赖注入避免重复调用
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
这样就可以避免springboot向容器注册它导致重复调用了
处理Security异常
ExceptionTranslationFilter可以将AccessDeniedException和AuthenticationException翻译衬托Http响应
并且这个FIlter作为SecurityFilter之一插入到FilterChainProxy之中
当进入ExceptionTranslationFilter,会调用FilterChain.doFIlter调用其他部分,看看是Authentication还是Access Denied
如果是前者,就会把SecurityContextHolder清理掉,如何把HttpServletRequest缓存起来,认证成功时使用
AuthenticationEntryPoint用于请求客户的凭证,可以重定向到一个登录页面或者发送一个WWW-Authenticate头
如果时AccessDenied就会调用AccessDeniedHandler拒绝访问
如果没有抛出这俩个错误就不会执行ExceptionTranslationFilter
RequestCache
HttpServletRequest被保存在RequestCache里面,当用户成功认证后从中拿取请求
RequestCacheAwareFilter就是用RequestCache来保存HttpServletRequest的
如何定制RequestCache的实现呢
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}