spring security 6的知识点总结

一、应用参考我之前的文章
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();
    }

}
相关推荐
李子园的李2 小时前
Java函数式接口——渐进式学习
java
running up2 小时前
Spring Bean生命周期- BeanDefinition 加载与 BeanFactoryPostProcessor BeanPostProcessor
java·后端·spring
222you2 小时前
Java线程的三种创建方式
java·开发语言
脸大是真的好~2 小时前
计算机408基础相关面试题-备用,不推荐
java
云上漫步者2 小时前
深度实战:Rust交叉编译适配OpenHarmony PC——unicode_width完整适配案例
开发语言·后端·rust·harmonyos
小费的部落2 小时前
Excel 在Sheet3中 匹配Sheet1的A列和Sheet2的A列并处理空内容
java·前端·excel
咘噜biu2 小时前
多租户动态数据源插件dynamic-datasource简介
java·mybatisplus·动态数据源·多租户
漫漫求2 小时前
Java内存模型【JMM】、JVM内存模型
java·开发语言·jvm
原来是好奇心2 小时前
深入Spring Boot源码(五):外部化配置与Profile机制深度解析
java·源码·springboot