Spring Security In Action 读书笔记

发布时间 2023-08-05 21:21:51作者: ivanohohoh

Spring Security in Action

2023-7-30 Just Book, Just learning!

这本书适用于初学者,简单的探讨 ss 认证,权限控制, 安全防护,OAth2 的使用,并没有涉及具体的架构(只有一个简单的认证架构图),其中权限控制讲的内容太少了!

Spring Security 是什么样的?我大概能干什么?

image

在这里我先简单的把 ss 引入先有的 spring 生态,ss 是基于 servlet 的 filter 来实现的,与 spring mvc 集成在一起。

上图简单的描述了 ss 的架构,我们可以创建很多个过滤器来认证,鉴权,保护 web 应用。

  1. authentication filter 是进入 ss 的入口,同时也是出口。在入口它会将 httpRequest 转变为 ss 中的输入对象,在出口会将认证结果记录到 security context 中。
  2. authentication manager 会把身份认证委托给 authentication provider 组件。
  3. authentication provider 会针对特定的认证方式,实现具体的身份验证逻辑,并且它把与用户相关的和密码相关的操作分解到 user detial service 和 password encoder 两个组件上。
  4. security context 会记录用户的认证信息,它会使用 cookie 等技术记住用户的请求。

覆盖默认配置

ss 默认提供 http basic 认证,默认用户名为 user, 启动项目时会打印对应的密码,并且默认所有请求都必须得到认证。

当我们想要提供新的认证方式时需要提供新的 authentication provider,因为它是实现具体认证逻辑的地方。

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    # 关键在这里,每个 authenticationProvider 会告诉 authentication manager 
    # 它能处理那些认证类型,所以当我们修改认证方式时要覆盖默认实现类,提供特定的实现类
    boolean supports(Class<?> authentication);
}

由于 ss 将用户管理和密码管理分离成出来了,所以我们也可以只替换 user details serivce 或 password encoder。

Tip: 当替换默认配置的 user details service 时,必须也把 password encoder 替换了。

覆盖默认配置:

  1. 当我们在配置类中注入 user details serivce 或 password encoder Bean 时,其会自动覆盖掉默认配置
  2. 当我们想要修改认证逻辑或认证类型时需要通过注入 SecurityFilterChain 实现
    # 注入自定义 AuthenticationProvider
    @Bean
    CustomAuthenticationProvider customAuthenticationProvider() {
        return new CustomAuthenticationProvider();
    }
    
    @Bean
    SecurityFilterChain configure(HttpSecurity http) throws Exception {
        # HttpSecurity 几乎可以做任何事情!!
    
        # 设置认证类型
        http.httpBasic(Customizer.withDefaults());
        # 设置自定义 AuthenticationProvider
        http.authenticationProvider(customAuthenticationProvider());
        # .....
        http.authorizeHttpRequests(c -> c.anyRequest().authenticated());
        return http.build();
    }
    
    # 这里有必要介绍一下 Customizer 接口,它其实就是个回调对象
    @FunctionalInterface
    public interface Customizer<T> {
        void customize(T t);
    
        static <T> Customizer<T> withDefaults() {
            return (t) -> {
            };
        }
    }
    
    
  3. 在端点级别控制授权
    @Bean
    SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.httpBasic(Customizer.withDefaults());
        http.authenticationProvider(customAuthenticationProvider());
    
        # 该方法也接收一个 Customizer 回调,用于在端点级别控制授权
        http.authorizeHttpRequests(c -> c.anyRequest().authenticated());
        return http.build();
    }
    

管理用户

image

ss 将用户相关操作分离成了独立的组件,并且制定了大量的接口,其中 user details service 和 user details manager 实现用户相关操作的具体逻辑,user details 是 ss 定义的用户接口,Granted authority是 ss 定义的权限接口

Tips: 我们为用户配置的 role,会被添加 ROLE_ 前缀后,存储在用户权限中。我们甚至可以通过添加带有 ROLE_ 前缀的权限来为用户添加权限角色。

这些接口都很简单,一眼就可以看出其作用,那就在此一一介绍一下:

  1. UserDetails
     // 在 ss 眼中的用户模型
     public interface UserDetails extends Serializable {
         String getUsername(); #A
         String getPassword();
         Collection<? extends GrantedAuthority> getAuthorities(); #B
    
         // 我们可以实现这些功能,也可以简单的返回 true
         boolean isAccountNonExpired(); #C
         boolean isAccountNonLocked();
         boolean isCredentialsNonExpired();
         boolean isEnabled();
     }
    
  2. GrantedAuthority
     public interface GrantedAuthority extends Serializable {
         // 返回权限标识
         String getAuthority();
     }
    
  3. UserDetailsService
     public interface UserDetailsService {
         // 认证最核心的操作
         UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
     }
    
  4. UserDetailsManager
     // 将用户管理,与安全框架耦合在一切好吗? 
     // 感觉独立成用户模块来管理更好呢!
     public interface UserDetailsManager extends UserDetailsService {
         void createUser(UserDetails user);
         void updateUser(UserDetails user);
         void deleteUser(String username);
         void changePassword(String oldPassword, String newPassword);
         boolean userExists(String username);
     }
    

这些接口很简单,可以很容易的通过扩展 UserDetailsService 来实现自己的用户认证,甚至通过 UserDetails 实现用户状态管控功能。

可以通过 User 类来快速构建 UserDetail 实例

UserDetails u1 = User.withUsername("ivan")
                .password("123456")
                .authorities("read")
                .roles("admin")  // 会转换为 ROLE_admin 添加到用户权限中
                .build();

密码管理

Spring Security 认为直接把密码存储起来不安全,应该加密后再存储。

PasswordEncoder 提供了更加便捷的加密操作和对比明文与秘文操作。

并且 Spring Security 的 Crypto 模块,提供了一定的加密功能。

PasswordEncoder

public interface PasswordEncoder {
    // 加密
    String encode(CharSequence rawPassword);
    // 对比
    boolean matches(CharSequence rawPassword, String encodedPassword);  
    // 如果返回true,会对加密结果再进行一次加密
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

Spring Security 还提供了一些具体的实现如下:

Pbkdf2PasswordEncoder: 
PasswordEncoder p = New Pbkdf2PasswordEncoder(“secret”, 16, 310000,
Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
                                             // PBKDF2WithHmacSHA1
                                             // PBKDF2WithHmacSHA256
                                             // PBKDF2WithHmacSHA512

BCryptPasswordEncoder:
PasswordEncoder p = new BCryptPasswordEncoder();
PasswordEncoder p = new BCryptPasswordEncoder(4); // 4 ~ 32
SecureRandom salt = SecureRandom.getInstanceStrong();
PasswordEncoder p = new BCryptPasswordEncoder(4, salt);  

SCryptPasswordEncoder:
PasswordEncoder p = new SCryptPasswordEncoder(163845, 8, 1, 32, 64)

做好的设计就是最容易变化的设计,加密方式也在不断的淘汰,进化。ss 给我提供了一个特殊的实现 DelegatingPasswordEncoder 来应对这种变化,使得我们可以自主选择加密方式来对用户密码进行保护。

DelegatingPasswordEncoder 在加密时会根据密码前缀把加密任务委托给对应的机密器,在对比时也根据密码前缀把对比任务委托出去。
image

1. 构建 DelegatingPasswordEncoder
// 创建加密前缀与加密器的映射关系
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());

return new DelegatingPasswordEncoder("bcrypt", encoders);

2. 加密
String pwd = "123456";
String encode = delegatingPasswordEncoder.encode("{noop}" + pwd);

3. 比对
delegatingPasswordEncoder.matches("{noop}" + pwd, encode);

Spring Security Crypto 模块

ss 加密模块主要提供两类工具密钥生成器和加密器

生成密钥

主要分为字符串和字节数组两种类型的密钥生成器

String:
StringKeyGenerator keyGenerator = KeyGenerators.string();
String salt = keyGenerator.generateKey(); // 默认 8 字节,编码为 16 进制字符串

Bytes:
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
byte [] key = keyGenerator.generateKey(); // 默认每次生成的不一样
int keyLength = keyGenerator.getKeyLength();

BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(16); // 设置字节长度

BytesKeyGenerator keyGenerator = KeyGenerators.shared(16); // 每次生成一样的结果
byte [] key1 = keyGenerator.generateKey();
byte [] key2 = keyGenerator.generateKey(); // key1 == key2

Filter Chain

这本书对于 Security Filter 谈论的非常少!

脑海中最先冒出来的就是多个验证过滤器同时存在会怎么样?过滤器的执行顺序?过滤器处于SpringMvc的那部分?默认提供那些过滤器......

Security Filter 存在哪?

其基于 servletFilter 实现,但是并不是 ServletFilter 如下图:
image
SecurityFilter 被套娃在了一个 servletFilter 内!

默认过滤器,过滤器顺序?

TIP: 有很多默认过滤器,但只需知道关键的几个过滤器来辅助添加过滤器即可!

启动应用后会按照顺序打印存在的所有过滤器!
image

TIP: 可添加过滤器可以在某个过滤器之前或之后,以及同一位置。Spring security 不保证同一位置的过滤器执行顺序!

我们无法改变默认过滤器的顺序,但可以在任意的位置添加过滤器,所以过滤器的相对顺序是不会改变的。

多个验证过滤器不起冲突吗?

当然不会,所有过滤器共享同一个 context,通过检查上下文中的验证事件对象 Authentication , 就可以解决一切事端了。

自定义过滤器

# 实现 Filter 接口
public class RequestValidationFilter
implements Filter { #A
    @Override
    public void doFilter(
        ServletRequest servletRequest,
        ServletResponse servletResponse,
        FilterChain filterChain)
        throws IOException, ServletException {
        // ...
    }
}

# 添加过滤器
http.addFilterBefore(newFilter, positionFilter)
http.addFilterAfter(newFilter, positionFilter)
http.addFilterAt(newFilter, positionFilter)

实现认证

Spring security 提供的认证框架大多都是基于前后端不分离模型,而本节介绍的也都是如此,所以我在一下主要介绍一下如果自定义认证过程,如何使用 SecurityContext

SpringSecurity 简易架构图

A Filter 的责任是把请求中认证相关的数据提取到一个对象中,把它叫做认证对象,来作为 ss 框架认证的输入。

A Manager 的责任是根据输入把认证任务委托给对应的 A Provider。

A Provider 的责任是根据认证对象来认证是否通过认证。

Security Context 的责任是作为同一 Filter Chain 的上下文来保存认证结果!

以下是要讨论的细节:

  1. 认证对象是什么?
  2. A Manager 如何进行认证委托的?
  3. 认证结果是什么,有什么用?
  4. 认证后,后续登录还需要再次认证吗?
  5. 自定义认证流程示例

认证对象是什么?

无论何种认证方式,本质上都是信息对比!

image

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    // 在认证完毕后最好删除密码相关信息
    Object getCredentials();
    Object getDetails();
    // 该方法返回对象主体,往往返回 UserDetails
    Object getPrincipal();
    // ss 表示认证对象是否认证成功
    boolean isAuthenticated();
    // 为了安全考量,该方法最好禁止设置 truer,通过构造函数来直接构建已授权的认证对象
    void setAuthenticated(boolean isAuthenticated)
    throws IllegalArgumentException;
}

在自定义认证对象时,越简单越明了,尽量不要实现以下接口!

ss 也提供了大量的具体实现,他们都实现了另一个接口:

public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
    ......
    // 关键方法
    public String getName() {
        if (this.getPrincipal() instanceof UserDetails) {
            return ((UserDetails)this.getPrincipal()).getUsername();
        } else if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
            return ((AuthenticatedPrincipal)this.getPrincipal()).getName();
        } else if (this.getPrincipal() instanceof Principal) {
            return ((Principal)this.getPrincipal()).getName();
        } else {
            return this.getPrincipal() == null ? "" : this.getPrincipal().toString();
        }
    }
    ......
}

// 该接口的目的是在验证结束后清除敏感信息,被 ss 框架内部使用
public interface CredentialsContainer {
    void eraseCredentials();
}

具体实现比如 UsernamePasswordAuthenticationToken......

A Manager 如何进行认证委托的?

A Manager 的常用实现为 ProviderManager 类,针对该类探讨委托具体过程。

可以简单的把认证对象看作一把?,AuthenticationProvider 看作各种开锁工具, AuthenticationMananger 看作开锁匠。

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}


public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    // 用于查看是否支持该?类型
    boolean supports(Class<?> authentication);
}

ProviderManager 委托过程:

  1. 通过构造函数,注册 AuthenticationProvider

  2. 将连续尝试 AuthenticationProvider 列表,直到 AuthenticationProvider 指示它能够验证传递的 Authentication 对象的类型。然后将尝试使用该 AuthenticationProvider 进行身份验证。

    如果多个 AuthenticationProvider 支持传递的 Authentication 对象,则第一个能够成功验证(返回打开的?,即 isAuthentication() 返回 true 的 Authentication 对象) Authentication 对象的对象将确定 result ,并覆盖早期支持 AuthenticationProvider 抛出的任何可能的 AuthenticationException 。身份验证成功后,不会尝试后续的 AuthenticationProvider 。如果任何支持 AuthenticationProvider 的身份验证均未成功,则将重新抛出最后抛出的 AuthenticationException 。

  3. 成功后将 Authentication 或异常抛给 Filter

认证结果是什么,有什么用?

认证结果就是打开的?,即 isAuthentication() 返回 true 的 Authentication 对象。

TIP: SecurityContextHolder 存在 3 种上下文管理方式:

  1. (default)MODE_THREADLOCAL 允许每个线程将自己的详细信息存储在安全上下文。在每个请求一个线程的 Web 应用程序中,这是一种常见的方法,因为每个请求都有一个单独的线程。
  2. MODE_INHERITABLETHREADLOCAL 与MODE_THREADLOCAL 类似,但是如果是异步方法,还指示 Spring Security 将安全上下文复制到下一个线程。
  3. MODE_GLOBAL 使应用程序的所有线程看到相同的内容

认证结果会被存储到全部 filter 共享的 SecurityContext 上下文中,它有一下几个作用:

  1. 告诉其他 filter,该请求通过认证了
  2. 供 cotroller 使用登录的用户信息
     1. 通过全局的 SecurityContextHolder 来获取
         @GetMapping("/hello")
         public String hello() {
             SecurityContext context = SecurityContextHolder.getContext();
             Authentication a = context.getAuthentication();
             return "Hello, " + a.getName() + "!";
         }
     2. 通过参数获取
         @GetMapping("/hello")
             public String hello(Authentication a) { #A
             return "Hello, " + a.getName() + "!";
         }
    

默认提供了 cookie 机制来维持用户的会话状态!

添加的自定义认证过滤器,要首先查看一下 context,看看当前请求是否已经认证完毕。因为cookie过滤器一定在认证过滤器之前进行认证。

自定义认证过滤器

注意构建每个组件,完成各自的责任

自定义认证对象(?)

public class CustomerAuthentication implements Authentication {
    private UserDetails userDetails;
    private Boolean authenticated;

    private String details;
    public CustomerAuthentication(UserDetails userDetails, String sender, Boolean authenticated) {
        this.authenticated = authenticated;
        this.userDetails = userDetails;
        // Http sender 字段
        this.details = sender;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return userDetails.getAuthorities();
    }

    @Override
    public Object getCredentials() {
        return userDetails.getPassword();
    }

    @Override
    public Object getDetails() {
        return details;
    }

    @Override
    public Object getPrincipal() {
        return userDetails;
    }

    @Override
    public boolean isAuthenticated() {
        return authenticated;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Can't set authenticated!");
        }
        this.authenticated = isAuthenticated;
    }

    @Override
    public String getName() {
        return userDetails.getUsername();
    }
}

自定义认证过滤器

public class CustomerFilter extends OncePerRequestFilter {
    private AuthenticationManager authenticationManager;


    public CustomerFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String name = request.getParameter("name");
        String pwd = request.getParameter("pwd");
        String sender = request.getHeader("sender");

        UserDetails build = User.withUsername(name)
                .password(pwd)
                .authorities("!")
                .build();
        CustomerAuthentication customerAuthentication = new CustomerAuthentication(build, sender, false);
        Authentication authenticate = authenticationManager.authenticate(customerAuthentication);

        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(authenticate);

        filterChain.doFilter(request, response);
    }
}

自定义开锁工具

public class CustomerProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;

    public CustomerProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UserDetails principal = (UserDetails) authentication.getPrincipal();
        if (userDetailsService.loadUserByUsername(authentication.getName()) == null) {
            throw new UsernameNotFoundException("Can't found user " + authentication.getName());
        }
        if (!passwordEncoder.matches(authentication.getCredentials().toString(), userDetailsService.loadUserByUsername(authentication.getName()).getPassword())) {
            throw new AuthenticationCredentialsNotFoundException("Compare false");
        }
        if (authentication.getDetails() == null || authentication.getDetails().equals("")) {
            throw new UsernameNotFoundException("Can't find sender!");
        }
        return new CustomerAuthentication(principal, authentication.getDetails().toString(), true);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomerAuthentication.class.isAssignableFrom(authentication);
    }
}

配置类

@Configuration
public class ProjectConfig {
    @Bean
    @Order(1)
    public UserDetailsService userDetailsService() {
        UserDetails build = User.withUsername("ivan")
                .password("123456")
                .authorities("read")
                .build();
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(build);
        return inMemoryUserDetailsManager;
    }

    @Bean
    @Order(2)
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    @Order(3)
    public AuthenticationProvider authenticationProvider() {
        return new CustomerProvider(userDetailsService(), passwordEncoder());
    }

    @Bean
    @Order(4)
    public AuthenticationManager authenticationManager() {
        return  new ProviderManager(authenticationProvider());
    }

    @Bean
    @Order(5)
    public Filter filter() {
        return new CustomerFilter(authenticationManager());
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.addFilterBefore(filter(), BasicAuthenticationFilter.class)
                .authorizeRequests(c -> c.anyRequest().authenticated());
        return http.build();
    }
}

配置端点级授权:限制访问

授权是应用程序决定是否允许经过身份验证的请求的过程。授权总是在身份验证之后发生

提取端点,设置权限的方式有很多,一下分别进行介绍:

mvcMathcers 针对 springmvc 实现,可以识别出 /hello == /hello/,会对相等的 url 进行一致的权限控制,而 antMathcers, regexMatchers,则是对 url 的硬编码。

  1. requestMatchers:
    requestMatchers 是一种更通用的方法,它允许你传递一个或多个 RequestMatcher 对象,用于匹配请求。这些 RequestMatcher 可以使用不同的匹配条件,如 URL、HTTP 方法、Headers 等。你可以自定义 RequestMatcher 实现来定义特定的匹配逻辑。
  2. antMatchers:
    antMatchers 允许你使用 Ant 风格的路径模式来匹配请求。Ant 风格的路径模式类似于正则表达式,但更简单。你可以使用通配符 ? 匹配单个字符,* 匹配零个或多个字符,以及 ** 递归匹配零个或多个目录
  3. regexMatchers:
    regexMatchers 允许你使用正则表达式来匹配请求的路径。这种方式更灵活,可以实现更复杂的匹配逻辑,但也更复杂.
  4. mvcMathcers:
    mvcMatchers 方法是针对 Spring MVC 控制器的映射路径进行匹配的。它与 antMatchers 和 regexMatchers 相似,但是更加针对 Spring MVC 的控制器方法。使用 mvcMatchers 可以更方便地与 Spring MVC 控制器的路径模式进行匹配,而无需使用硬编码的路径字符串。

设置权限:

  1. hasRole()
  2. hasAnyRole()
  3. hasAuthority()
  4. hasAnyAuthority()
  5. access("spel 表达式")
  6. denyAll()
  7. permitAll()
TIP: 在设置端点授权时,必须先设置请求路径范围小的配置,再设置范围大的配置,比如 anyRequest 相关配置应该再最后配置
@Configuration
public class ProjectConfig {
    // Omitted code
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
    throws Exception {
        http.httpBasic(Customizer.withDefaults());
        http.authorizeHttpRequests(
            c -> c.requestMatchers(new AntPathRequestMatcher("/hr/**")).hasRole("MANAGER") 
                    .requestMatchers(new AntPathRequestMatcher("/product/{code:^[0-9]*$}")).permitAll()
                    .mvcMatchers("/hello").hasRole("ADMIN")
                    .mvcMatchers("/hello").hasRole("ADMIN")
                    .regexMatchers(".*/(us|uk|ca)+/(en|fr).*").authenticated()
                    .anyRequest().hasRole("ADMIN")
                    .anyRequest().hasAnyRole("ABC")
                    .anyRequest().access("hasAuthority('WRITE')")
                    .anyRequest().hasAuthority("READ")
                    .anyRequest().hasAnyAuthority("READ")
                    .anyRequest().denyAll() 
        ); 
        return http.build();
    }
}

CSRF

image

csrf 是通过利用用户的登录状态,来进行恶意操作。但是它只对 cookie 会话有效!

如果我们使用 jwt 来维护 http 状态,csrf 根本无法利用用户的登录状态,因为 jwt 请求头没有 cookie 的自携带特性。

如果不使用 cookie,还有必要设置 csrf 防护吗?我感觉没必要!

即使没必要,也来看看 ss 如何实现 csrf 防护的。

ss 使用 csrf-token 来为用户提供一个新令牌,客户端在发起异化操作时,必须携带该令牌。由于那些恶意代码无法获取 csrf-token,所以可以起到防护的作用。

所以主要的逻辑就是要验证令牌,以及创建新令牌:CsrfFilter 在认证过滤器之前,目的就是为请求生成 csrf-token,并在每次请求中进行比对,如果比对失败会返回 403.

该过滤器主要分为 3 大组件:

  1. CsrfTokenRepository 接口,用于创建,保存,获取 Token,属于工具类。
  2. CsrfTokenRequestHandler 接口,是 csfr 防护的核心逻辑。
  3. CsrfToken 接口,表示 token 主体。
 public interface CsrfToken extends Serializable {
     String getHeaderName();         // 设置 csrf-token 所处的请求头名

     // 当成功验证 csrftoken 后,会把该对象存储到 request 中,供后面的 filter 使用,在此设置其在request 中的属性名
     String getParameterName();      
     
     // csrf-token
     String getToken(); 
 }

 默认实现,DefaultCsrfToken
 

 public interface CsrfTokenRepository {
     CsrfToken generateToken(HttpServletRequest request);

     void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);

     CsrfToken loadToken(HttpServletRequest request);
 }


自定义 csrf 防护

@Getter @Setter @ToString
public class Token {
    private Long id;
    private String identify;
    private String token;
}

// 通过 mybatis 把 token 存到数据库中
public class CustomCsrfTokenRepository implements CsrfTokenRepository {
    @Resource

    private TokenMapper tokenMapper;
    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        String uuid = UUID.randomUUID().toString();
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", uuid);
    }

    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        // 假设每个用户都会发送该头部,来标识自己的身份
        String identify = request.getHeader("X-IDENTIFIER");
        Token tokenBYIdentifier = tokenMapper.findTokenBYIdentifier(identify);
        if (tokenBYIdentifier != null) {
            tokenBYIdentifier.setToken(token.getToken());
            tokenMapper.updateToken(tokenBYIdentifier);
        } else {
            Token token1 = new Token();
            token1.setToken(token.getToken());
            token1.setIdentify(identify);
            tokenMapper.saveToken(token1);
        }
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        String identify = request.getHeader("X-IDENTIFIER");
        if (identify == null) {
            return null;
        }
        Token tokenBYIdentifier = tokenMapper.findTokenBYIdentifier(identify);
        if (tokenBYIdentifier == null) {
            return null;
        }

        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", tokenBYIdentifier.getToken());
    }
}


config:
    @Bean
    public CsrfTokenRepository customCsrfTokenRepository() {
        return new CustomCsrfTokenRepository();
    }

    @Bean
    public SecurityFilterChain config(HttpSecurity http) throws Exception {
        http.csrf(
                c -> {
                    c.csrfTokenRepository(customCsrfTokenRepository());
                }
        );
        http.addFilterAfter(new CsrfTokenLogger(), CsrfFilter.class)
                .authorizeRequests(
                        c -> c.anyRequest().permitAll()
                );
        return http.build();
    }

CORS

注解:
@CrossOrigin({"example.com","example.org"}).


配置:
http.cors(c -> { 
    CorsConfigurationSource source = request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(
            List.of("example.com", "example.org"));
            config.setAllowedMethods(
            List.of("GET", "POST", "PUT", "DELETE"));
            config.setAllowedHeaders(List.of("*"));
            return config;
        };
    c.configurationSource(source);
})

全局方法安全性:预授权和后授权

基于 Spring AOP 实现的方法级权限控制

这些权限验证操作,完全可以在 coroller, service 中实现,但是这样就把业务和授权混杂在一起了,业务就无法复用了!因为权限框架可能随时会变化!

ss 提供通过方法注释来对方法的执行进行权限控制功能,它提供了多种注解类型,以下介绍如何开启,使用这些注解:

  1. 开启全局方法安全性

    // ss 提供了 3 中注解类型: prePostEnabled(常用), securedEnabled,jsr250Enabled 
     @Configuration
     @EnableGlobalMethodSecurity(prePostEnabled = true)  // 开启指定类型的注解
     class ProjectConfig {
         ...
     }
    
  2. 使用 preostEnable 类型注解,其包含 @PreAuthorize, @PostAuthorize 两个注解,类似 assess 权限设置方法,都接收 SpEL 表达式作为参数。

    TIPS: 在 SpEL 中有些内置对象 returnObject,authentication, 以及内置语法 #argName

     @PreAuthorize("hasAnyAuthority('admin', 'market')")
    
     // 内置 returnObject,引用方法的返回值
     @PostAuthorize("returnObject.targetUser.contains(authentication.name)")
    
     // 使用 #argName 语法,引用方法参数
     @PreAuthorize("#name == authentication.principal.username")
    
     /** 
         当权限控制非常复杂时,可以使用 PermissionEvaluator 接口,
         @PreAuthorize(“@PermissionEvaluator实例.hasPermission('')”)
         我们只需将实现该接口的实例注入,然后通过 @实例名 语法在 SpEL 调用即可!
     */ 
    
     // 该接口的目的就是为了把那些复杂权限验证的操作从 SpEL 中分离出来
     public interface PermissionEvaluator extends AopInfrastructureBean {
    
         // 最好在 PostAuthorize 中调用该函数
         // targetDomainObject 接收 returnObject
         // permission 接收操作的权限要求
         boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
    
         // 在 PreAuthorize 中调用该函数
         // targetId 接收请求参数
         // targetType 接收目标类型(我也不知道它的真实作用)
         // permission 接收操作的权限要求
         boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission);
     }
    

全局方法安全性:预过滤和后过滤

假设您不想禁止对某个方法的调用,但您想确保发送给该方法的参数遵循某些规则。或者,在另一种情况下,您希望确保在有人调用该方法后,该方法的调用者仅收到返回值的授权部分。我们将这种功能命名为过滤,并将其分为两类:

  1. Prefiltering: 框架在调用之前过滤参数的值
  2. Postfiltering: 框架在调用之后过滤返回值

TIPS: 只能过滤数组,集合对象!!