工具使用集| SpringSecurity 之 核心知识
spring-security 下
前言
距离上次记录spring-security
的内容已有13天了。时间过得好快,这段时间13天里,有的时候都在学习别的内容,或者是断断续续的学习spring-security
。我为我之前的嘴硬道歉,shiro
安全框架搭建起来确实会比springsecurity
来的简单,它没有springsecurity
框框条条。不过,springsecurity
确实厉害。这这几天了解了核心的类。就可以自己独立搭建一个简单的springsecurity
项目了,也可以自定义和扩展一些类了。长话短说,那就开始我们学习之旅吧😊。
Spring Security 生命周期
Authentication 流程:
- 当用户尝试登录时,Spring Security 会创建一个
Authentication
对象,其中包括用户输入的用户名和密码等信息。 Authentication
对象传递给AuthenticationManager
对象,由它来进行验证用户信息。在这一过程中,AuthenticationManager 可以使用一个或多个AuthenticationProvider
对象,分别对应不同的认证方式。例如,DaoAuthenticationProvider
对象用于验证用户名和密码,JwtAuthenticationProvider
用于验证 JWT Token 等等。- 如果
AuthenticationManager
成功验证了用户信息,它会返回一个填充了用户信息和权限信息的Authentication
对象。否则,会抛出相应的认证异常,比如BadCredentialsException
。 Authentication
对象传递给SecurityContextHolder
对象,Spring Security 会在整个应用程序中存储该对象。这个对象用于后续的身份验证授权操作,可以通过SecurityContextHolder
获取。
客户端向登录页面发起请求,用户填写用户名和密码后,会向认证请求发起请求,认证请求会验证用户名和密码并生成 Authentication 对象,随后调用 AuthenticationManager 进行认证并返回认证结果。如果认证成功,则生成登录成功事件,调用 AuthenticationSuccessHandler 处理登录成功;如果认证失败,则生成登录失败事件,调用 AuthenticationFailureHandler 处理登录失败。最终,处理登录成功或登录失败后,根据处理结果进行重定向或返回。
Authorization 流程:
- 当用户尝试访问一个受保护的资源时,Spring Security 会检查该用户是否已经被认证。如果没有,会跳转到登录页面让用户输入用户名和密码。
- 如果用户已经被认证,Spring Security 会创建一个
SecurityContext
对象,并存储在SecurityContextHolder
中。SecurityContext
包含了当前认证用户的Authentication
对象。 - Spring Security 会根据请求中的 URL、HTTP 方法等信息,查找与之匹配的
AccessDecisionManager
对象,并调用它的decide
方法。AccessDecisionManager
的作用是根据当前用户的权限和请求的资源权限,判断当前用户是否有访问该资源的权限。 AccessDecisionManager
对象会根据预设的策略(例如AffirmativeBased
、UnanimousBased
、ConsensusBased
)来决定用户是否有权限访问该资源。如果没有权限,则抛出相应的AccessDeniedException
异常。- 如果
AccessDecisionManager
判断当前用户有权限访问该资源,则该用户可以访问该资源。
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
,它继承自WebSecurityConfigurerAdapter
。SecurityConfig
中包含了多个方法,其中最重要的是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
会调用UserDetailsService
的loadUserByUsername
方法获取用户详细信息,这里包括用户名、密码以及用户的角色等信息。随后,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
中,设置了UserDetailsService
和PasswordEncoder
,进行密码校验。自定义示例:
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
方法中,通过取出用户输入的密码并调用UserService
的checkPassword
方法来校验密码是否正确,如果不正确则抛出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
实现,该实现使用HttpSessionSecurityContextRepository
从HttpSession
中获取用户数据(存储和恢复安全上下文)。可以通过自定义
SecurityContextRepository
实现,则可以将其传递给SecurityContextPersistenceFilter
的构造函数中
⏰
自从 Spring Security 5.4 版本开始,
SecurityContextPersistenceFilter
已被弃用。替代它的是SecurityContextRepository
接口和HttpSessionSecurityContextRepository
实现类。
SecurityContextRepository
负责在当前会话中创建、读取和存储SecurityContext
。HttpSessionSecurityContextRepository
则是一个具体的实现类,用于将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 提供的一个线程绑定的上下文对象,用于存储和管理 SecurityContext
。SecurityContextHolder
中有一个静态的 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
的子类InheritableThreadLocal
:InheritableThreadLocal
可以让子线程继承父线程的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
变量,应该及时将它从内存中清除,以释放相关的内存资源。可以使用ThreadLocal
的remove()
方法或者使用ThreadLocal
的子类InheritableThreadLocal
的remove()
方法来清除无用的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
实现:
LoginUrlAuthenticationEntryPoint
:将用户请求重定向到登录页面进行身份认证。Http403ForbiddenEntryPoint
:返回 HTTP 403 状态码(Forbidden),表示用户请求被禁止访问。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 的注销逻辑中,这样当用户注销登录时,会先执行CustomLogoutHandler
的logout()
方法,然后再跳转到登录页面。@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
实例。需要注意的是,我们需要为这个过滤器设置AuthenticationManager
、AuthenticationSuccessHandler
、AuthenticationFailureHandler
等参数,这些参数可以根据实际需求进行定制。在实际应用中,我们还需要实现自己的
UserDetailsService
、AuthenticationSuccessHandler
、AuthenticationFailureHandler
等组件,以便实现更为灵活的认证和授权逻辑。代码示例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
和密码编码器。注意:
- 安全性:
BasicAuthenticationFilter
只是对 HTTP Basic 认证进行了处理,因此在实际应用中需要配合其他安全措施来确保应用程序的安全性,如 HTTPS、CSRF 防御等。- 认证方式:
BasicAuthenticationFilter
验证的是 HTTP Basic 认证方式,该认证方式的安全性相对较低,建议使用更加安全的认证方式,如 OAuth2 认证、JWT 认证等。- 用户名和密码管理:在使用
BasicAuthenticationFilter
进行认证时,需要管理用户名和密码的存储、加密和更新等操作。建议将用户名和密码存储在数据库或其他安全的存储介质中,并使用密码加密算法对密码进行加密。- 集成其他认证方式:在实际应用中可能需要同时使用多种认证方式,如 HTTP Basic 认证、OAuth2 认证、JWT 认证等。这时需要对
BasicAuthenticationFilter
进行扩展或者自定义认证过滤器来实现多种认证方式的集成。- 自定义错误处理:在认证失败时,
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对象,并将其存储到SecurityContextHolde
r中。如果SecurityContextHolder
中已经存在一个SecurityContext
对象,则不做任何操作。
需要注意的是,SecurityContextHolderAwareRequestFilter
并不负责身份验证或授权操作,它只是一个过滤器,用于将当前用户的安全上下文信息存储到SecurityContextHolder
中,以便在整个请求处理期间可以访问该信息。因此,在使用SecurityContextHolderAwareRequestFilter
时,还需要结合其他组件,如AuthenticationManager
和AccessDecisionManager
等来完成身份验证和授权操作。
示例代码:
@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)的创建和失效。
- 会话创建与失效的处理
SessionManagementFilter 可以通过配置来决定用户会话的创建和失效方式,如设置会话过期时间、限制同一账户的同时登录等。当用户的会话失效时,该过滤器会自动清除 SecurityContextHolder 中的 Authentication 对象
- 会话固定攻击的防范
SessionManagementFilter 可以使用随机生成的 Session ID 防范会话固定攻击,即避免攻击者利用相同的会话 ID 进行会话劫持攻击。该过滤器还提供了一些配置选项,可以控制是否在每个请求上生成新的 Session ID。
- 并发会话控制
SessionManagementFilter 可以限制同一个账户同时登录的数量,从而防止账户被盗用。可以通过配置选项 maxSessions 来设置最大的同时登录会话数,并通过 expiredSessionStrategy 接口来实现会话达到最大数限制时的处理策略。
- 会话跟踪的控制
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
FilterSecurityInterceptor
是Spring Security
中的一个重要过滤器,它主要用于对Web请求进行安全性拦截和授权验证。FilterSecurityInterceptor
是Spring Security
过滤器链中的最后一个过滤器,它会在请求进入Controller之前,对请求进行授权验证。
当一个请求到达FilterSecurityIntercepto
r时,它会首先获取当前用户的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中,这些问题通常由其他过滤器来处理,例如UsernamePasswordAuthenticationFilter
、CsrfFilter
等。
用 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>
转载自:https://juejin.cn/post/7215541697954496567