likes
comments
collection
share

工具使用集| SpringSecurity 之 核心知识

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

spring-security 下

前言

距离上次记录spring-security的内容已有13天了。时间过得好快,这段时间13天里,有的时候都在学习别的内容,或者是断断续续的学习spring-security。我为我之前的嘴硬道歉,shiro安全框架搭建起来确实会比springsecurity来的简单,它没有springsecurity框框条条。不过,springsecurity确实厉害。这这几天了解了核心的类。就可以自己独立搭建一个简单的springsecurity项目了,也可以自定义和扩展一些类了。长话短说,那就开始我们学习之旅吧😊。

Spring Security 生命周期

Authentication 流程:

  1. 当用户尝试登录时,Spring Security 会创建一个 Authentication 对象,其中包括用户输入的用户名和密码等信息。
  2. Authentication 对象传递给 AuthenticationManager 对象,由它来进行验证用户信息。在这一过程中,AuthenticationManager 可以使用一个或多个 AuthenticationProvider 对象,分别对应不同的认证方式。例如,DaoAuthenticationProvider 对象用于验证用户名和密码,JwtAuthenticationProvider 用于验证 JWT Token 等等。
  3. 如果 AuthenticationManager 成功验证了用户信息,它会返回一个填充了用户信息和权限信息的 Authentication 对象。否则,会抛出相应的认证异常,比如 BadCredentialsException
  4. Authentication 对象传递给 SecurityContextHolder 对象,Spring Security 会在整个应用程序中存储该对象。这个对象用于后续的身份验证授权操作,可以通过 SecurityContextHolder 获取。
请求登录
填写用户名密码
验证用户名密码
调用 AuthenticationManager
成功
失败
调用 AuthenticationSuccessHandler
调用 AuthenticationFailureHandler
重定向或返回
重定向或返回
客户端
登录页面
认证请求
生成 Authentication 对象
认证结果
生成登录成功事件
生成登录失败事件
处理登录成功
处理登录失败

客户端向登录页面发起请求,用户填写用户名和密码后,会向认证请求发起请求,认证请求会验证用户名和密码并生成 Authentication 对象,随后调用 AuthenticationManager 进行认证并返回认证结果。如果认证成功,则生成登录成功事件,调用 AuthenticationSuccessHandler 处理登录成功;如果认证失败,则生成登录失败事件,调用 AuthenticationFailureHandler 处理登录失败。最终,处理登录成功或登录失败后,根据处理结果进行重定向或返回。

Authorization 流程:

  1. 当用户尝试访问一个受保护的资源时,Spring Security 会检查该用户是否已经被认证。如果没有,会跳转到登录页面让用户输入用户名和密码。
  2. 如果用户已经被认证,Spring Security 会创建一个 SecurityContext 对象,并存储在 SecurityContextHolder 中。SecurityContext 包含了当前认证用户的 Authentication 对象。
  3. Spring Security 会根据请求中的 URL、HTTP 方法等信息,查找与之匹配的 AccessDecisionManager 对象,并调用它的 decide 方法。AccessDecisionManager 的作用是根据当前用户的权限和请求的资源权限,判断当前用户是否有访问该资源的权限。
  4. AccessDecisionManager 对象会根据预设的策略(例如 AffirmativeBasedUnanimousBasedConsensusBased)来决定用户是否有权限访问该资源。如果没有权限,则抛出相应的 AccessDeniedException 异常。
  5. 如果 AccessDecisionManager 判断当前用户有权限访问该资源,则该用户可以访问该资源。
客户端
Filter Chain
SecurityContextHolder
Authentication 对象
AccessDecisionManager
Access Controller
成功响应
失败响应

Spring Security 核心类

springSecurityFilterChain是Spring Security框架中的一个核心组件,是一个Filter链,用于处理Web应用程序的安全性,包括身份验证(Authentication)和授权(Authorization)。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/images/**");
    }
    //还有另外一种方法来配置静态资源的访问,即通过 mvcMatchers 方法(使用 mvcMatchers 方法可以更加精确地控制哪些 URL 不需要进行身份验证),如下所示
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
            .ignoring()
                .mvcMatchers("/css/**", "/js/**", "/images/**");
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

我们定义了一个 Spring Security 配置类 SecurityConfig,它继承自 WebSecurityConfigurerAdapterSecurityConfig 中包含了多个方法,其中最重要的是 configure(HttpSecurity http) 方法。

configure(HttpSecurity http) 方法中,我们定义了 HTTP 安全策略,指定了哪些 URL 需要哪些角色才能访问,以及如何进行用户登录和注销。

configureGlobal(AuthenticationManagerBuilder auth) 方法中,我们定义了用户信息和密码加密方式。在本例中,我们使用 CustomUserDetailsService 来获取用户信息,使用 BCryptPasswordEncoder 来加密密码。

WebSecurity 配置中,我们忽略了静态资源(css、js、images),这些资源不需要进行身份验证。

Spring Security 5版本以后,推荐使用Java配置方式来配置安全策略,而不是继承WebSecurityConfigurerAdapter类。不再需要显式地扩展WebSecurityConfigurerAdapter,因为默认情况下,Spring Security会自动配置许多常见的安全配置。

但是,如果需要自定义安全策略,仍然可以通过扩展WebSecurityConfigurerAdapter类来实现。此时需要使用@EnableWebSecurity注解来启用Spring Security,然后在扩展的WebSecurityConfigurerAdapter中覆盖configure方法来配置安全策略。

新版本代码示例

@Configuration
@EnableWebSecurity
public class SecurityConfig {

   @Bean
   SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       http.authorizeRequests().anyRequest().authenticated();
       return http.build();
   }
}

该方法来创建 SecurityFilterChain 实例,则不需要显式地继承 WebSecurityConfigurerAdapter

我们创建了一个 SecurityConfig 类,并在该类中创建了一个 securityFilterChain 方法,该方法返回一个 SecurityFilterChain 实例。该方法使用 HttpSecurity 对象配置了授权请求,允许所有请求进行身份验证。对了,虽然该类没有显式地扩展 WebSecurityConfigurerAdapter 类,但是它仍然需要使用 @EnableWebSecurity 注解来启用 Spring Security,并使其能够加载 SecurityConfig 类。

Class & Filter & Handle

UserDetails

UserDetails 是 Spring Security 框架中定义用户认证信息的接口,是一个包含用户认证信息的模型对象,其中包含了用户名、密码、是否启用等认证信息。

UserDetails 接口定义了以下方法:

  • getAuthorities():返回用户授权的权限信息,通常为一个集合;
  • getPassword():返回用户的密码;
  • getUsername():返回用户的用户名;
  • isAccountNonExpired():账户是否未过期;
  • isAccountNonLocked():账户是否未锁定;
  • isCredentialsNonExpired():用户凭证是否未过期;
  • isEnabled():账户是否可用。
示例代码:
public class CustomUserDetails implements UserDetails {

    private String username;
    private String password;
    private boolean enabled;
    private List<GrantedAuthority> authorities;

    public CustomUserDetails(String username, String password, boolean enabled, List<GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.authorities = authorities;
    }

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

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

在上述代码中,我们定义了一个 CustomUserDetails 类实现了 UserDetails 接口,实现了接口中的所有方法,并定义了用户的用户名、密码、是否启用、用户权限等属性。在实际使用中,我们通常需要自定义一个实现了 UserDetailsService 接口的类,用于从数据库或其他持久层中获取用户信息,并将其封装成 UserDetails 对象返回。

UserDetailsService

UserDetailsService 是 Spring Security 提供的一个接口,用于加载用户信息并返回一个 UserDetails 对象,以便在认证过程中使用。其作用是从持久层存储中读取用户信息,并将其转换为 UserDetails 对象,供 Spring Security 使用。

UserDetailsService 接口只有一个方法:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

在实现 UserDetailsService 接口时,我们需要根据自己的数据源从数据库或其他地方获取用户信息,然后将其封装到 UserDetails 对象中并返回。

示例代码:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名从数据库中查询用户信息
     User user = userService.getBaseMapper().selectOne(
                new LambdaQueryWrapper<User>() .eq(User::getUsername,username)
        );

        // 将查询到的用户信息封装成 UserDetails 对象
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
    }
}

我们使用 UserService 对象从数据库中获取了用户信息。然后,我们将用户信息封装成了一个 UserDetails 对象,并返回给 Spring Security 使用。在这个过程中,我们还需要将用户所拥有的权限信息转换为 GrantedAuthority 对象,添加到 UserDetails 对象中。

DaoAuthenticationProvide

Spring Security密码校验主要是在org.springframework.security.authentication.dao.DaoAuthenticationProvider中完成的,该类实现了org.springframework.security.authentication.AuthenticationProvider接口,它使用UserDetailsService来获取用户的详细信息,然后使用PasswordEncoder对用户输入的密码进行校验,从而进行认证。

当用户进行认证时,DaoAuthenticationProvider会调用UserDetailsServiceloadUserByUsername方法获取用户详细信息,这里包括用户名、密码以及用户的角色等信息。随后,DaoAuthenticationProvider会根据UserDetails对象中的密码与用户输入的密码进行比较。如果密码一致,那么用户通过认证,否则认证失败。

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        auth.authenticationProvider(provider);
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .defaultSuccessUrl("/")
            .and()
            .logout()
            .logoutSuccessUrl("/login")
            .and()
            .csrf().disable();
    }
}

上面创建了一个 DaoAuthenticationProvider 对象,并将其配置到 AuthenticationManagerBuilder 中。在 DaoAuthenticationProvider 中,设置了 UserDetailsServicePasswordEncoder,进行密码校验。

自定义示例:

public class CustomDaoAuthenticationProvider extends DaoAuthenticationProvider {
    
    @Autowired
    private UserService userService;

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        UserDetails user = userService.loadUserByUsername(username);
        if (user == null) {
            throw new BadCredentialsException("Invalid username or password");
        }
        return user;
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object credentials = authentication.getCredentials();
        if (credentials == null) {
            throw new BadCredentialsException("Invalid username or password");
        }
        String password = credentials.toString();
        if (!userService.checkPassword(userDetails, password)) {
            throw new BadCredentialsException("Invalid username or password");
        }
    }
}

在这个自定义的 DaoAuthenticationProvider 中,注入了一个 UserService,用来处理用户信息和密码的校验。在 retrieveUser 方法中,通过 UserService 来加载用户信息,并在找不到用户时抛出 BadCredentialsException 异常。在 additionalAuthenticationChecks 方法中,通过取出用户输入的密码并调用 UserServicecheckPassword 方法来校验密码是否正确,如果不正确则抛出 BadCredentialsException 异常。这里的 checkPassword 方法是一个自定义的方法,用来校验密码是否正确。

SecurityContextPersistenceFilter

它用于处理与安全上下文相关的持久化和恢复操作,在 SecurityFilterChain 中,SecurityContextPersistenceFilter 通常作为第一个过滤器运行,用于确保在处理请求时能够恢复与请求相关联的安全上下文。

SecurityContextPersistenceFilter 的作用是从一个持久化的存储位置(如 Session 或者数据库)中恢复 SecurityContext,然后将其放置到当前线程的上下文中。这样,在后续的请求中,SecurityContext 就可以被访问和使用了

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(new SecurityContextPersistenceFilter(), AnonymousAuthenticationFilter.class) //SecurityContextPersistenceFilter 一般应该选择在 AnonymousAuthenticationFilter 过滤器之前,并且不能放置在其他过滤器之前。如果将 SecurityContextPersistenceFilter 放置在 AnonymousAuthenticationFilter 之后,那么在匿名身份被创建之前,SecurityContextPersistenceFilter 将无法获取 SecurityContext,因为此时 SecurityContext 尚未创建。在这种情况下,如果后续的请求需要使用 SecurityContext 进行授权验证,那么将会失败。
            .authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

}

SecurityContextPersistenceFilter 添加到 SecurityFilterChain 中,并指定它的位置是在 AnonymousAuthenticationFilter 之前。这确保了在处理请求之前,安全上下文已经正确地恢复和存储。

这里的 SecurityContextPersistenceFilter 是使用默认构造函数创建的,这意味着它使用默认的 SecurityContextRepository 实现,该实现使用 HttpSessionSecurityContextRepositoryHttpSession 中获取用户数据(存储和恢复安全上下文)。

可以通过自定义 SecurityContextRepository 实现,则可以将其传递给 SecurityContextPersistenceFilter 的构造函数中

自从 Spring Security 5.4 版本开始,SecurityContextPersistenceFilter 已被弃用。替代它的是 SecurityContextRepository 接口和 HttpSessionSecurityContextRepository 实现类。

SecurityContextRepository 负责在当前会话中创建、读取和存储 SecurityContextHttpSessionSecurityContextRepository 则是一个具体的实现类,用于将 SecurityContext 存储在 HttpSession 中。如果你想使用其他存储方式,可以自定义实现 SecurityContextRepository 接口。

示例代码:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
   http
           .securityContext()
           .securityContextRepository(securityContextRepository());

   http    .authorizeRequests()
           .anyRequest().authenticated()
           .and()
           .formLogin()
           .and()
           .httpBasic()
           ;
}

@Bean
public HttpSessionSecurityContextRepository securityContextRepository() {
   return new HttpSessionSecurityContextRepository();
}

}
//创建了一个 HttpSessionSecurityContextRepository 实例,并将其注册为一个 Spring Bean。然后,我们在 configure 方法中使用 securityContextRepository 配置 securityContext。

//这样做可以确保 SecurityContext 被正确地存储和检索,而不需要使用 SecurityContextPersistenceFilter。
SecurityContextHolder

它是 Spring Security 提供的一个线程绑定的上下文对象,用于存储和管理 SecurityContextSecurityContextHolder 中有一个静态的 SecurityContext 对象,可以通过调用 SecurityContextHolder.getContext() 方法获取。

使用方法:

  • SecurityContextPersistenceFilter 中,它通过 SecurityContextHolder.getContext().setAuthentication() 方法,将从持久化存储中获取到的 Authentication 对象设置到 SecurityContext 中,并将其放置到当前线程的上下文中。
  • 在后续的请求中,SecurityContextHolder.getContext().getAuthentication() 方法可以获取到当前用户的 Authentication 对象,以便进行授权验证等操作。
示例代码:
//假设我们有一个 Web 应用程序,需要进行基于角色的授权验证。
//在用户登录后,他们的角色信息将被存储在 SecurityContext 中,并在后续的请求中使用。
//如果用户关闭浏览器并重新打开它,我们希望能够自动恢复他们之前的 SecurityContext,
//以便无需重新登录就可以进行访问。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    

    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http

            .and()
            .addFilterBefore(new SecurityContextPersistenceFilter(
            new HttpSessionSecurityContextRepository()), SecurityContextPersistenceFilter.class)             
            //使用了 HttpSessionSecurityContextRepository
            //它是一个 SecurityContextRepository 实现,用于从 HttpSession 中获取和保存 SecurityContext。

    }
    
}
SecurityContext

它表示了当前线程或进程的安全上下文,即当前实体的身份验证信息和授权信息。通常是一个包含了访问控制上下文信息的数据结构,包括身份验证信息、权限、角色等。

示例代码:
// 获取SecurityContext对象
SecurityContext securityContext = SecurityContextHolder.getContext();

// 设置当前用户的身份验证信息和授权信息
Authentication authentication = new UsernamePasswordAuthenticationToken(user, password, authorities);
securityContext.setAuthentication(authentication);

// 获取当前用户的身份验证信息和授权信息
Authentication authentication = securityContext.getAuthentication();
List<GrantedAuthority> authorities = authentication.getAuthorities();

SecurityContext 通常存储在 SecurityContextHolder 中,而 SecurityContextHolder 则可以在多个过滤器中使用。

SecurityContext 的创建和存储是由 SecurityContextPersistenceFilter 完成的。

注意:

SecurityContextHolder 默认使用线程本地变量来存储 SecurityContext,这意味着每个线程都有自己的 SecurityContext。因此,如果在多线程环境下使用 SecurityContextHolder,需要确保每个线程都使用自己的 SecurityContext。此外,也可以使用不同的SecurityContextHolderStrategy 来自定义 SecurityContextHolder 的存储方式。

线程本地变量(Thread Local Variables,简称TLV)是一种特殊类型的变量,它们的值只能被访问和修改由同一个线程创建的所有代码块,其他线程无法访问或修改这个变量的值。

当我们在多线程应用程序中使用线程本地变量来存储 SecurityContext 时,每个线程都有自己的 SecurityContext,这意味着每个线程都可以在不干扰其他线程的情况下独立维护其自己的安全上下文。

示例代码:

public class SecurityContextHolder {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

public static void setContext(SecurityContext context) {
   contextHolder.set(context);
}

public static SecurityContext getContext() {
   return contextHolder.get();
}

public static void clearContext() {
   contextHolder.remove();
}
}

ThreadLocal 是一个 Java 中的类,它提供了一种实现线程本地存储的机制。线程本地存储是一种机制,它允许我们在一个线程中存储数据,这些数据对于其他线程不可见,也不会被其他线程修改

ThreadLocal 可以看作是一个容器,每个线程都有一个独立的容器实例,它可以存储特定于该线程的数据。线程本地存储对于并发编程非常有用,因为它提供了一种可靠的方式(为每个线程创建一个独立的变量副本来实现多个线程之间的变量是线程安全的)来处理并发访问共享变量的问题,而无需使用同步机制。

示例代码:

public class ThreadLocalExample {
private static ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
   @Override
   protected Integer initialValue() {
       return 0;
   }
};

public static void main(String[] args) {
   Thread t1 = new Thread(() -> {
       for (int i = 0; i < 5; i++) {
           int count = counter.get();
           System.out.println(Thread.currentThread().getName() + " count:" + count);
           counter.set(count + 1);
       }
   });
   Thread t2 = new Thread(() -> {
       for (int i = 0; i < 5; i++) {
           int count = counter.get();
           System.out.println(Thread.currentThread().getName() + " count:" + count);
           counter.set(count + 1);
       }
   });
   t1.start();
   t2.start();
}
}

虽然 ThreadLocal 可以保证多个线程之间的变量是线程安全的,但是如果同一个线程中的多个方法都使用了同一个 ThreadLocal 变量,那么它们仍然可能会相互影响。因此,在使用 ThreadLocal 时,我们需要注意线程内部的同步问题。

可以通过:

1.将 ThreadLocal 变量作为方法参数传递:我们可以将 ThreadLocal 变量作为方法参数传递,这样每个方法都会访问自己的变量副本,而不会相互影响

2.使用 ThreadLocal 的子类 InheritableThreadLocalInheritableThreadLocal 可以让子线程继承父线程的 ThreadLocal 变量副本,从而避免了子线程中的多个方法之间的相互影响

示例代码:

public class MyThreadLocal<T> extends InheritableThreadLocal<T> {
@Override
protected T initialValue() {
   // return the initial value for the ThreadLocal variable
   return null;
}
}

MyThreadLocal<String> threadLocal = new MyThreadLocal<>();
threadLocal.set("hello");

// create a new thread
Thread thread = new Thread(() -> {
String value = threadLocal.get();
// do something with value
});

thread.start();

还需要注意使用 ThreadLocal 存储变量会导致一定的内存开销,因为每个线程都需要创建自己的变量副本。如果我们创建了大量的 ThreadLocal 变量,或者使用 ThreadLocal 存储大量的数据,就可能会导致内存占用过高,从而影响程序的性能和稳定性。

可以通过:

1.使用我们使用弱引用的 ThreadLocal,当一个线程不再使用变量时,它所创建的变量副本就会被自动回收,从而避免了内存泄漏的问题。

2.清理无用的 ThreadLocal:如果我们不再使用某个 ThreadLocal 变量,应该及时将它从内存中清除,以释放相关的内存资源。可以使用 ThreadLocalremove() 方法或者使用 ThreadLocal 的子类 InheritableThreadLocalremove() 方法来清除无用的 ThreadLocal 变量。

AuthenticationException

AuthenticationException是Spring Security中抛出的一个顶级异常,用于表示认证过程中发生的错误。当用户尝试进行认证但是失败时,Spring Security会抛出该异常。

AuthenticationException继承自SecurityException,其主要有以下几个子类:

  • BadCredentialsException:表示用户提供的凭据(例如用户名或密码)无效。
  • LockedException:表示用户账号被锁定,无法进行认证。
  • DisabledException:表示用户账号被禁用,无法进行认证。
  • AccountExpiredException:表示用户账号已过期,无法进行认证。
  • CredentialsExpiredException:表示用户凭据已过期,无法进行认证。

当出现以上情况时,Spring Security会抛出对应的AuthenticationException子类。应用程序可以通过AuthenticationFailureHandler来处理这些异常。通常,AuthenticationFailureHandler将在Web应用程序中重定向到登录页面,并显示适当的错误消息,以便用户能够进行重新尝试认证。

AccessDeniedException

AccessDeniedException是Spring Security中的一种异常类型,通常在用户没有足够的权限来访问资源时抛出。它继承自AuthenticationException,表示用户已经被认证,但由于权限不足,无法访问所请求的资源。

AccessDeniedException通常由AccessDecisionManager抛出,它是决策管理器,用于决定用户是否有权访问所请求的资源。当AccessDecisionManager确定用户没有足够的权限时,就会抛出AccessDeniedException。

AccessDeniedException通常可以通过配置Spring Security的异常处理来进行自定义处理,比如重定向到错误页面或者返回特定的错误信息。可以通过实现AccessDeniedHandler接口来实现自定义处理,该接口包含一个handle方法,该方法将在AccessDeniedException抛出时被调用。

示例代码:
@Slfj
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {



    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {

        log.error("Access denied: {}", accessDeniedException.getMessage());

        response.sendRedirect(request.getContextPath() + "/accessDenied"); // 重定向到自定义的"拒绝访问"页面
    }
}

我们通过@Component注解将CustomAccessDeniedHandler注册到Spring容器中。handle()方法在AccessDeniedException被抛出时被调用,它将重定向到一个自定义的“拒绝访问”页面,该页面的URL为"/accessDenied"。同时,我们还将日志记录器用于记录访问被拒绝的详细信息。

AuthenticationEntryPoint

AuthenticationEntryPoint 是 Spring Security 中用来处理未经过认证的用户请求的策略接口。当用户试图访问需要身份认证的资源时,如果用户没有提供认证信息或者认证信息无效,则会触发 AuthenticationEntryPoint 接口的实现来处理这种情况。

通常情况下,AuthenticationEntryPoint 会将用户请求重定向到登录页面或者返回错误信息提示用户进行身份认证。

在 Spring Security 中,有以下几种常见的 AuthenticationEntryPoint 实现:

  1. LoginUrlAuthenticationEntryPoint:将用户请求重定向到登录页面进行身份认证。
  2. Http403ForbiddenEntryPoint:返回 HTTP 403 状态码(Forbidden),表示用户请求被禁止访问。
  3. Http401UnauthorizedEntryPoint:返回 HTTP 401 状态码(Unauthorized),表示用户未提供有效的身份认证信息。

需要注意的是,对于前后端分离的项目,一般不会使用 LoginUrlAuthenticationEntryPoint,而是使用 Http401UnauthorizedEntryPoint 或者自定义的 AuthenticationEntryPoint,返回 JSON 格式的错误信息。

示例代码:
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter out = response.getWriter();
        out.write("{"status":"error","message":"请先登录"}");
        out.flush();
        out.close();
    }
}

LogoutHandler

LogoutHandler 是 Spring Security 提供的接口之一,用于在用户注销登录时执行一些自定义逻辑,例如清除用户登录状态、清除相关的 Cookie 等。

当用户点击注销按钮或请求 /logout 路径时,Spring Security 会调用 LogoutFilter 过滤器来处理注销逻辑,其中就包括执行注册的 LogoutHandler 实例的 logout() 方法。

示例代码:
public class CustomLogoutHandler implements LogoutHandler {

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // 执行自定义的注销逻辑
        // 比如清除用户登录状态、清除相关的 Cookie 等
    }
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .logout()
            .logoutUrl("/logout")
            .addLogoutHandler(new CustomLogoutHandler())
            .logoutSuccessUrl("/login");
}

在上面的示例中,我们把自定义的 CustomLogoutHandler 注册到了 Spring Security 的注销逻辑中,这样当用户注销登录时,会先执行 CustomLogoutHandlerlogout() 方法,然后再跳转到登录页面。

@Component
public class CustomLogoutHandler implements LogoutHandler {

    @Autowired
    private UserService userService;

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // 获取当前用户的用户名
        String username = authentication.getName();
        // 从数据库中获取该用户的信息
        User user = userService.findUserByUsername(username);
        // 更新用户的最后登录时间
        user.setLastLogoutTime(new Date());
        userService.updateUser(user);
    }
}

在上面的示例代码中,我们实现了LogoutHandler接口,并重写了其中的logout方法。该方法会在用户登出时被调用。在该方法中,我们可以获取当前用户的信息,并进行相应的处理。在本例中,我们从数据库中获取了该用户的信息,并更新了该用户的最后登录时间。这个示例是一个比较简单的示例,实际上,我们可以在这个方法中执行更多的操作,例如清除用户的缓存、记录用户的日志等。

LogoutFilter

LogoutFilter是Spring Security中的一个过滤器(Filter),用于处理用户注销(logout)的请求。当用户点击注销按钮或者通过其他方式触发注销操作时,LogoutFilter会执行一些操作,如清空用户的认证信息、清空Session、重定向到注销成功页面等

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

       @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .permitAll();
    }

    @Bean
    public CustomLogoutFilter customLogoutFilter() {
        LogoutHandler[] handlers = new LogoutHandler[]{
                new SecurityContextLogoutHandler()
        };
        CustomLogoutFilter customLogoutFilter = new CustomLogoutFilter("/login", handlers);
        customLogoutFilter.setFilterProcessesUrl("/logout");
        return customLogoutFilter;
    }
}

//自定义 CustomLogoutFilter
 class CustomLogoutFilter extends LogoutFilter {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public CustomLogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
        super(logoutSuccessUrl, handlers);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws  ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 在注销前可以执行一些自定义的操作,如记录用户注销日志
        String username = request.getRemoteUser();
        logger.info("User {} is logging out.", username);

        super.doFilter(request, response, chain);
    }
}

LogoutFilter被配置在SecurityConfig类中的logoutFilter()方法中。其中,"/logout"指定了用户注销的URL地址,SecurityContextLogoutHandler()用于清空用户的认证信息,setFilterProcessesUrl("/logout")用于指定LogoutFilter要拦截的URL。

OncePerRequestFilter

OncePerRequestFilter 是 Spring Security 提供的一个过滤器抽象类,它的作用是确保在一次请求中只被调用一次,即使过滤器链中包含了其他的过滤器,也不会重复调用。

OncePerRequestFilter 继承了 GenericFilterBean 抽象类,它实现了 doFilter() 方法,并在其中通过 shouldNotFilter() 方法判断当前请求是否应该被过滤。如果返回 true,则表示当前请求不应该被过滤,直接跳过本过滤器;如果返回 false,则表示当前请求应该被过滤,调用 doFilterInternal() 方法执行过滤逻辑。

通常情况下,我们需要继承 OncePerRequestFilter 并实现 doFilterInternal() 方法来自定义过滤器逻辑。

以下是一个自定义的 OncePerRequestFilter

示例代码

public class CustomFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 执行过滤逻辑
        // ...

        // 继续执行过滤器链
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        // 判断当前请求是否应该被过滤
        // ...

        return false;
    }
}

在实现自定义的过滤器逻辑时,我们需要注意以下几点:

  • doFilterInternal() 方法中的过滤逻辑需要根据实际需求来编写。
  • doFilterInternal() 方法中,如果需要让请求继续执行过滤器链,需要调用 filterChain.doFilter(request, response) 方法。
  • shouldNotFilter() 方法用于判断当前请求是否应该被过滤,如果返回 true,则直接跳过本过滤器,不执行 doFilterInternal() 方法;如果返回 false,则执行 doFilterInternal() 方法。
public class CustomHeaderFilter extends OncePerRequestFilter {

    private String headerName;
    private String headerValue;

    public CustomHeaderFilter(String headerName, String headerValue) {
        this.headerName = headerName;
        this.headerValue = headerValue;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        response.setHeader(headerName, headerValue);
        filterChain.doFilter(request, response);
    }
}

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(new CustomHeaderFilter("X-Custom-Header", "SomeValue"), SecurityContextPersistenceFilter.class)
            //...
    }

    //...
}

在上面的配置中,CustomHeaderFilter将被添加到Spring Security过滤器链的SecurityContextPersistenceFilter过滤器之后。每个请求都将具有X-Custom-Header: SomeValue HTTP头。

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 是 Spring Security 提供的一个用于处理用户名密码认证的过滤器,主要用于处理表单登录认证请求。它继承自 AbstractAuthenticationProcessingFilter,并通过覆盖父类的 attemptAuthentication 方法来实现认证逻辑。

在用户提交表单登录认证请求时,UsernamePasswordAuthenticationFilter 会拦截请求,并从请求中获取用户名和密码等认证信息,然后通过调用 AuthenticationManager 对象来进行认证,认证成功则将用户的认证信息存储到 SecurityContextHolder 中,方便后续的权限控制。

示例代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private CustomUserDetailsService userDetailsService;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .usernameParameter("username")  // 自定义表单提交的用户名参数名,默认是username
                .passwordParameter("password")  // 自定义表单提交的密码参数名,默认是password
                .defaultSuccessUrl("/home")     // 登录成功后的默认跳转地址
                .failureUrl("/login?error")     // 登录失败后跳转地址
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .permitAll();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public UsernamePasswordAuthenticationFilter authenticationFilter() throws Exception {
        UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager());
        filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
        filter.setUsernameParameter("username");
        filter.setPasswordParameter("password");
        return filter;
    }
}

我们定义了一个 authenticationFilter() 方法,用于创建一个 UsernamePasswordAuthenticationFilter 实例。需要注意的是,我们需要为这个过滤器设置 AuthenticationManagerAuthenticationSuccessHandlerAuthenticationFailureHandler 等参数,这些参数可以根据实际需求进行定制。

在实际应用中,我们还需要实现自己的 UserDetailsServiceAuthenticationSuccessHandlerAuthenticationFailureHandler 等组件,以便实现更为灵活的认证和授权逻辑。

代码示例1:

//简单的自定义认证过滤器的例子,该过滤器可以用于实现基于 JWT 的认证。我们需要创建一个名为 JwtAuthenticationFilter 的类,该类继承自 OncePerRequestFilter,并实现 doFilterInternal 方法,用于实现具体的认证逻辑。
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;

public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
   this.jwtTokenProvider = jwtTokenProvider;
}

//从 JwtTokenProvider 对象中获取 JWT,然后验证 JWT 是否合法,如果合法则使用 jwtTokenProvider.getAuthentication 方法获取 Authentication 对象,并将其存储到 SecurityContextHolder 中,以便后续的权限控制。

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
   String token = jwtTokenProvider.resolveToken(request);
   if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
       Authentication authentication = jwtTokenProvider.getAuthentication(token);
       SecurityContextHolder.getContext().setAuthentication(authentication);
   }
   filterChain.doFilter(request, response);
}
}

//在 Spring Security 配置中注册这个自定义过滤器。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

private final JwtTokenProvider jwtTokenProvider;

public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
   this.jwtTokenProvider = jwtTokenProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
   http.csrf().disable()
       .authorizeRequests()
       .antMatchers("/api/authenticate").permitAll()
       .anyRequest().authenticated()
       .and()
       .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
       .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
//使用 addFilterBefore 方法将 JwtAuthenticationFilter 过滤器添加到 Spring Security 过滤器链的 UsernamePasswordAuthenticationFilter 之前,并将其配置为 STATELESS 模式,以便禁用 Spring Security 的 Session 机制,从而实现基于 JWT 的认证。同时,我们还需要注入 JwtTokenProvider 对象,以便在 JwtAuthenticationFilter 中使用
// 其他配置...
}

代码示例2:

首先,定义一个类 CustomUsernamePasswordAuthenticationFilter,继承 UsernamePasswordAuthenticationFilter,然后覆盖 attemptAuthentication 方法,自定义验证逻辑:

public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
 
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // 在此处可以添加自定义逻辑,例如验证码校验等等
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private MyUserDetailsService userDetailsService;
 
    @Autowired
    private PasswordEncoder passwordEncoder;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/css/**", "/js/**").permitAll()
                .antMatchers("/", "/home").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler())
                .failureHandler(authenticationFailureHandler())
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/home")
                .addLogoutHandler(logoutHandler())
                .and()
            .csrf().disable()
            .addFilterBefore(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
 
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }
 
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new CustomAuthenticationFailureHandler();
    }
 
    @Bean
    public LogoutHandler logoutHandler() {
        return new CustomLogoutHandler();
    }
 
    @Bean
    public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() throws Exception {
        CustomUsernamePasswordAuthenticationFilter filter = new CustomUsernamePasswordAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(authenticationFailureHandler());
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       
AuthenticationFailureHandler

Spring Security提供了一个接口AuthenticationFailureHandler,它可以用于在用户认证失败时进行自定义处理。当用户提供的用户名和密码无效时,Spring Security会抛出相应的AuthenticationException异常,AuthenticationFailureHandler可以捕获该异常并做出相应的响应。

示例代码
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        String errorMessage = "Invalid username and password.";

        // 根据异常类型设置错误消息
        if (exception instanceof BadCredentialsException) {
            errorMessage = "Invalid username and password.";
        } else if (exception instanceof LockedException) {
            errorMessage = "User account is locked.";
        } else if (exception instanceof DisabledException) {
            errorMessage = "User account is disabled.";
        } else if (exception instanceof AccountExpiredException) {
            errorMessage = "User account has expired.";
        } else if (exception instanceof CredentialsExpiredException) {
            errorMessage = "User credentials have expired.";
        }

        // 设置错误消息到Session中,以便在登录页面中显示
        request.getSession().setAttribute("errorMessage", errorMessage);

        // 重定向到登录页面
        redirectStrategy.sendRedirect(request, response, "/login?error=true");
    }
}

Json 格式返回

@Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {

private ObjectMapper objectMapper;

public RestAuthenticationFailureHandler(ObjectMapper objectMapper) {
   this.objectMapper = objectMapper;
}

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                   AuthenticationException exception) throws IOException, ServletException {
   response.setStatus(HttpStatus.UNAUTHORIZED.value());
   response.setContentType(MediaType.APPLICATION_JSON_VALUE);

   String message;

   if (exception instanceof BadCredentialsException) {
       message = "Invalid username or password";
   } else if (exception instanceof LockedException) {
       message = "User account is locked";
   } else if (exception instanceof DisabledException) {
       message = "User account is disabled";
   } else if (exception instanceof AccountExpiredException) {
       message = "User account has expired";
   } else if (exception instanceof CredentialsExpiredException) {
       message = "User credentials have expired";
   } else {
       message = "Authentication failed";
   }

   Map<String, Object> error = new HashMap<>();
   error.put("message", message);

   String json = objectMapper.writeValueAsString(error);
   response.getWriter().write(json);
}
}

AuthenticationSuccessHandler

AuthenticationSuccessHandler 是 Spring Security 提供的一个接口,用于处理用户认证成功后的操作。它定义了一个 onAuthenticationSuccess() 方法,当用户成功认证后,这个方法将会被调用。

在这个方法中,可以执行任何针对认证成功后的操作,例如:

  • 跳转到指定页面
  • 返回 JSON 数据
  • 发送邮件或短信通知
  • 记录日志
  • 等等
示例代码
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
    //获取用户信息
    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
    //将用户信息传递到欢迎页面
    ModelAndView modelAndView = new ModelAndView("welcome");
    modelAndView.addObject("username", userDetails.getUsername());
    modelAndView.addObject("roles", userDetails.getAuthorities());
    modelAndView.addObject("successMessage", "Login successful.");

    //使用ModelAndView进行页面跳转
    RequestDispatcher dispatcher = request.getRequestDispatcher(modelAndView.getViewName());
    dispatcher.forward(request, response);
}

在该示例中,ModelAndView 的构造函数中传递的参数是欢迎页面的名称,addObject 方法用于将数据传递到页面中,在页面中可以使用 ${} 表达式来读取这些数据。

最后使用 RequestDispatcher 对象进行页面跳转。需要注意的是,使用 RequestDispatcher 进行页面跳转时,必须使用 forward 方法而不是 redirect 方法,因为使用 redirect 方法会导致传递的数据丢失。

前后端分离

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 记录成功登录日志
  log.info("User {} logged in successfully.", authentication.getName());

   // 设置返回的 JSON 数据
   response.setContentType("application/json;charset=UTF-8");
    PrintWriter out = response.getWriter();
     out.write("{"status":"success","message":"Login succeeded."}");
     out.flush();
     out.close();
   }
}
DefaultLoginPageGeneratingFilter

DefaultLoginPageGeneratingFilter 是 Spring Security 中的一个过滤器,用于生成默认的登录页面。当用户尝试访问受保护的资源时,如果用户未经身份验证,则 Spring Security 会重定向到默认的登录页面,该页面由 DefaultLoginPageGeneratingFilter 生成。

在默认情况下,DefaultLoginPageGeneratingFilter 会生成一个简单的 HTML 页面,该页面包含一个表单,其中包含用户名和密码字段,以及一个登录按钮。用户可以使用此表单来输入其凭据以进行身份验证。

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/my-login-page")
                .and()
                .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new CustomLogoutFilter(), LogoutFilter.class)
                .exceptionHandling().accessDeniedPage("/accessDenied")
                .and()
            .csrf().disable();
    }

    @Bean
    public DefaultLoginPageGeneratingFilter loginPageGeneratingFilter() {
        return new DefaultLoginPageGeneratingFilter();
    }

    
}

//自定义实现LoginPageGeneratingFilter 
public class CustomLoginPageGeneratingFilter implements LoginPageGeneratingFilter {

    private String loginPageUrl;

    public CustomLoginPageGeneratingFilter(String loginPageUrl) {
        this.loginPageUrl = loginPageUrl;
    }

    @Override
    public void generateLoginPage(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {

        response.sendRedirect(loginPageUrl);
    }
}



使用 loginPage 方法将登录页面的 URL 设置为 /my-login-page。当用户尝试访问受保护的资源时,如果未经身份验证,则 Spring Security 将重定向到此 URL,以显示自定义的登录页面。注意,我们需要在我们的应用程序中创建名为 my-login-page 的视图(JSP、Thymeleaf 等),以实现自定义登录页面。

如果使用 loginPage 方法指定了自定义登录页面的 URL,DefaultLoginPageGeneratingFilter 将不再生成默认的登录页面。如果我们希望在自定义登录页面中包含一些默认的元素(如 CSRF 令牌),则需要手动将这些元素添加到我们的自定义登录页面中。

DefaultLogoutPageGeneratingFilter

用于生成默认的退出登录页面。

DefaultLogoutPageGeneratingFilter 类似于 DefaultLoginPageGeneratingFilter,但用于生成默认的注销页面。与 DefaultLoginPageGeneratingFilter 类似,我们也可以使用 DefaultLogoutPageGeneratingFilter 生成默认的注销页面,但是我们不能直接配置它来指定自定义注销页面的 URL。同样,我们也可以自定义注销页面生成过滤器,并将其添加到 Spring Security 过滤器链中。

示例代码:
public class CustomLogoutPageGeneratingFilter implements LogoutPageGeneratingFilter {

    private String logoutPageUrl;

    public CustomLogoutPageGeneratingFilter(String logoutPageUrl) {
        this.logoutPageUrl = logoutPageUrl;
    }

    @Override
    public void generateLogoutPage(HttpServletRequest request, HttpServletResponse response) {

        response.sendRedirect(logoutPageUrl);
    }
}

BasicAuthenticationFilter

处理HTTP Basic认证请求。

BasicAuthenticationFilter 是 Spring Security 提供的一种身份认证过滤器,用于对 HTTP Basic 认证进行处理。它通过解析 HTTP 请求头中的 Authorization 字段,提取其中的用户名和密码,并根据配置的认证方式进行验证,从而实现对用户身份的认证。

默认的 BasicAuthenticationFilter 在验证时会先将请求头中的 Basic 认证信息进行解码,然后根据解码后的用户名和密码进行验证。如果用户名和密码正确,就会创建一个 Authentication 对象并保存到 SecurityContextHolder 中,表示用户已经通过认证。

如果用户名和密码不正确,就会返回 HTTP 401 Unauthorized 响应,表示认证失败。如果没有提供 Basic 认证信息,或者认证信息格式不正确,也会返回 HTTP 401 Unauthorized 响应

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
             .antMatchers("admin/**").hasRole("ADMIN")
            .anyRequest().authenticated() //只要认证了即可获取接口
            .and()
            .httpBasic() //开启basic认证
            .and()
            .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}
//在上述代码中,我们启用了 Basic 认证,只有具有 ADMIN 角色的用户才能访问 /admin/** 的路径,其他路径都需要进行身份验证。我们还通过 userDetailsService() 方法配置了认证管理器,并设置了密码编码器

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 这里可以根据 username 从数据库或其他数据源中获取用户信息
        // 下面的代码只是用于演示
        if ("admin".equals(username)) {
            return new User("admin", "$2a$10$FAEeAXAHv6lKHdEdCcrVwO8.Z0HzwT1JXXOFlnltfKh9XWhsdQW92", AuthorityUtils.createAuthorityList("ADMIN")); //原密码 mysecretpassword
        } else {
            throw new UsernameNotFoundException("User not found");
        }
    }
}
//需要实现自己的 UserDetailsService 接口,用于获取用户信息:


//我们需要在控制器中添加一个受保护的资源,用于测试是否能够通过 Basic 认证进行访问
@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, world!";
    }

    @GetMapping("/admin")
    public String admin() {
        return "Hello, admin!";
    }
}
//可以使用postman在请求头中添加 Authorization : Basic dXNlcjpteXNlY3JldHBhc3N3b3Jk 这一段,请求接口复现

也可以通过自定义来实现

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomUserDetailsService customUserDetailsService;

@Override
protected void configure(HttpSecurity http) throws Exception {
   http.csrf().disable()
           .authorizeRequests()
           .anyRequest().authenticated()
           .and()
           .addFilter(new CustomBasicAuthenticationFilter(authenticationManager()))
           .sessionManagement()
           .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
}

public class CustomBasicAuthenticationFilter extends BasicAuthenticationFilter {

   public CustomBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
       super(authenticationManager);
   }

   @Override
   protected void onSuccessfulAuthentication(HttpServletRequest request,
                                             HttpServletResponse response,
                                             Authentication authResult) throws IOException {
       super.onSuccessfulAuthentication(request, response, authResult);

       // do custom logic here after authentication succeeds
   }

   @Override
   protected void onUnsuccessfulAuthentication(HttpServletRequest request,
                                               HttpServletResponse response,
                                               AuthenticationException failed) throws IOException {
       super.onUnsuccessfulAuthentication(request, response, failed);

       // do custom logic here after authentication fails
   }

   @Override
   protected String extractUsername(HttpServletRequest request) {
       // extract username from request
       String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
       if (authorizationHeader != null && authorizationHeader.startsWith("Basic ")) {
           String base64Credentials = authorizationHeader.substring(6);
           String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8);
           return credentials.split(":", 2)[0];
       }
       return null;
   }

   @Override
   protected String extractPassword(HttpServletRequest request) {
       // extract password from request
       String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
       if (authorizationHeader != null && authorizationHeader.startsWith("Basic ")) {
           String base64Credentials = authorizationHeader.substring(6);
           String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8);
           return credentials.split(":", 2)[1];
       }
       return null;
   }
}
}

我们自定义了 CustomBasicAuthenticationFilter 类来扩展 BasicAuthenticationFilter。我们重写了 onSuccessfulAuthentication()onUnsuccessfulAuthentication() 方法来添加自己的逻辑,以及 extractUsername()extractPassword() 方法来从请求中提取用户名和密码。

configure() 方法中,我们添加了我们的自定义过滤器 CustomBasicAuthenticationFilter。此外,我们还使用了自定义的 UserDetailsService 和密码编码器。

注意:

  1. 安全性:BasicAuthenticationFilter 只是对 HTTP Basic 认证进行了处理,因此在实际应用中需要配合其他安全措施来确保应用程序的安全性,如 HTTPS、CSRF 防御等。
  2. 认证方式:BasicAuthenticationFilter 验证的是 HTTP Basic 认证方式,该认证方式的安全性相对较低,建议使用更加安全的认证方式,如 OAuth2 认证、JWT 认证等。
  3. 用户名和密码管理:在使用 BasicAuthenticationFilter 进行认证时,需要管理用户名和密码的存储、加密和更新等操作。建议将用户名和密码存储在数据库或其他安全的存储介质中,并使用密码加密算法对密码进行加密。
  4. 集成其他认证方式:在实际应用中可能需要同时使用多种认证方式,如 HTTP Basic 认证、OAuth2 认证、JWT 认证等。这时需要对 BasicAuthenticationFilter 进行扩展或者自定义认证过滤器来实现多种认证方式的集成。
  5. 自定义错误处理:在认证失败时,BasicAuthenticationFilter 会返回 HTTP 401 Unauthorized 错误码。如果需要自定义错误处理逻辑,可以实现 AuthenticationEntryPoint 接口并注入到 httpBasic 方法中。
RequestCacheAwareFilter

是 Spring Security 中的一个过滤器,用于处理请求缓存,即在重定向后恢复先前的请求。

RequestCacheAwareFilter 过滤器会将当前请求缓存起来,当用户完成身份验证后,会将用户重定向回之前缓存的请求。它的内部实现比较简单,主要是将请求缓存到当前会话的属性中,然后在身份验证完成后,从会话属性中获取缓存的请求并重定向回去。

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .addFilterAfter(new RequestCacheAwareFilter(), SecurityContextPersistenceFilter.class);
    }

}
// RequestCacheAwareFilter 过滤器添加到了 SecurityContextPersistenceFilter 过滤器之后。这样在用户完成身份验证后,就可以将用户重定向回之前缓存的请求。

我们创建了一个继承自 RequestCacheAwareFilter 的自定义过滤器 MyRequestCacheAwareFilter。在该过滤器中,我们重写了 onSuccessfulAuthentication() 方法,用于处理身份验证成功后的重定向逻辑。在该方法中,我们首先通过 getRequestCache() 方法获取当前请求的缓存请求对象 SavedRequest,然后判断缓存请求是否存在。如果存在,则清除当前会话中的认证属性,然后使用 getRedirectStrategy() 方法重定向回缓存请求的 URL;如果不存在,则调用父类的 onSuccessfulAuthentication() 方法,执行默认的身份验证成功逻辑。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
   http.authorizeRequests()
       .anyRequest().authenticated()
       .and()
       .formLogin()
       .and()
       .addFilterBefore(new MyRequestCacheAwareFilter(), LogoutFilter.class);
}

private static class MyRequestCacheAwareFilter extends RequestCacheAwareFilter {
   @Override
   protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException {
       SavedRequest savedRequest = getRequestCache().getRequest(request, response);
       if (savedRequest == null) {
           super.onSuccessfulAuthentication(request, response, authResult);
       } else {
           clearAuthenticationAttributes(request);
           getRedirectStrategy().sendRedirect(request, response, savedRequest.getRedirectUrl());
       }
   }
}
}

SecurityContextHolderAwareRequestFilter

用于将当前请求包装成SecurityContextHolderAwareRequestWrapper对象,以便在请求处理过程中可以方便地获取SecurityContext

当一个HTTP请求到达应用程序时,SecurityContextHolderAwareRequestFilter会检查当前请求是否已经通过身份验证,并将当前请求的HttpServletRequest对象进行封装,以便在处理请求的过程中可以方便地访问当前用户的安全上下文信息。

在实现上,SecurityContextHolderAwareRequestFilter会检查SecurityContextHolder中是否已经存在一个SecurityContext对象,如果不存在,则创建一个空的SecurityContext对象,并将其存储到SecurityContextHolder中。如果SecurityContextHolder中已经存在一个SecurityContext对象,则不做任何操作。

需要注意的是,SecurityContextHolderAwareRequestFilter并不负责身份验证或授权操作,它只是一个过滤器,用于将当前用户的安全上下文信息存储到SecurityContextHolder中,以便在整个请求处理期间可以访问该信息。因此,在使用SecurityContextHolderAwareRequestFilter时,还需要结合其他组件,如AuthenticationManagerAccessDecisionManager等来完成身份验证和授权操作。

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      // 其他的http安全配置
      .addFilterBefore(new SecurityContextHolderAwareRequestFilter(), UsernamePasswordAuthenticationFilter.class);
  }
}

//我们可以其他地方调用 SecurityContextHolder.getContext() 获取信息
@Service
public class MyService {

  public void doSomething() {
    // 获取当前用户的安全上下文信息
    SecurityContext securityContext = SecurityContextHolder.getContext();
    Authentication authentication = securityContext.getAuthentication();

    // 其他业务逻辑
  }
}

通过调用addFilterBefore()方法,将SecurityContextHolderAwareRequestFilter过滤器添加到了Spring Security的过滤器链中。需要注意的是,由于SecurityContextHolderAwareRequestFilter是用来获取当前用户的安全上下文信息的,因此通常情况下应该将它添加到UsernamePasswordAuthenticationFilter过滤器之前。

AnonymousAuthenticationFilter

AnonymousAuthenticationFilter是 Spring Security 框架中的一个过滤器,用于在用户未经过身份验证时为其创建一个匿名身份。

当用户访问一个需要身份验证的资源时,如果用户尚未经过身份验证,则 AnonymousAuthenticationFilter 会在 Spring Security 上下文中创建一个 AnonymousAuthenticationToken 对象,该对象表示一个匿名身份,并将其添加到 SecurityContextHolder 中。

样,在后续的请求中,即使用户尚未经过身份验证,也可以在 Spring Security 上下文中获得一个身份。这对于一些公共资源或匿名访问的资源非常有用。

AnonymousAuthenticationFilter 的默认配置如下:

  • 匿名用户的身份验证令牌的 principal 属性默认为 "anonymousUser"
  • 匿名用户的身份验证令牌的 authorities 属性默认为 ROLE_ANONYMOUS

如果需要更改默认配置,可以通过 AnonymousAuthenticationFilter 的构造函数或相应的配置属性进行更改。例如,可以使用以下代码创建一个具有自定义 principal 的匿名身份:

AnonymousAuthenticationFilter anonymousFilter = new AnonymousAuthenticationFilter("customKey", "customAnonymous", Arrays.asList(new SimpleGrantedAuthority("ROLE_CUSTOM_ANONYMOUS")));

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/public/**").permitAll() // 放行公共接口
                .antMatchers("admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new AnonymousAuthenticationFilter("Anony"), AnonymousAuthenticationFilter.class) // 添加匿名用户过滤器
                .httpBasic()
                .and()
                .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}


@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 这里可以根据 username 从数据库或其他数据源中获取用户信息
        // 下面的代码只是用于演示
        if ("user".equals(username)) {
            return new User("user", "$2a$10$FAEeAXAHv6lKHdEdCcrVwO8.Z0HzwT1JXXOFlnltfKh9XWhsdQW92", AuthorityUtils.createAuthorityList("ADMIN")); 
    //原密码 mysecretpassword 可自行调用springsecurity进行加密
        } else {
            throw new UsernameNotFoundException("User not found");
        }
    }
}




@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, world!";
    }

    @GetMapping("/admin")
    public String admin() {
        return "Hello, admin!";
    }
    
        @GetMapping("/public")
    public String publiC() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        log.info("username:{}",authentication.getPrincipal());
        log.info("password:{}",authentication.getCredentials());
        log.info("roles:{}",authentication.getAuthorities());
        return "Hello, public!";
    }
}

它的底层实现,主要分为以下两个步骤:

1、在请求中创建匿名身份信息

当请求进入 AnonymousAuthenticationFilter 时,该过滤器会检查当前 SecurityContext 中是否已经存在 Authentication 对象。如果已经存在,则说明该请求已经进行过认证,并且该认证信息可以被后续的过滤器和请求处理器使用。

如果 SecurityContext 中不存在 Authentication 对象,则 AnonymousAuthenticationFilter 会根据配置的属性(如 key 和 authorities)创建一个匿名身份信息,并将其保存到 SecurityContextHolder 中。

2、将匿名身份信息与当前请求相关联

创建匿名身份信息后,AnonymousAuthenticationFilter 会将该身份信息与当前请求相关联,以便后续的过滤器和请求处理器可以访问该身份信息。具体实现方式是通过调用 SecurityContextHolder 的 setContext() 方法,将当前请求的 SecurityContext 与线程相关联。

在请求处理完成后,AnonymousAuthenticationFilter 会自动将当前请求的 SecurityContext 从线程中移除,以避免对后续请求的影响。

总体来说,AnonymousAuthenticationFilter 的底层实现比较简单,主要是通过创建匿名身份信息和将其与当前请求相关联来支持匿名用户的访问控制。

SessionManagementFilter

用于管理会话(Session)的创建和失效。

  1. 会话创建与失效的处理

SessionManagementFilter 可以通过配置来决定用户会话的创建和失效方式,如设置会话过期时间、限制同一账户的同时登录等。当用户的会话失效时,该过滤器会自动清除 SecurityContextHolder 中的 Authentication 对象

  1. 会话固定攻击的防范

SessionManagementFilter 可以使用随机生成的 Session ID 防范会话固定攻击,即避免攻击者利用相同的会话 ID 进行会话劫持攻击。该过滤器还提供了一些配置选项,可以控制是否在每个请求上生成新的 Session ID。

  1. 并发会话控制

SessionManagementFilter 可以限制同一个账户同时登录的数量,从而防止账户被盗用。可以通过配置选项 maxSessions 来设置最大的同时登录会话数,并通过 expiredSessionStrategy 接口来实现会话达到最大数限制时的处理策略。

  1. 会话跟踪的控制

SessionManagementFilter 还可以控制客户端是否支持会话跟踪,从而避免跨站点脚本攻击。可以通过设置 SessionTrackingMode 选项来控制客户端是否支持 Cookie 或 URL Rewriting。

示例代码:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/public/**").permitAll() // 放行公共接口
                .antMatchers("admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new AnonymousAuthenticationFilter("anony"), AnonymousAuthenticationFilter.class) // 添加匿名用户过滤器
                .httpBasic()
                .and()
                .csrf().disable();

        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
                .sessionRegistry(sessionRegistry())
                .and()
                .and()
        
                .addFilterBefore(new SessionManagementFilter(httpSessionSecurityContextRepository()), ConcurrentSessionFilter.class);
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public HttpSessionSecurityContextRepository httpSessionSecurityContextRepository() {
        return new HttpSessionSecurityContextRepository();
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
// sessionManagement() 方法来配置会话管理。
// sessionCreationPolicy 为 SessionCreationPolicy.STATELESS,这意味着将不会使用会话(session)来管理用户状态。
//接着,设置 maximumSessions 为 1,这意味着同一个用户最多只能同时拥有一个会话。设置 maxSessionsPreventsLogin 为 true,这意味着如果同一个用户已经拥有一个活动会话,那么任何尝试登录的请求都将被拒绝。
//接着,使用 addFilterBefore() 方法来将 SessionManagementFilter 添加到 Spring Security 过滤器链中。

JSESSIONID,这个值是做什么用的?

在 Spring Security 中,JSESSIONID 是一个用于跟踪用户会话的标识符。它是在用户第一次访问 Web 应用程序时生成的,并被存储在一个 cookie 中。当用户发送后续请求时,该 cookie 将被包含在请求中,以便 Web 服务器可以识别用户的会话。

在 Spring Security 中,JSESSIONID cookie 是用来保存用户认证的会话信息的。例如,如果用户通过用户名和密码进行了身份验证,那么这些信息将被存储在服务器端,并与 JSESSIONID 关联起来。然后,每次用户发送请求时,服务器会检查 JSESSIONID cookie,以查找与该会话相关联的认证信息。

因此,JSESSIONID 对于 Spring Security 来说是非常重要的,因为它允许服务器跟踪用户的会话,并保留用户认证的信息。

ExceptionTranslationFilte

处理安全异常,如访问被拒绝或未经身份验证的请求

在 Spring Security 中,安全过程可以分为多个阶段,每个阶段都可能出现异常。例如,认证阶段可能出现用户名或密码不正确的异常,授权阶段可能出现权限不足的异常等等。ExceptionTranslationFilter 就是用来处理这些异常的。

具体来说,ExceptionTranslationFilter 的主要作用是将 Spring Security 中的异常转换为标准的 HTTP 响应码,以便客户端能够正确地处理异常。例如,当认证失败时,ExceptionTranslationFilter 可以将异常转换为 HTTP 401 响应码,表示未授权访问;当权限不足时,ExceptionTranslationFilter 可以将异常转换为 HTTP 403 响应码,表示禁止访问等等。

示例代码:
   @Autowired
    private UserDetailsService userDetailsService;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/public/**").permitAll() // 放行公共接口
                .antMatchers("admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new AnonymousAuthenticationFilter("anony"), AnonymousAuthenticationFilter.class) // 添加匿名用户过滤器
                .httpBasic()
                .and()
                .csrf().disable();

        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
                .sessionRegistry(sessionRegistry())
                .and()
                .and()
                // other security configurations...
                .addFilterBefore(new SessionManagementFilter(httpSessionSecurityContextRepository()), ConcurrentSessionFilter.class);

        http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler());
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public HttpSessionSecurityContextRepository httpSessionSecurityContextRepository() {
        return new HttpSessionSecurityContextRepository();
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public CustomAccessDeniedHandler customAccessDeniedHandler() {
        return new CustomAccessDeniedHandler();
    }

    //CustomAccessDeniedHandler实现了Spring Security的AccessDeniedHandler接口。当用户试图访问未经授权的资源时,ExceptionTranslationFilter将捕获AccessDeniedException,并将其传递给CustomAccessDeniedHandler来处理。在这个示例中,CustomAccessDeniedHandler会将HTTP响应的状态设置为403 Forbidden,并以JSON格式返回一个错误消息。

    @Component
    public class CustomAccessDeniedHandler implements AccessDeniedHandler {

        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write("{ "error": "Access denied" }");
        }
    }

如果请求没有通过身份验证,将返回401 Unauthorized响应。在这种情况下,ExceptionTranslationFilter并不能捕获该异常,因为该异常是在SecurityContextHolder中发生的,而不是在Spring Security的过滤器链中发生的。

Spring Security中有一个专门用于处理未经身份验证的请求的过滤器,称为BasicAuthenticationFilter。该过滤器会在收到未经身份验证的请求时,发送一个Authenticate头部响应,提示客户端进行身份验证。

FilterSecurityInterceptor

FilterSecurityInterceptorSpring Security中的一个重要过滤器,它主要用于对Web请求进行安全性拦截和授权验证。FilterSecurityInterceptorSpring Security过滤器链中的最后一个过滤器,它会在请求进入Controller之前,对请求进行授权验证。

当一个请求到达FilterSecurityInterceptor时,它会首先获取当前用户的Authentication对象(通常是从SecurityContextHolder中获取的),然后使用该对象来判断当前用户是否有权限访问该请求。如果用户没有足够的权限,则FilterSecurityInterceptor将拒绝该请求,并返回一个403 Forbidden响应。

示例代码:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout();
    }
}

我们将使用http.authorizeRequests()方法来定义哪些URL需要进行授权验证。对于/public/**的URL,我们允许所有用户访问。对于/admin/**的URL,我们要求用户具有"ADMIN"角色才能访问。对于所有其他URL,我们要求用户必须已经进行身份验证才能访问。

需要注意的是,FilterSecurityInterceptor仅用于安全性授权验证,它不处理身份验证、防止攻击等其他安全性问题。在Spring Security中,这些问题通常由其他过滤器来处理,例如UsernamePasswordAuthenticationFilterCsrfFilter等。

用 SpringSecurity

数据库脚本


-- 创建用户表
CREATE TABLE `user` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(50) NOT NULL,
  `password` VARCHAR(100) NOT NULL,
  `email` VARCHAR(100) NOT NULL,
  `phone` VARCHAR(20),
  `enabled` TINYINT(1) NOT NULL DEFAULT '1',
  `last_login_time` DATETIME,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username_UNIQUE` (`username`)
);

-- 创建角色表
CREATE TABLE `role` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL,
  `description` VARCHAR(200),
  PRIMARY KEY (`id`),
  UNIQUE KEY `name_UNIQUE` (`name`)
);

-- 创建权限表
CREATE TABLE `authority` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL,
  `description` VARCHAR(200),
  PRIMARY KEY (`id`),
  UNIQUE KEY `name_UNIQUE` (`name`)
);

-- 创建用户角色表
CREATE TABLE `user_role` (
  `user_id` INT(11) NOT NULL,
  `role_id` INT(11) NOT NULL,
  PRIMARY KEY (`user_id`, `role_id`)
);

-- 创建角色权限表
CREATE TABLE `role_authority` (
  `role_id` INT(11) NOT NULL,
  `authority_id` INT(11) NOT NULL,
  PRIMARY KEY (`role_id`, `authority_id`)
);

-- 添加测试数据
INSERT INTO `user` (`username`, `password`, `email`, `phone`, `enabled`, `last_login_time`)
VALUES ('admin', 'admin123', 'admin.com', '1234567890', 1, NOW()),
       ('user', 'user123', 'user.com', NULL, 1, NOW());

INSERT INTO `role` (`name`, `description`)
VALUES ('ROLE_ADMIN', 'Administrator Role'),
       ('ROLE_USER', 'User Role');

INSERT INTO `authority` (`name`, `description`)
VALUES ('READ', 'Read Permission'),
       ('WRITE', 'Write Permission');

INSERT INTO `user_role` (`user_id`, `role_id`)
VALUES (1, 1),
       (2, 2);

INSERT INTO `role_authority` (`role_id`, `authority_id`)
VALUES (1, 1),
       (1, 2),
       (2, 1);

代码实现

首先,创建一个 Spring Boot 项目,并添加以下依赖:

<dependencies>
    <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>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.26</version>
    </dependency>
</dependencies>

创建一个名为 User 的实体类,表示用户:


package com.qiaose.entity.security;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

import java.security.acl.Permission;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@ApiModel(value="`user`")
@Data
@Builder
@TableName(value = "`user`")
@AllArgsConstructor //全参构造函数
@NoArgsConstructor  //无参构造函数
public class User implements UserDetails {
    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    @ApiModelProperty(value="")
    private Integer id;

    @TableField(value = "username")
    @ApiModelProperty(value="")
    private String username;

    @TableField(value = "`password`")
    @ApiModelProperty(value="")
    private String password;

    @TableField(value = "email")
    @ApiModelProperty(value="")
    private String email;

    @TableField(value = "phone")
    @ApiModelProperty(value="")
    private String phone;

    @TableField(value = "enabled")
    @ApiModelProperty(value="")
    private Byte enabled;

    @TableField(value = "last_login_time")
    @ApiModelProperty(value="")
    private Date lastLoginTime;

    @TableField(exist = false)
    private Set<Role> roles;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> authorities = new HashSet<>();
        for (Role role : roles) {
//            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
            for (Authority authority : role.getAuthorities()) {
                authorities.add(new SimpleGrantedAuthority(authority.getName()));
            }
        }
        return authorities;
    }


    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

创建一个名为 Role 的实体类,表示角色:


package com.qiaose.entity.security;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Set;

@ApiModel(value="`role`")
@Data
@Builder
@TableName(value = "`role`")
@AllArgsConstructor //全参构造函数
@NoArgsConstructor  //无参构造函数
public class Role {
    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    @ApiModelProperty(value="")
    private Integer id;

    @TableField(value = "`name`")
    @ApiModelProperty(value="")
    private String name;

    @TableField(value = "description")
    @ApiModelProperty(value="")
    private String description;

    @TableField(exist = false)
    private Set<Authority> authorities;
}

创建一个名为 Authority 的实体类,表示权限:


package com.qiaose.entity.security;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@ApiModel(value="authority")
@Data
@Builder
@TableName(value = "authority")
@AllArgsConstructor //全参构造函数
@NoArgsConstructor  //无参构造函数
public class Authority {
    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    @ApiModelProperty(value="")
    private Integer id;

    @TableField(value = "`name`")
    @ApiModelProperty(value="")
    private String name;

    @TableField(value = "description")
    @ApiModelProperty(value="")
    private String description;

}

创建一个名为 UserDetailsService 的接口,用于加载用户信息:


package com.qiaose.security.service;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;


public interface UserDetailsService extends org.springframework.security.core.userdetails.UserDetailsService {

    @Override
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}


创建一个名为 UserDetailsServiceImpl 的实现类,实现 UserDetailsService 接口:


package com.qiaose.security.service.serviceImpl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.qiaose.entity.security.*;
import com.qiaose.security.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UserService userService;

    @Autowired
    RoleService roleService;

    @Autowired
    AuthorityService authorityService;

    @Autowired
    UserRoleService userRoleService;

    @Autowired
    RoleAuthorityService roleAuthorityService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("username:{}",username) ;

        User user = userService.getBaseMapper().selectOne(
                new LambdaQueryWrapper<User>() .eq(User::getUsername,username)
        );

        if (null != user) {
            //从中间表 根据用户id 查找 角色id 并转为 roleId 集合
            List<Integer> roleIds = userRoleService.getBaseMapper().selectList( new LambdaQueryWrapper<UserRole>() .eq(UserRole::getUserId,user.getId()) )
                    .stream().map(UserRole::getRoleId).collect(Collectors.toList());

            //根据roleId 查找 角色
            List<Role> role = roleService.getBaseMapper().selectList( new QueryWrapper<Role>().in("id", roleIds) );
            List<Integer> collect = role.stream().map(Role::getId).collect(Collectors.toList());

            ////从中间表 根据角色id 查找 权限id 并转为  authorityId集合
            List<Integer> authorityIds = roleAuthorityService.getBaseMapper().selectList(
                    new LambdaQueryWrapper<RoleAuthority>() .in(RoleAuthority::getRoleId,collect ))
                    .stream().map(RoleAuthority::getAuthorityId).collect(Collectors.toList());

            List<Authority> authorities = authorityService.getBaseMapper().selectList(new QueryWrapper<Authority>().in("id", authorityIds));
            //将角色权限赋予角色
            role.forEach(v -> v.setAuthorities(new HashSet<>(authorities)));
            //将角色赋予给用户
            user.setRoles(new HashSet<>(role));

            return user;
        } else {
            throw new UsernameNotFoundException("User not found");
        }
    }



}

这个类需要添加@Service注解,表示是一个Spring Bean,用于提供用户认证服务。在loadUserByUsername()方法中,我们需要根据用户名查找对应的User对象,如果找到了就创建一个UserDetails对象返回,否则抛出UsernameNotFoundException异常。在这里,我们需要创建一个Spring Security提供的UserDetails对象来保存用户的基本信息和角色信息。

定义安全配置类


package com.qiaose.configuration;

import com.qiaose.componet.CustomAccessDeniedHandler;
import com.qiaose.componet.CustomAuthenticationFailureHandler;
import com.qiaose.componet.CustomAuthenticationSuccessHandler;
import com.qiaose.componet.CustomPermissionEvaluator;
import com.qiaose.security.service.UserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CustomPermissionEvaluator customPermissionEvaluator;

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers( "/test","/login", "/css/**", "/js/**").anonymous()    //将login页面权限解开
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")    //设置登录路径
                .failureHandler( authenticationFailureHandler())
                .successHandler( authenticationSuccessHandler())
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")      //设置登录路径
                .logoutSuccessUrl("/login")     //设置登录后的页面 ---> 通过 controller来控制跳转到具体的页面,并不会之间跳转的页面上
                .permitAll();

        http.httpBasic() //开启basic认证
                .and()
                .csrf().disable();



    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }



    @Bean
    public CustomAuthenticationFailureHandler authenticationFailureHandler(){
        return new CustomAuthenticationFailureHandler();
    }

    @Bean
    public CustomAuthenticationSuccessHandler authenticationSuccessHandler(){
        return new CustomAuthenticationSuccessHandler();
    }

}

这里我们配置了三个URL地址可以匿名访问,其他的URL地址需要进行身份认证后才能访问。其中:

  • antMatchers()方法用于配置要拦截的URL地址
  • permitAll()方法表示允许所有用户访问,不需要进行身份认证
  • authenticated()方法表示只有认证后才能访问
  • formLogin()方法用于配置表单登录的相关信息
  • loginPage()方法表示登录页面的URL地址
  • logout()方法用于配置退出登录的相关信息

等等

编写组件

package com.qiaose.componet;

import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws  IOException {
        String errorMessage = "Invalid username or password.";

        // 根据异常类型设置错误消息
        if (exception instanceof BadCredentialsException) {
            errorMessage = "Invalid username or password.";
        } else if (exception instanceof LockedException) {
            errorMessage = "User account is locked.";
        } else if (exception instanceof DisabledException) {
            errorMessage = "User account is disabled.";
        } else if (exception instanceof AccountExpiredException) {
            errorMessage = "User account has expired.";
        } else if (exception instanceof CredentialsExpiredException) {
            errorMessage = "User credentials have expired.";
        }

        // 设置错误消息到Session中,以便在登录页面中显示
        request.getSession().setAttribute("errorMessage", errorMessage);

        // 重定向到登录页面
        redirectStrategy.sendRedirect(request, response, "/login?error=true");
    }
}

package com.qiaose.componet;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Slf4j
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        //获取用户信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        //将用户信息传递到欢迎页面
        log.info("{}",userDetails);

        redirectStrategy.sendRedirect(request, response, "/welcome");
    }
}

编写Controller

package com.qiaose.controller;

import com.qiaose.entity.security.User;
import com.qiaose.security.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import java.util.Collection;
import java.util.HashMap;

@Slf4j
@Controller
public class HomeController {

    @Autowired
    UserService userService;

    @GetMapping("/")
    public String index() {
        return "welcome";
    }

    @GetMapping("/login")
    public String login() {
        return "login";
    }


    @GetMapping("/welcome")
    public String welcome(Model model, Authentication authentication) {
        String username = authentication.getName();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        User principal = (User) authentication.getPrincipal();

        log.info("{},{},{}",username,authorities,principal);
        model.addAttribute("username", username);
        model.addAttribute("roles", principal.getRoles());
        model.addAttribute("authorities",authorities);
        return "welcome";
    }

}

前端页面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Login Page</title>
</head>
<body>
<h1> 欢迎 Login Page</h1>
<form method="post" action="/login">
  <label>Username:</label>
  <input type="text" name="username"/>
  <br/>
  <label>Password:</label>
  <input type="password" name="password"/>
  <br/>
  <input type="submit" value="Login"/>
</form>
<div th:if="${param.error}">
  <p style="color: red;">Invalid username and password.</p>
</div>
</body>
</html>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Welcome Page</title>
</head>
<body>
<h1>Welcome, <span th:text="${username}"></span>!</h1>
<p>Your roles: <span th:text="${roles}"></span></p>
<p>Your authorities: <span th:text="${authorities}"></span></p>
<p><a href="/logout">Logout</a></p>
</body>
</html>