一、应用参考我之前的文章
spring boot 3集成spring security6
二、管理用户
1、用户的描述类是UserDetails
新建用户如下
UserDetails user= User.withUsername("john")
.password("12345")
.authorities("read").build();
但上述代码一般只用于测试环境
2、用户详情服务UserDetailsService。在进行身份验证时,根据用户名通过该接口的loadUserByUsername方法查找用户,如果找到了再进行身份密码验证。
2.1 测试环境下建立的用户详情服务
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
UserDetails user= User.withUsername("john")
.password("12345")
.authorities("read").build();
userDetailsService.createUser(user);
return userDetailsService;
}
2.2 生产环境一般使用JdbcUserDetailsManager,代码如下
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
String usersByUsernameQuery = "select username,password,enabled from users where enabled=1 and username = ?";
String authsByUserQuery="select username,authority from authorities where username = ?";
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);
jdbcUserDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
return jdbcUserDetailsManager;
}
二、密码管理
1、核心接口是PasswordEncoder,其中该接口的encode方法是对密码进行哈希加密的,不可逆;boolean matches(CharSequence rawPassword, String encodedPassword)方法是验证密码是否匹配的,其中第一个参数是前端传递过来的密码,第二个参数是数据库里面存储用户的正确哈希后的密码。
2、将PasswordEncoder注入bean,及其几种密码方式的介绍,包括PBKDF2,bcrypt强哈希函数,scrypt 哈希函数
//密码编码
@Bean
public PasswordEncoder passwordEncoder() throws NoSuchAlgorithmException {
//方式1 PBKDF2
// return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
//方式2 bcrypt强哈希函数
// BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(4);
// BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(4, SecureRandom.getInstanceStrong());
// return bCryptPasswordEncoder;
//方式3 scrypt 哈希函数
// SCryptPasswordEncoder sCryptPasswordEncoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
// return sCryptPasswordEncoder;
//方式4 多种密码方式
Map<String, PasswordEncoder> encoderMap=new LinkedHashMap<>();
encoderMap.put("bcrypt", new BCryptPasswordEncoder());
encoderMap.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoderMap.put("noop",NoOpPasswordEncoder.getInstance());
return new DelegatingPasswordEncoder("scrypt",encoderMap);
}
3、注意如果使用方式4的话,在数据库或者静态密码中,需要在密码前面加上花括号的前缀,如下图所示

三、实现身份验证
1、核心接口AuthenticationProvider,需要实现该接口,其中Authentication authenticate(Authentication authentication) throws AuthenticationException方法用于身份验证
2、定义CustomAuthenticationProvider类实现该接口
package com.example.serurity.service_config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
// AuthenticationManager
@Resource
@Lazy
private UserDetailsService userDetailsService;
@Resource
@Lazy
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(username);
if(passwordEncoder.matches(password,user.getPassword())){
return new UsernamePasswordAuthenticationToken(username,password,user.getAuthorities());
}else {
throw new BadCredentialsException("密码不正确");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
3、最后将该接口bean对象注入到WebAuthenticationConfig类的HttpSecurity http 对象中去
例如
http..authenticationProvider(customAuthenticationProvider);
4、SecurityContext安全上下文接口,在身份验证后,如果还需要获取用户名或者权限时,可通过安全上下文来获取。工具类为SecurityContextHolder,代码示例如下:
SecurityContext securityContext=SecurityContextHolder.getContext();
String userName = securityContext.getAuthentication().getName();
System.out.println("goodbye:"+userName);
5、关于线程同步的问题
5.1 MODE_THREADLOCAL,是默认的,每单独请求一次,生成一个SecurityContext实例,但如果在该请求中异步线程如@Async时,则不会传递给子线程
5.2 MODE_INHERITABLETHREADLOCAL,和5.1 类似 ,不同的是但请求中有@Async异步时,可将SecurityContext传递到子线程,原理是父线程复制了一份到子线程中去了
5.3 执行方式,通过注入bean来实现,新建SecurityContextConfig类,代码如下
@Configuration
public class SecurityContextConfig {
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_THREADLOCAL);
}
}
5.4 针对自定义线程时 引用SecurityContext实例
5.4.1 使用DelegatingSecurityContextCallable包装线程执行部分,代码如下
Callable<String> task=()->{
SecurityContext securityContext=SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
};
ExecutorService e= Executors.newFixedThreadPool(1);
try {
//对任务进行包装,实现安全上下文的传递
DelegatingSecurityContextCallable<String> delegatingSecurityContextCallable=new DelegatingSecurityContextCallable<>(task);
return e.submit(delegatingSecurityContextCallable).get();
}finally {
e.shutdown();
}
5.4.2 使用DelegatingSecurityContextExecutorService直接包装线程池,代码如下:
Callable<String> task=()->{
SecurityContext securityContext=SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
};
//包装线程池
ExecutorService e=new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(1));
try {
return e.submit(task).get();
}finally {
e.shutdown();
}
四、关于角色与权限的访问
1、在配置@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception 方法中配置
2、权限,方法有hasAuthority,hasAnyAuthority和access,其中hasAuthority代表当该鉴权用户拥有的权限包含该方法参数的权限时,hasAnyAuthority代表当该鉴权用户有其中的一种权限时,access为复杂SPEL表达式。其中用户的权限要定义在数据库表中
3、权限代码
3.1 hasAuthority方法
http.authorizeHttpRequests(auth->{
auth.anyRequest().hasAuthority("READ");
});
3.2 hasAnyAuthority方法
http.authorizeHttpRequests(auth->{
auth.anyRequest().hasAnyAuthority("READ","WRITE");
});
3.3 access表达式
//access的自定义表达式,当包含读权限 无写权限时
AuthorizationManager<RequestAuthorizationContext> authorizationManager=new WebExpressionAuthorizationManager("""
hasAuthority("READ") && !hasAuthority("WRITE")
""");
http.authorizeHttpRequests(auth->{
auth.anyRequest().access(authorizationManager);
});
//配置允许12点之后进行访问
AuthorizationManager<RequestAuthorizationContext> authorizationManager=new WebExpressionAuthorizationManager("""
T(java.time.LocalTime).now().isAfter(T(java.time.LocalTime).of(12,0))
""");
http.authorizeHttpRequests(auth->{
auth.anyRequest().access(authorizationManager);
});
4、角色,类似权限,注意在数据库中角色具有ROLE_前缀,方法有hasRole、hasAnyRole,并且在这俩方法中角色名称没有ROLE_前缀,ROLE_前缀前缀只有用户权限 角色的数据库里面有
测试代码如下
4.1 hasRole方法 鉴权用户包含该角色
http.authorizeHttpRequests(auth->{
auth.anyRequest().hasRole("ADMIN");
});
4.2 hasAnyRole方法,鉴权用户包含任一一种角色
http.authorizeHttpRequests(auth->{
auth.anyRequest().hasAnyRole("ADMIN", "MANAGER");
});
4.3 access表达式
//access的自定义表达式
AuthorizationManager<RequestAuthorizationContext> authorizationManager=new WebExpressionAuthorizationManager("""
hasRole("ADMIN") && !hasRole("MANAGER")
""");
http.authorizeHttpRequests(auth->{
auth.anyRequest().access(authorizationManager);
});
五、应用限制
核心方法requestMatchers方法,可匹配一个或者多个路径
1、匹配接口路径示例
http.authorizeHttpRequests(auth->{auth
.requestMatchers("/hello","/test").hasRole("ADMIN")
.anyRequest().authenticated();
});
2、关于的使用,当使用 *时,代码匹配下面任一层级的路径,当使用时代表匹配下面的一个层级的路径,示例如下:
http.authorizeHttpRequests(auth->{auth
.requestMatchers("/hello/**","/test/*").hasRole("ADMIN")
.anyRequest().authenticated();
});
3、关于requestMatchers(HttpMethod method, String... patterns)方法需要传入两个参数,一个是HTTP的方式,一个是路径,不常用
4、关于正则表达式匹配,需要新建一个RegexRequestMatcher对象
//正则表达式
RequestMatcher regexRequestMatcher = new RegexRequestMatcher("^/api/v1/.*",null);
http.authorizeHttpRequests(auth->{auth
.requestMatchers(regexRequestMatcher).hasRole("ADMIN")
.anyRequest().authenticated();
});
六、过滤器
1、基础接口Filter、OncePerRequestFilter,这俩接口的区别在于后者过滤器保证只调用一次,前者可能会有多次。过滤器用于在已有的过滤器链中增加自定义过滤器
2、新建RequestValidationFilter和AuthenticationLoggingFilter测试过滤器,代码如下
package com.example.serurity.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class AuthenticationLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestId=request.getHeader("Request-ID");
System.out.println("过滤器执行之后");
System.out.println("请求ID:"+requestId);
filterChain.doFilter(request, response);
}
}
package com.example.serurity.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class RequestValidationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestId=request.getHeader("Request-ID");
if(requestId==null || requestId.isBlank()){
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
System.out.println("错误");
return;
}
filterChain.doFilter(request, response);
}
}
3、注入过滤器
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(new RequestValidationFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(new AuthenticationLoggingFilter(), BasicAuthenticationFilter.class);
}
七、解决跨域
1、新建全局跨域配置类CorsConfig
package com.example.serurity.config;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.List;
public class CorsConfig implements CorsConfigurationSource {
//全局跨域配置
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:4200"));
//config.addAllowedOrigin("*");
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
return config;
}
}
2、注入到public SecurityFilterChain filterChain(HttpSecurity http)
http.cors(v->{
v.configurationSource(new CorsConfig());
});
八、全部整体WebAuthenticationConfig配置
package com.example.serurity.config;
import com.example.serurity.filter.AuthenticationLoggingFilter;
import com.example.serurity.filter.CsrfTokenLogger;
import com.example.serurity.filter.RequestValidationFilter;
import com.example.serurity.handler.CustomCsrfTokenRepository;
import com.example.serurity.handler.CustomEntryPoint;
import com.example.serurity.service_config.CustomAuthenticationProvider;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class WebAuthenticationConfig {
@Resource
@Lazy
private CustomAuthenticationProvider customAuthenticationProvider;
//授权
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// http.addFilterBefore(new RequestValidationFilter(), BasicAuthenticationFilter.class)
// .addFilterAfter(new AuthenticationLoggingFilter(), BasicAuthenticationFilter.class);
// //正则表达式
// RequestMatcher regexRequestMatcher = new RegexRequestMatcher("^/api/v1/.*",null);
// http.authorizeHttpRequests(auth->{auth
// .requestMatchers(regexRequestMatcher).hasRole("ADMIN")
// .anyRequest().authenticated();
// });
// //配置允许12点之后进行访问
// AuthorizationManager<RequestAuthorizationContext> authorizationManager=new WebExpressionAuthorizationManager("""
// T(java.time.LocalTime).now().isAfter(T(java.time.LocalTime).of(12,0))
// """);
// http.addFilterBefore(new RequestValidationFilter(), BasicAuthenticationFilter.class)
// .addFilterAfter(new AuthenticationLoggingFilter(), BasicAuthenticationFilter.class);
// http.addFilterAfter(new CsrfTokenLogger(), CsrfFilter.class);
http.httpBasic(c->{
c.realmName("OTHER");
c.authenticationEntryPoint(new CustomEntryPoint());
}).authorizeHttpRequests(auth -> auth
.requestMatchers("/hello").hasRole("ADMIN")
.anyRequest().authenticated()
// .requestMatchers(HttpMethod.POST,"/hello/*").hasRole("ADMIN")
// .requestMatchers(regexRequestMatcher).hasRole("ADMIN")
).authenticationProvider(customAuthenticationProvider);
http.csrf(v->{
v.csrfTokenRepository(new CustomCsrfTokenRepository());
v.ignoringRequestMatchers("/helloPost");
});
//跨域
http.cors(c->{
c.configurationSource(new CorsConfig());
});
return http.build();
}
}