Spring Security 5+:无需构建 HttpSecurity 即可获取 AuthenticationManager

hot*_*oup 6 java spring-security spring-boot

这里是 Java 11 和 Spring Security 2.7.x。我正在尝试将我的配置基于(已弃用的)WebSecurityConfigurerAdapter的实现升级为使用SecurityFilterChain.

我的实现的重要之处在于我能够定义和配置/连接我自己的:

  • 身份验证过滤器(UsernamePasswordAuthenticationFilter实现)
  • 授权过滤器(BasicAuthenticationFilterimpl)
  • 自定义身份验证错误处理程序(AuthenticationEntryPointimpl)
  • 自定义授权错误处理程序(AccessDeniedHandlerimpl)

这是我基于阅读大量博客和文章的当前设置:

public class ApiAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private ObjectMapper objectMapper;
    private ApiAuthenticationFactory authenticationFactory;
    private TokenService tokenService;

    public ApiAuthenticationFilter(
            AuthenticationManager authenticationManager,
            TokenService tokenService,
            ObjectMapper objectMapper,
            ApiAuthenticationFactory authenticationFactory) {

        super(authenticationManager);
        this.tokenService = tokenService;
        this.objectMapper = objectMapper;
        this.authenticationFactory = authenticationFactory;

        init();

    }

    private void init() {
        setFilterProcessesUrl("/v1/auth/sign-in");
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {

        try {

            SignInRequest signInRequest = objectMapper.readValue(request.getInputStream(), SignInRequest.class);

            Authentication authentication = authenticationFactory
                    .createAuthentication(signInRequest.getEmail(), signInRequest.getPassword());

            // perform authentication and -- if successful -- populate granted authorities
            return authenticationManager.authenticate(authentication);

        } catch (IOException e) {
            throw new BadCredentialsException("malformed sign-in request payload", e);
        }

    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest  request,
            HttpServletResponse response,
            FilterChain filterChain,
            Authentication authentication) {

        // called if-and-only-if the attemptAuthentication method above is successful

        ApiAuthentication apiAuthentication = (ApiAuthentication) authentication;
        TokenPair tokenPair = tokenService.generateTokenPair(apiAuthentication);
        response.setStatus(HttpServletResponse.SC_OK);
        try {
            response.getWriter().write(objectMapper.writeValueAsString(tokenPair));
        } catch (IOException e) {
            throw new ApiServiceException(e);
        }

    }

}

public class ApiAuthorizationFilter extends BasicAuthenticationFilter implements SecurityConstants {

    private ApiAuthenticationFactory authenticationFactory;
    private AuthenticationService authenticationService;
    private String jwtSecret;

    public ApiAuthorizationFilter(
            AuthenticationManager authenticationManager,
            ApiAuthenticationFactory authenticationFactory,
            AuthenticationService authenticationService,
            @Value("${myapp.jwt-secret}") String jwtSecret) {

        super(authenticationManager);
        this.authenticationFactory = authenticationFactory;
        this.authenticationService = authenticationService;
        this.jwtSecret = jwtSecret;

    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws IOException, ServletException {

        String authHeader = request.getHeader(AUTHORIZATION_HEADER);

        // allow the request through if no valid auth header is set; spring security
        // will throw access denied exceptions downstream if the request is for an
        // authenticated url
        if (authHeader == null || !authHeader.startsWith(BEARER_TOKEN_PREFIX)) {
            filterChain.doFilter(request, response);
            return;
        }

        // otherwise an auth header was specified so lets take a look at it and grant access based
        // on what we find
        try {

            DecodedJWT decodedJWT = JwtUtils.verifyToken(authHeader.replace(BEARER_TOKEN_PREFIX, ""), jwtSecret);
            String subject = decodedJWT.getSubject();
            ApiAuthentication authentication = authenticationFactory.createAuthentication(subject, null);

            // TODO: I believe I need to look up granted authorities here and set them

            authenticationService.setCurrentAuthentication(authentication);
            filterChain.doFilter(request, response);

        } catch (JWTVerificationException jwtVerificationEx) {
            throw new AccessDeniedException("access denied", jwtVerificationEx);
        }

    }

}

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfigV2 {

    private boolean securityDebug;

    private ObjectMapper objectMapper;
    private ApiAuthenticationFactory authenticationFactory;
    private TokenService tokenService;

    private AuthenticationService authenticationService;
    private String jwtSecret;
    private ApiUnauthorizedHandler unauthorizedHandler;
    private ApiSignInFailureHandler signInFailureHandler;

    private BCryptPasswordEncoder passwordEncoder;
    private RealmService realmService;

    @Autowired
    public SecurityConfigV2(
            @Value("${spring.security.debug:false}") boolean securityDebug,
            ObjectMapper objectMapper,
            ApiAuthenticationFactory authenticationFactory,
            TokenService tokenService,
            AuthenticationService authenticationService,
            @Value("${myapp.authentication.jwt-secret}") String jwtSecret,
            ApiUnauthorizedHandler unauthorizedHandler,
            ApiSignInFailureHandler signInFailureHandler,
            BCryptPasswordEncoder passwordEncoder,
            RealmService realmService) {
        this.securityDebug = securityDebug;
        this.objectMapper = objectMapper;
        this.authenticationFactory = authenticationFactory;
        this.tokenService = tokenService;
        this.authenticationService = authenticationService;
        this.jwtSecret = jwtSecret;
        this.unauthorizedHandler = unauthorizedHandler;
        this.signInFailureHandler = signInFailureHandler;
        this.passwordEncoder = passwordEncoder;
        this.realmService = realmService;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {

        // build authentication manager
        AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
            .userDetailsService(realmService)
            .passwordEncoder(passwordEncoder)
            .and()
            .build();   // <-- calling it once up here, to get an AuthenticationManager instance

        // enable CSRF
        // TODO: enable once you are ready to provide 'CSRF tokens'
        //  /sf/answers/5295270921/
        httpSecurity.csrf().disable();

        // add CORS filter
        httpSecurity.cors();

        // add anonoymous/permitted paths (that is: what paths are allowed to bypass authentication)
        httpSecurity.authorizeRequests()
            .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            .antMatchers(HttpMethod.GET, "/actuator/health").permitAll()
            .antMatchers(HttpMethod.POST, "/v*/tokens/refresh").permitAll();

        // restrict all other paths and set them to authenticated
        httpSecurity.authorizeRequests().anyRequest().authenticated();

        // add authn + authz filters -- using AuthenticationManager instance here
        httpSecurity.addFilter(apiAuthenticationFilter(authenticationManager));
        httpSecurity.addFilter(apiAuthorizationFilter(authenticationManager));

        // configure exception-handling for authn and authz
        httpSecurity.exceptionHandling().accessDeniedHandler(unauthorizedHandler);
        httpSecurity.exceptionHandling().authenticationEntryPoint(signInFailureHandler);

        // configure stateless http sessions (appropriate for RESTful web services)
        httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // and building it a 2nd time here, to complete the filter
        // but I believe this is what causes the error
        return httpSecurity.build();
    }

    public ApiAuthenticationFilter apiAuthenticationFilter(AuthenticationManager authenticationManager) {

        ApiAuthenticationFilter authenticationFilter = new ApiAuthenticationFilter(
                authenticationManager, tokenService, objectMapper, authenticationFactory);
        return authenticationFilter;

    }

    public ApiAuthorizationFilter apiAuthorizationFilter(AuthenticationManager authenticationManager) {

        ApiAuthorizationFilter authorizationFilter = new ApiAuthorizationFilter(
            authenticationManager,
            authenticationFactory,
            authenticationService,
            jwtSecret);

        return authorizationFilter;

    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.debug(securityDebug)
            .ignoring()
            .antMatchers("/css/**", "/js/**", "/img/**", "/lib/**", "/favicon.ico");
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {

        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        corsConfiguration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));

        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);

        return corsConfigurationSource;

    }

}
Run Code Online (Sandbox Code Playgroud)

当我启动我的应用程序时,我得到:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 
'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': 
Unsatisfied dependency expressed through method 'setFilterChains' parameter 0; nested exception is 
org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'filterChain' defined 
in class path resource [myapp/ws/security/v2/SecurityConfigV2.class]: 
Bean instantiation via factory method failed; nested exception is 
org.springframework.beans.BeanInstantiationException: 
Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: 
Factory method 'filterChain' threw exception; 
nested exception is org.springframework.security.config.annotation.AlreadyBuiltException: 
This object has already been built
Run Code Online (Sandbox Code Playgroud)

谷歌之神说这是因为我打了httpSecurity.build() 两次电话,这是不允许的。然而:

  • 我的 authn 和 authz 过滤器需要一个AuthenticationManager实例;和
  • 似乎获得实例的唯一方法(如果我错了请告诉我!)AuthenticationManager就是运行httpSecurity.build();
  • 我需要 authn/authz 过滤器才能调用httpSecurity.build()

有人能帮我冲过终点线吗?感谢您的任何帮助!

And*_*lov 2

我们正在做类似的事情:

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
  AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
    .userDetailsService(realmService)
    .passwordEncoder(passwordEncoder);

...

  httpSecurity.addFilter(apiAuthenticationFilter(ctx -> httpSecurity.getSharedObject(AuthenticationManager.class)));

...

}

public ApiAuthenticationFilter apiAuthenticationFilter(AuthenticationManagerResolver<?> authenticationManagerResolver) {
  return new ApiAuthenticationFilter(
    authenticationManagerResolver, 
    tokenService, 
    objectMapper, 
    authenticationFactory
  );
}
Run Code Online (Sandbox Code Playgroud)

AuthenticationManager由于仅在运行时需要实例,因此足以在配置阶段将供应商传递给过滤器


UPD。

好吧,现在很清楚为什么您需要引用AuthenticationManager.

第一个选项

如果ApiAuthorizationFilter您实际上不需要扩展BasicAuthenticationFilter- 只需让它spring-security完成它的工作并通过 启用基本身份验证httpSecurity.httpBasic()。因为ApiAuthenticationFilter可以传递AuthenticationManagerResolverSupplier<AuthenticationManager>传递给它:

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
  httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
    .userDetailsService(realmService)
    .passwordEncoder(passwordEncoder);

...

  httpSecurity.addFilter(apiAuthenticationFilter(() -> httpSecurity.getSharedObject(AuthenticationManager.class)));

...

}

public ApiAuthenticationFilter apiAuthenticationFilter(Supplier<AuthenticationManager> authenticationManagerSupplier) {
  return new ApiAuthenticationFilter(
    authenticationManagerSupplier, 
    tokenService, 
    objectMapper, 
    authenticationFactory
  );
}

Run Code Online (Sandbox Code Playgroud)
public class ApiAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private Supplier<AuthenticationManager> authenticationManagerSupplier;
    private ObjectMapper objectMapper;
    private ApiAuthenticationFactory authenticationFactory;
    private TokenService tokenService;

    public ApiAuthenticationFilter(
            Supplier<AuthenticationManager> authenticationManagerSupplier,
            TokenService tokenService,
            ObjectMapper objectMapper,
            ApiAuthenticationFactory authenticationFactory) {

        super();
        this.tokenService = tokenService;
        this.objectMapper = objectMapper;
        this.authenticationFactory = authenticationFactory;

        init();

    }

    private void init() {
        setFilterProcessesUrl("/v1/auth/sign-in");
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {

        try {

            SignInRequest signInRequest = objectMapper.readValue(request.getInputStream(), SignInRequest.class);

            Authentication authentication = authenticationFactory
                    .createAuthentication(signInRequest.getEmail(), signInRequest.getPassword());

            // perform authentication and -- if successful -- populate granted authorities
            return getAuthenticationManager().authenticate(authentication);

        } catch (IOException e) {
            throw new BadCredentialsException("malformed sign-in request payload", e);
        }

    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest  request,
            HttpServletResponse response,
            FilterChain filterChain,
            Authentication authentication) {

        // called if-and-only-if the attemptAuthentication method above is successful

        ApiAuthentication apiAuthentication = (ApiAuthentication) authentication;
        TokenPair tokenPair = tokenService.generateTokenPair(apiAuthentication);
        response.setStatus(HttpServletResponse.SC_OK);
        try {
            response.getWriter().write(objectMapper.writeValueAsString(tokenPair));
        } catch (IOException e) {
            throw new ApiServiceException(e);
        }

    }

    @Override
    protected AuthenticationManager getAuthenticationManager() {
        if (this.authenticationManager == null) {
            this.authenticationManager = authenticationManagerSupplier.get();
        }
        return this.authenticationManager;
    }

}
Run Code Online (Sandbox Code Playgroud)

第二个选项

编写您自己的 实现AbstractHttpConfigurer,应如下所示:

public class ApiSecurityBuilderConfigurer<H extends HttpSecurityBuilder<H>>
        extends AbstractHttpConfigurer<ApiSecurityBuilderConfigurer<H>, H> {

    private TokenService tokenService;

    private ObjectMapper objectMapper;

    private ApiAuthenticationFactory apiAuthenticationFactory;
    
    public ApiSecurityBuilderConfigurer<H> tokenService(TokenService tokenService) {
        this.tokenService = tokenService;
        return this;
    }

    public ApiSecurityBuilderConfigurer<H> objectMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        return this;
    }

    public ApiSecurityBuilderConfigurer<H> apiAuthenticationFactory(ApiAuthenticationFactory ApiAuthenticationFactory) {
        this.ApiAuthenticationFactory = ApiAuthenticationFactory;
        return this;
    }

    @Override
    public void configure(H builder) throws Exception {
        AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
        builder.addFilter(apiAuthenticationFilter(authenticationManager));
    }

    public ApiAuthenticationFilter apiAuthenticationFilter(AuthenticationManager authenticationManager) {
        return new ApiAuthenticationFilter(authenticationManager, tokenService, objectMapper, authenticationFactory);
    }

}
Run Code Online (Sandbox Code Playgroud)
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
  httpSecurity.apply(new ApiSecurityBuilderConfigurer())
    .tokenService(tokenService)
    .objectMapper(objectMapper)
    .apiAuthenticationFactory(apiAuthenticationFactory);

...

}
Run Code Online (Sandbox Code Playgroud)