likes
comments
collection
share

工具使用集| SpringSecurity 之 使用它的前置条件

作者站长头像
站长
· 阅读数 31

spring-security 上

前言

之前学习过 Shiro 对它有的大致的了解。没深入看源码,那能算是熟练使用吗?(不是 。这次来体验一下它的兄的,SpringSecurity。之前是有使用它做过项目的,但是都是在对着代码敲,在进补充添加和修改。初次使用确实很吃力。不过,也勉勉强强的做了下去。现在就正式的认识一下吧!你好,SpringSecurity 😍。

使用版本要求

版本要求:

Spring Security 需要 Java 8 或更高版本的运行时环境

优点:

有一说一,Shiro 对于实现权限框架确实很快速方便,要说简单倒也没有很简单吧?所以简单我不认为是使用 Shiro的优点

如 Shiro 一般可以通过配置 ini 文件进行角色或者权限、组件设置

但是 SpringSecurity 也是可以通application.yml 快速配置 角色或者权限的

SpringSecurity 一直说笨重,但是如果只是实现权限功能,那也是很快捷的。

我认为 SpringSecurity 笨重的很大原因就在于,它可扩展性高,高度可定制化也就意味我们需要自己去实现一些内容,就意味着自由,这时候我们可能就会迷茫什么功能是必要,什么功能是比不必要的,实现了这个功能,是不是也可以实现另外的功能。虽然功能强大,但很多时候都不是必要。

Spring Security的高度可扩展性和高度定制化确实是它的优势,它允许我们根据具体业务需求实现自定义的安全策略。然而,这也意味着我们需要花费更多的时间和精力来实现必要的功能和策略,同时也需要避免实现不必要的功能和策略,这可能会导致过度复杂和冗余的代码。因此,在使用Spring Security时,开发人员需要仔细权衡和考虑,以确保实现必要的功能和策略,并避免实现不必要的功能和策略

Spring Security 6.0 的新特性

前置知识

验证

PasswordEncoder接口

PasswordEncoder是一种单向转换,当密码转换需要双向时(例如存储用于向数据库进行身份验证的凭据),它就没有用了。不过还是可以通过比较传递的密码和存储的密码进行比较

⏰INFO

开发人员在通过单向哈希(例如 SHA-256)运行密码后存储密码。当用户尝试进行身份验证时,散列密码将与他们输入的密码的散列值进行比较。这意味着系统只需要存储密码的单向散列。但是会被恶意用户使用恶意用户决定创建称为Rainbow Tables 的查找表,进行攻击。

为了降低 Rainbow Tables 的有效性,鼓励开发人员使用加盐密码。不是只使用密码作为散列函数的输入,而是为每个用户的密码生成随机字节(称为盐)。

鼓励开发人员利用自适应单向函数来存储密码。使用自适应单向函数验证密码是有意占用资源的(它们有意使用大量 CPU、内存或其他资源)。自适应单向函数允许配置一个可以随着硬件变得更好而增长的“工作因素”。我们建议将“工作因素”调整为大约需要一秒钟来验证系统上的密码。bcryptPBKDF2scryptargon2

DelegatingPasswordEncoder

为了保证对旧数据(密码)或者环境的迁移便利。SpringSecurity 引入了DelegatingPasswordEncoder,它通过以下方式解决了所有问题:

  • 确保使用当前密码存储建议对密码进行编码
  • 允许验证现代和传统格式的密码
  • 允许将来升级编码

创建一个 DelegatingPasswordEncoder

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

自定义 DelegatingPasswordEncoder

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("sha256", new StandardPasswordEncoder());
​
PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);

⏰INFO

这个很像 Shiro 中的过滤器

Password Storage Format

Spring Boot Security 中,密码存储格式是指将用户的密码进行加密或哈希等处理后存储在数据库中,而不是以明文形式存储。

密码的一般格式是:

{id}encodedPassword

eg:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
{noop}password 
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 

Password Encoding

其实就是加密算法模式(密码编码是将用户密码转换为不可逆的散列值的过程)。Spring Security 采用密码编码器的方式,将密码存储为散列值。在用户登录时,将用户输入的密码同样采用相同的密码编码器进行转换为散列值,然后与数据库中存储的散列值进行比较,以判断用户输入的密码是否正确。因此,即使攻击者获取了数据库中的散列值,也无法通过比较得知用户的密码

BCrypt密码编码器 --- BACK

为了使其更能抵抗密码破解,该算法会通过迭代哈希来延长加密时间,防止暴力破解

bcrypt 函数的输入是密码字符串(最多 72 字节)、数字成本和 16 字节(128 位)盐值。盐通常是随机值。bcrypt 函数使用这些输入来计算 24 字节(192 位)的散列。

$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
__// ____________________/_____________________________/
算法成本盐散列
  • $2a$:哈希算法标识符(bcrypt)
  • 12:输入成本(2 12即4096发)
  • R9h/cIPz0gi.URNNX3kh2O:输入盐的base-64编码
  • PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW:计算的 24 字节哈希的前 23 个字节的 base-64 编码
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
​
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user")
        .password(passwordEncoder().encode("password"))
        .roles("USER");
}
Argon2密码编码器 --- back

Argon2 是一种需要大量内存的故意缓慢的算法。

  • Argon2d 最大限度地抵抗 GPU 破解攻击。它以密码相关顺序访问内存阵列,这降低了时间-内存权衡(TMTO) 攻击的可能性,但引入了可能[边信道攻击。
  • Argon2i 针对抵御边信道攻击进行了优化。它以与密码无关的顺序访问内存阵列。
  • Argon2id 是一个混合版本。它遵循 Argon2i 方法进行前半段内存传递,随后的传递采用 Argon2d 方法。如果您不知道类型之间的区别或者您认为边信道攻击是一种可行的威胁,RFC 建议使用 Argon2id。

所有三种模式都允许通过三个参数进行规范,这些参数控制:

  • 执行时间处理时间
  • 需要内存
  • 并行度
@Bean
public PasswordEncoder passwordEncoder() {
    return Argon2PasswordEncoder();
    // return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8() 是在 Spring Security 5.8 版本中引入的,因此它只适用于 Spring Security 5.8 及以上版本。在低版本的 Spring Security 中,该方法是不存在的
}
​
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user")
        .password(passwordEncoder().encode("password"))
        .roles("USER");
}
Pbkdf2密码编码器 --- back

其实,加密常用的一种防暴力破解就是延迟加密时间,该算法也有这一特点

PBKDF2 将伪随机函数(例如基于散列的消息身份验证代码(HMAC))与盐)值一起应用于输入密码或密码,并多次重复该过程以生成派生密钥,然后可以将其用作加密密钥在后续操作中。增加的计算工作使密码破解变得更加困难,这被称为key stretching

@Bean
public PasswordEncoder passwordEncoder() {
    return Argon2PasswordEncoder();
        //return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8(); 同上所述,低版本是不存在该方法
}
​
​
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user")
        .password(passwordEncoder().encode("password"))
        .roles("USER");
}
SCrypt密码编码器 --- back

scrypt 是一种时间-内存衡量算法,如果攻击方追求速度,那么花费的内存代码会变高;反之速度就会慢

scrypt(算法专门设计用于通过需要大量内存来执行大规模自定义硬件攻击,从而降低成本)背后的想法是故意使这种权衡在任一方向上都代价高昂。因此,攻击者可以使用不需要很多资源的实现(因此可以以有限的费用进行大规模并行化)但运行速度非常慢,或者使用运行速度更快但内存需求非常大因此成本更高的实现并行化

@Bean
public PasswordEncoder passwordEncoder() {
    return return new SCryptPasswordEncoder();
        //return SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8(); 同上所述,低版本是不存在该方法
}
​
​
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user")
        .password(passwordEncoder().encode("password"))
        .roles("USER");
}

Spring Security 默认使用 DelegatingPasswordEncoder,来管理密码

也可以通过将 PasswordEncoder 公开为 Spring bean 来自定义它

!Tip

哇,我真的贴心,做了一个“双向链接”。其实,之前有看文章,遇到链接跳转到文章的某个地方,回去却没有快捷方式,而是只能通过目录寻找或者是滚轮来滑动。等我滑到位置了,我的思绪都飘远了。如果有一个回退按钮可能会更舒服。我真的太细节,牛逼Plus!

总结归纳:

在 Spring Security 的官方文档中,有关密码匹配的部分包含了详细的说明和示例。下面是一些关键点:

  1. 密码编码器(PasswordEncoder):密码匹配的核心是密码编码器,它可以将原始密码加密为散列值,并将散列值与存储在数据库中的密码进行比较,以确保密码的正确性。
  2. 密码编码器的种类:Spring Security 支持多种密码编码器,如 BCryptPasswordEncoderStandardPasswordEncoderMessageDigestPasswordEncoder 等。每种编码器都有其自身的特点和优缺点,可以根据具体情况进行选择。
  3. 密码匹配过程:在 Spring Security 中,密码匹配的过程通常是在 AuthenticationProvider 接口中实现的。在实现这个接口的 authenticate(Authentication authentication) 方法时,可以使用密码编码器对用户输入的密码进行编码,并与数据库中存储的密码进行比较。
  4. 配置密码编码器:Spring Security 提供了一个 PasswordEncoderFactories 工厂类,可以根据配置信息创建密码编码器。在配置文件中,可以通过指定密码编码器的 ID 来选择使用哪种编码器。

示例代码:

pom 文件引入

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Data
public class User implements UserDetails {
    private String username;
    private String password;
    private boolean enabled;
​
    // 省略 getter 和 setter 方法
​
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }
​
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
​
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
​
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
}
​
​
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

使用 BCryptPasswordEncoder

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Autowired
    private UserRepository userRepository;
​
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated().and().formLogin()
                .loginPage("/login").permitAll().and().logout().permitAll();
    }
​
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(username -> {
            User user = userRepository.findByUsername(username);
            if (user != null) {
                return new org.springframework.security.core.userdetails.User(user.getUsername(),
                        user.getPassword(), user.isEnabled(), true, true, true, user.getAuthorities());
            }
            throw new UsernameNotFoundException("User '" + username + "' not found");
        }).passwordEncoder(passwordEncoder());
    }
}
​

首先定义了一个 PasswordEncoder 的 Bean,并指定使用 BCryptPasswordEncoder 进行密码编码。然后,在 configure(AuthenticationManagerBuilder auth) 方法中,使用 userDetailsService() 方法从 UserRepository 中获取用户信息,并将用户的密码使用密码编码器进行编码。最后,将编码后的密码和用户信息封装成一个 UserDetails 对象返回。

防止漏洞利用

CSRF

CSRF 跨站请求 CSRF 攻击利用用户在已登录状态下的身份(这个很重要,不是字面理解:从另一个网站可以请求到另一个网站的接口,虽然看起来挺像是这样的)。

通过第三方网站向目标网站发起恶意请求(CSRF 攻击者通常会在另一个网站上放置一个恶意页面,页面中包含一些针对目标网站的恶意请求。然后,攻击者会通过某种方式诱导用户访问该恶意页面,或者将该页面注入到其他页面中,使得用户在访问该页面或其他页面时,恶意请求就会被发送到目标网站)。

攻击方式:

比如,攻击者可以在某个社交网站上发送一条包含恶意链接的消息(评论),如果用户点击该链接,就会跳转到恶意页面,从而被攻击者利用。另外,攻击者还可以通过钓鱼邮件、恶意广告等方式来诱导用户点击链接,或者将恶意请求注入到常用的网站或应用中,使得用户在不知情的情况下受到攻击。

所以陌生人给的链接不能乱哦 好看的哦!hit!hit!

从而执行某些非法操作,如修改用户密码、转账等。攻击者并不能直接访问目标网站的接口,也不是从另一个网站上请求目标网站的接口。

CSRF Token 防止 CSRF

CSRF保护依赖于使用幂等的HTTP方法来进行请求,这是因为在处理安全性问题时,幂等性可以确保系统的一致性和可预测性。如果应用程序使用的HTTP方法不是幂等的,那么就需要考虑使用其他方法来保护系统的安全性

使用 CSRF Token 可以防止 CSRF 攻击的原理是,攻击者无法获取到合法用户的 CSRF Token,从而无法在合法用户不知情的情况下发送包含 CSRF Token 的请求。

同步器令牌模式(Synchronizer Token Pattern,STP)

是一种防范 CSRF 攻击的常见解决方案,它基于同步器令牌,也被称为双重提交解决方案

STP 的核心思想是:将一个随机生成的令牌(也称为同步器令牌)存储在表单中,并将该令牌存储在 Cookie 中。在每次表单提交时,通过比较表单中的令牌和 Cookie 中的令牌是否一致,来判断是否为合法的请求。

  1. 在服务端生成一个随机的同步器令牌,并将其存储在 Cookie 中。
  2. 在表单中添加一个隐藏域,用于存储同步器令牌的值。
  3. 在表单提交时,将表单中的同步器令牌值和 Cookie 中的同步器令牌值进行比较。如果两个值不一致,说明该请求可能是 CSRF 攻击,需要进行拦截。
  4. 在服务器端,每次生成一个新的同步器令牌,并更新存储在 Cookie 中的同步器令牌的值。这样可以避免同一个 CSRF Token 被重复使用。
在同步器令牌模式下,前端在发送请求时需要携带一个有效的 CSRF Token,如果当前 Token 已经失效了,那么前端需要向后端请求一个新的 Token,一般是通过发送一个特定的请求来获取 Token,例如 Spring Security 默认的获取 Token 的接口是 /csrf,前端可以向这个接口发送请求来获取新的 Token。获取到新的 Token 后,前端需要将其存储到内存或者本地存储中,然后在后续的请求中携带这个新的 Token。

注意:如果采用这种方法,在每个需要保护的接口上都需要进行校验隐藏字段的值是否正确。这也是这种方法的一个缺点,因为需要在每个接口上都加上校验逻辑,增加了代码量和维护成本。同时,如果有新的接口加入,也需要在新增的接口上进行相应的校验。

这里引申一个方法,通过spring AOP 切面注解来解决这个问题

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CsrfProtection {
}
​
@Aspect
@Component
public class CsrfAspect {
​
@Autowired
private CsrfTokenRepository csrfTokenRepository;
​
//在切面中,我们首先注入了 CsrfTokenRepository,然后在切面方法中,通过 RequestContextHolder 获取到当前请求的 HttpServletRequest 对象,使用 CsrfTokenRepository 对象加载当前请求的 CSRF Token,如果 Token 不存在,或者请求头中的 CSRF Token 与加载的 Token 不一致,就抛出 CsrfException 异常,表示 CSRF Token 校验不通过。
@Before("@annotation(com.example.demo.annotation.CsrfProtection)")
public void validateCsrfToken(JoinPoint joinPoint) {
   HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
   CsrfToken csrfToken = csrfTokenRepository.loadToken(request);
   if (csrfToken == null) {
       throw new CsrfException("CSRF token is missing");
   }
   String csrfHeader = request.getHeader("X-CSRF-TOKEN");
   if (csrfHeader == null || csrfHeader.isEmpty()) {
       throw new CsrfException("CSRF token is missing");
   }
   if (!csrfToken.getToken().equals(csrfHeader)) {
       throw new CsrfException("CSRF token is not valid");
   }
}
}
​
@RestController
public class MyController {
​
@Autowired
private CsrfTokenRepository csrfTokenRepository;
​
@CsrfProtection
@PostMapping("/my-api")
public String myApi(@RequestBody MyRequest request) {
   // handle request
}
}
​

!Warn

使用 Spring Security 的同步器令牌模式可以帮助你自动在前端和后端之间传递和验证 CSRF 令牌,但是在后端仍然需要在每个接口中进行验证。

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .csrf()
        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .and()
      .authorizeRequests()
        .antMatchers("/", "/home").permitAll()
        .anyRequest().authenticated()
        .and()
      .formLogin()
        .loginPage("/login")
        .permitAll()
        .and()
      .logout()
        .permitAll();
  }
​
  /**
  * 设置 token 的名称
  */
  
  @Bean
  public CsrfTokenRepository csrfTokenRepository() {
    CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository();
    repository.setHeaderName("X-XSRF-TOKEN");
    return repository;
  }
​
    
  @Override
  public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/resources/**");
  }
​
}
​
@Controller
public class FormController {
    
    @Autowired
    private CsrfTokenRepository csrfTokenRepository;
    
    @PostMapping("/submit-form")
    public String submitForm(HttpServletRequest request) {
        CsrfToken csrfToken = csrfTokenRepository.loadToken(request);
        if (csrfToken == null) {
            throw new IllegalStateException("CSRF token not found");
        }
        
        String actualToken = request.getParameter(csrfToken.getParameterName());
        if (!csrfToken.getToken().equals(actualToken)) {
            throw new IllegalStateException("Invalid CSRF token");
        }
        
        // process the form submission
        return "success";
    }
}
​
​
​

!Warn

在生成和验证 CSRF Token 时,需要考虑安全性和可用性的平衡。例如,CSRF Token 的生成算法应该足够复杂,以防止被恶意攻击者猜测或者暴力破解,但同时应该保证生成算法的可用性,以便正常用户能够正常访问系统。同时,在使用 Cookie 存储 CSRF Token 时,需要考虑设置 Cookie 的 PathSameSiteLaxDomain 属性,以防止 CSRF Token 被窃取或者被其他站点使用。

SameSite是一种新的 Cookie 属性,它可以让网站指定这个 Cookie 是否可以作为第三方 Cookie,从而防止 CSRF 和 XSS 攻击。设置SameSiteStrict时,Cookie 仅可被同站点请求使用;设置SameSiteLax时,Cookie 可以被同站点 GET 方式提交和第三方站点以 GET 方式提交的请求使用。而不设置SameSite或者设置为None时,Cookie 可以被所有站点使用,这是最危险的情况,可能导致 CSRF 和 XSS 攻击。

PathDomain属性是用来控制浏览器何时将 Cookie 发送回服务器的。Path属性表示该 Cookie 所属的 URL 路径,只有访问该路径及其子路径的请求才会带上该 Cookie。而Domain属性则表示该 Cookie 的域名,只有访问该域名及其子域名的请求才会带上该 Cookie。这两个属性也可以帮助防止 CSRF 和 XSS 攻击,例如限制 Cookie 只能被特定的 URL 路径和域名所使用。但它们的作用范围比SameSite更广,也不是直接针对 CSRF 和 XSS 攻击的防御措施。

安全 HTTP 响应标头

一块的内容有一个简单的认识,看到能够知道是什么即可 我是这么认为的(确信 🤔 ing

是一组HTTP响应头部,可以在Web应用程序中使用,以提高安全性和保护Web应用程序免受许多类型的攻击。这些响应头通常是由服务器发送到客户端的,指示浏览器和其他客户端如何处理应用程序的资源。

  1. X-Frame-Options:防止Clickjacking攻击,指定哪些网站可以在iframe中嵌入您的页面。
  2. X-XSS-Protection:启用内置的浏览器跨站脚本保护,防止XSS攻击。
  3. X-Content-Type-Options:禁用浏览器的MIME类型猜测机制,防止MIME类型欺骗攻击。
  4. Content-Security-Policy:防止XSS、代码注入等攻击,指定允许加载哪些资源。
  5. Strict-Transport-Security:启用HTTP Strict Transport Security(HSTS),防止SSL剥离和中间人攻击。
缓存控制(Cache-Control)

缓存控制是指通过设置HTTP响应头信息来控制客户端缓存页面、脚本、样式表和其他资源的方式。它可以让浏览器缓存页面内容,从而提高网站的性能和用户体验。

缓存控制通常包括以下HTTP响应头:

  1. Cache-Control:用于控制缓存机制。该响应头可以设置多个指令,如no-cache(每次请求都需要从服务器获取新的内容)、no-store(不缓存任何内容,每次请求都需要从服务器获取)、max-age(缓存内容在客户端的最长时间,单位为秒)、private(缓存内容只对单个用户有效)等。
  2. Expires:指定一个日期/时间,该时间之前缓存的响应被认为是新鲜的,之后的请求将从服务器获取新的内容。
  3. ETag:用于识别资源的版本号。当客户端请求一个资源时,它可以使用ETag值与服务器上的值进行比较,以判断缓存的内容是否过期。

通过合理地设置缓存控制响应头,可以有效地减少网络带宽的使用和客户端资源的消耗,从而提高网站的性能和可访问性。

示例代码:

在 Spring Security 中,可以通过 http.headers().cacheControl().disable() 方法禁用缓存控制,或者通过 http.headers().cacheControl().maxAge(3600, TimeUnit.SECONDS) 方法设置缓存最大有效期为 1 小时。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .and()
            .logout()
                .and()
            .headers()
                .cacheControl()
                    .disable();
    }
}
​
//上面的配置将缓存最大有效期设置为 60 秒,并设置缓存为公共缓存。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .and()
            .logout()
                .and()
            .headers()
                .cacheControl()
                    .maxAge(60, TimeUnit.SECONDS)
                    .cachePublic();
    }
}
​
内容类型选项(Content-Type Options)

是一种安全 HTTP 响应标头,用于控制浏览器如何解释响应中的内容类型。它可以帮助防止某些类型的攻击,例如 MIME 类型混淆攻击(MIME type sniffing)。

当浏览器接收到服务器响应时,它会检查 Content-Type 头来确定如何处理响应中的内容。如果响应中的 Content-Type 不正确或被篡改,可能会导致浏览器解释错误的内容类型。这可能会导致安全漏洞,例如 XSS 攻击等。

为了防止这种攻击,可以使用内容类型选项标头,强制浏览器使用响应中指定的 Content-Type 头。这可以通过在 HTTP 响应中添加以下标头来完成:

示例代码:
X-Content-Type-Options: nosniff
​
//使用 disable() 方法可以禁用内容类型选项标头,使用默认设置启用它。默认情况下,Spring Security 会启用内容类型选项标头   
http.headers().contentTypeOptions().disable();
​
HTTP 严格传输安全 (HSTS)

HTTP 严格传输安全(HTTP Strict Transport Security,HSTS)是一种HTTP响应头,用于保护网站免受 SSL/TLS 前向保密(Forward Secrecy)的攻击,同时确保客户端浏览器始终通过 HTTPS 访问该网站。

当一个网站启用 HSTS 时,客户端浏览器会将该网站的域名和协议类型(HTTP 或 HTTPS)存储在其预加载列表中。从此以后,当用户尝试使用 HTTP 访问该网站时,浏览器会自动将其重定向到 HTTPS,而无需用户手动输入 HTTPS。

以下是一个 HSTS 响应头的示例:

​
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • max-age 指定了 HSTS 头的有效时间,以秒为单位。
  • includeSubDomains 指定了 HSTS 头是否应该应用于所有子域名。
  • preload 告诉浏览器将此网站添加到其内置的 HSTS 预加载列表中,以便更好地保护用户。
示例代码:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers().httpStrictTransportSecurity().maxAgeInSeconds(31536000).includeSubDomains(true);
    }
}
//maxAgeInSeconds 指定了 HSTS 头的有效时间为 1 年,includeSubDomains 则告诉浏览器应该应用于所有子域名。
HTTP 公钥固定 (HTTP Public Key Pinning, HPKP)

是一种防止中间人攻击的安全机制。它通过在服务器响应头中添加公钥指纹信息,告诉客户端浏览器哪些公钥是可以信任的,从而保证客户端和服务器通信的安全性。

HPKP 的工作原理是服务器在 HTTPS 响应头中添加 Public-Key-Pins 选项,包含公钥指纹和最长有效期等信息。浏览器第一次连接时会保存这些公钥信息,接下来的每一次连接都会检查服务器返回的公钥指纹是否与之前保存的一致。如果不一致,浏览器就会认为遭遇了中间人攻击,停止连接。

Public-Key-Pins: max-age=31536000; includeSubDomains; 
    pin-sha256="abc123..."; 
    pin-sha256="def456..."; 
    report-uri="https://example.com/hpkp-report"
示例代码:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers()
                .httpPublicKeyPinning()
                    .addSha256Pins("pin1", "pin2")
                    .maxAgeInSeconds(31536000);
    }
}
​

!INFO

HSTS 和 HPKP 是增强 Web 应用程序安全性的重要工具,但需要在使用时谨慎小心,以避免意外的问题。

X-Frame-Option

是一个 HTTP 响应头,用于控制浏览器是否允许在 iframe 中加载页面,可以帮助防止跨站点脚本攻击(XSS)中的点击劫持攻击。该头部可设置三个值:

  • DENY: 表示不允许任何网站在 iframe 中嵌入自己的页面。
  • SAMEORIGIN: 表示允许与当前页面具有相同源的网站在 iframe 中嵌入页面,而其他网站不行。
  • ALLOW-FROM uri: 表示允许指定的 uri 在 iframe 中嵌入页面。
示例代码:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .headers()
                .frameOptions()
                    .deny();
    }
}
​
X-XSS-protection

是一个 HTTP 响应头,用于防止跨站脚本攻击(XSS)。它可以在浏览器检测到反射型XSS攻击时阻止页面加载或渲染受到攻击的页面,从而保护用户不受攻击。当浏览器检测到可能存在XSS攻击时,它将停止渲染页面并显示错误信息。

该响应头有三个值可以使用:0、1 和 mode=block。

  • 0:表示禁用 XSS 过滤器。
  • 1:表示启用 XSS 过滤器,并尝试从页面中删除受到攻击的部分。
  • mode=block:表示启用 XSS 过滤器,并阻止页面加载。
示例代码:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().xssProtection().block(true);
    }
}
​
内容安全策略 (CSP)

是一种增强浏览器安全的机制,它通过指定服务器信任的内容源,减少浏览器执行恶意脚本的风险。CSP 可以减少跨站点脚本攻击 (XSS) 和数据注入攻击等攻击手段的成功率。

CSP 可以让服务器端通过 HTTP 响应头来告诉浏览器,哪些内容是安全的,哪些内容是不被信任的,从而帮助浏览器正确地执行页面中的脚本、样式等资源。CSP 可以限制 JavaScript 脚本只能从指定的源加载,从而防止 XSS 攻击,还可以限制 CSS 样式只能从指定的源加载,防止 CSS 注入攻击。

示例代码
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .headers()
                .contentSecurityPolicy("default-src 'self'"); //我们通过 contentSecurityPolicy() 方法将 CSP 配置为只信任当前域名的内容源,即只允许从当前域名加载资源。
    }
}
​

需要注意的是,CSP 的配置需要根据实际情况进行调整,例如可以指定允许的图像、字体等资源来源,也可以指定允许的脚本来源等。同时,CSP 的配置需要在服务端和客户端共同实现才能生效。

Referrer Policy

是一种HTTP头,用于控制客户端(例如浏览器)在导航和请求期间如何公开来源信息。Referrer Policy可用于减少来自第三方站点的信息泄漏和某些跨站点攻击。

  • no-referrer:不发送Referer header。
  • no-referrer-when-downgrade:对于从HTTPS网站发出的导航请求,不发送Referer header。对于从HTTPS网站到HTTP网站的导航请求,仍然发送Referer header。
  • origin:仅发送源信息(包括协议、域名和端口),不发送完整的URL。
  • origin-when-cross-origin:对于同源请求,发送完整的Referer header,对于跨源请求,仅发送源信息(包括协议、域名和端口)。
  • same-origin:对于同源请求,发送完整的Referer header,对于跨源请求,不发送Referer header。
  • strict-origin:仅发送源信息(包括协议、域名和端口),但仅在请求源和目标源具有相同的协议时发送。
  • strict-origin-when-cross-origin:对于同源请求,发送完整的Referer header,对于跨源请求,仅发送源信息(包括协议、域名和端口),但仅在请求源和目标源具有相同的协议时发送。
  • unsafe-url:对于所有请求,都发送完整的Referer header。
示例代码
http
  .headers()
    .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN)
    .and()
  // Other configurations
  .authorizeRequests()
  .antMatchers("/admin/**").hasRole("ADMIN")
  .anyRequest().authenticated()
  .and()
  .formLogin()
  .and()
  .httpBasic();
​
Feature Policy

是一种Web应用程序安全机制,它允许开发者限制浏览器中已启用的特定功能的使用范围。使用Feature Policy,开发人员可以控制一组API(例如麦克风、摄像头、全屏显示等)如何与页面交互,从而提高应用程序的安全性。

可以通过HTTP头或在页面中设置特定元标记来定义Feature Policy。设置Feature Policy时,可以限制某些API或功能只能在安全的上下文中(如HTTPS)使用,或者限制哪些来源可以使用某些功能。Feature Policy可以帮助应用程序限制访问用户数据的范围,提高隐私和安全性。

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.headers()
      .featurePolicy("camera 'none'; microphone 'none'");
  }
}
//将在HTTP响应头中设置Feature Policy。如果应用程序使用HTML meta标记设置Feature Policy,则可以使用ContentSecurityPolicyFilter过滤器确保它们的一致性。
Permissions Policy

是一个 Web 安全标准,它允许网站所有者指定哪些功能和 API 可以在站点中使用,并阻止未经授权的行为。Permissions Policy 可以在 HTTP 响应头中设置,并使用有效的 CSP(内容安全策略)指令语法。它可以用来限制网站对用户设备和浏览器的访问,以减少潜在的安全威胁。

示例代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .headers()
                .contentSecurityPolicy("default-src 'self';style-src 'self' example.com");
    }
 
}
//contentSecurityPolicy 方法允许定义 CSP 的规则和限制了只能从 example.com 加载 CSS。例如,上面的配置规定只允许从相同域加载资源。您可以根据需要添加更多的指令和限制
Clear Site Data

允许网站通过响应头通知用户代理清除浏览器中的特定类型的数据,例如缓存、Cookie、存储等。这可以用于保护用户的隐私或重置网站状态。

Clear-Site-Data 标头包含一个或多个指令,指定要清除的数据类型。可用的指令包括:

  • "cache":清除浏览器缓存。
  • "cookies":清除浏览器的 Cookie。
  • "storage":清除浏览器的本地存储和 IndexedDB 数据库。
  • "executionContexts":关闭任何正在执行的脚本。

指令之间用逗号分隔。可以使用星号 (*) 通配符指定所有数据类型

@Configuration
@EnableWebSecurity
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
​
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .logout()
        .logoutUrl("/logout")
        .logoutSuccessUrl("/")
        .invalidateHttpSession(true)
        .addLogoutHandler(new SecurityContextLogoutHandler())
        .deleteCookies("JSESSIONID");
  }
}
//Spring Security 配置中添加了一个 LogoutFilter,并在其中添加了一个 SecurityContextLogoutHandler,以便在注销时清除 SecurityContext 中的用户信息。我们还使用 deleteCookies() 方法来删除名为 JSESSIONID 的 Cookie。这将清除用户在浏览器中保留的会话数据

模块

Spring Security Crypto模块

Spring Security中的一个子模块,用于提供加密和解密的支持。它是一个通用的加密和解密库,可以用于任何应用程序,而不仅仅是Spring Security应用程序。

Spring Security Crypto 模块还提供了一些其他常用的方法,包括:

  1. 摘要算法:提供 SHA-1、SHA-256、SHA-384、SHA-512、MD5 等常用的摘要算法,可用于生成密码的哈希值。
  2. 密码强度检测:提供 PasswordStrength 函数,可检测密码的强度,并根据需要进行修改。
  3. 随机数生成:提供 RandomValueStringGenerator 函数,可生成指定长度的随机字符串。
  4. BCrypt 密码编码器:提供 BCryptPasswordEncoder 类,可使用 BCrypt 算法加密和验证密码。
  5. 对称加密算法:提供 AES、DES、TripleDES 等常用的对称加密算法。
  6. 密钥生成器KeyGenerators:用于生成通用密钥、SecretKeyFactory:用于生成对称加密所需的密钥、KeyPairGenerator:用于生成非对称加密所需的密钥对

示例代码:

1.创建一个Encryptor对象,用于指定加密算法和密钥。

2.调用Encryptor对象的encrypt方法,传递要加密的明文数据作为参数,该方法将返回一个密文。

3.将密文存储在数据库或文件系统中。

解密数据也非常简单,只需要使用相同的Encryptor对象调用decrypt方法即可。在使用Spring Security Crypto模块时,要确保密钥是安全的,并且不会被泄露。

import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.encrypt.TextEncryptor;
​
public class CryptoExample {
    public static void main(String[] args) {
        // 创建一个Encryptor对象,使用AES算法和密钥1234567890abcdef
        TextEncryptor encryptor = Encryptors.text("1234567890abcdef", "deadbeef");
​
        // 要加密的明文数据
        String plaintext = "Hello, world!";
​
        // 加密数据
        String ciphertext = encryptor.encrypt(plaintext);
        System.out.println("加密后的数据:" + ciphertext);
​
        // 解密数据
        String decryptedText = encryptor.decrypt(ciphertext);
        System.out.println("解密后的数据:" + decryptedText);
    }
}
//我们创建了一个TextEncryptor对象,使用AES算法和密钥1234567890abcdef。我们将要加密的明文数据传递给encrypt方法,该方法将返回一个密文。我们将密文存储在数据库或文件系统中。要解密数据,我们使用相同的Encryptor对象调用decrypt方法。在这个例子中,我们解密了加密后的数据,并将其打印到控制台上。

[1]  en.wikipedia.org/wiki/Bcrypt "Bcrypt"

[2]  en.wikipedia.org/wiki/Argon2 "Argon2"

[3]  en.wikipedia.org/wiki/Argon2 "Pbkdf2"

[4]  en.wikipedia.org/wiki/Argon2 "SCrypt"