Spring Security 在没有 WebSecurityConfigurerAdapter 的情况下公开 AuthenticationManager

Yan*_*n39 42 java spring spring-security spring-boot

我正在尝试传入 Spring Boot 2.7.0-SNAPSHOT,它使用 Spring Security 5.7.0,它不推荐使用WebSecurityConfigurerAdapter.

我阅读了这篇博文,但我不确定如何AuthenticationManager向我的 JWT 授权过滤器公开默认实现。

旧的WebSecurityConfig,使用WebSecurityConfigurerAdapter(工作正常):

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    protected AuthenticationManager getAuthenticationManager() throws Exception {
        return authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // JWT authorization filter
                .addFilter(new JWTAuthorizationFilter(getAuthenticationManager(), jwtTokenUtils))
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

}
Run Code Online (Sandbox Code Playgroud)

新的WebSecurityConfig

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        final AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // JWT authorization filter
                .addFilter(new JWTAuthorizationFilter(authenticationManager, jwtTokenUtils))
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}
Run Code Online (Sandbox Code Playgroud)

如你所见,我不再有AuthenticationManager暴露的豆子了。我无法从WebSecurityConfigurerAdapter. 所以我尝试直接从HttpSecurity方法中获取它filterChain,这样我就可以将它直接传递给我的 JWT 过滤器。

但我仍然需要一个AuthenticationManager豆子来暴露我的JWTAuthorizationFilter

com.example.config.security.JWTAuthorizationFilter 中构造函数的参数 0 需要类型为“org.springframework.security.authentication.AuthenticationManager”的 bean,但无法找到。

我怎样才能揭露它?

这是 JWT 授权过滤器(检查令牌并对用户进行身份验证,我有一个UserDetailsService在数据库中进行凭据检查的自定义过滤器):

@Component
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    private final JWTTokenUtils jwtTokenUtils;

    public JWTAuthorizationFilter(AuthenticationManager authManager, JWTTokenUtils jwtTokenUtils) {
        super(authManager);
        this.jwtTokenUtils = jwtTokenUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {

        // retrieve request authorization header
        final String authorizationHeader = req.getHeader("Authorization");

        // authorization header must be set and start with Bearer
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {

            // decode JWT token
            final JWTTokenPayload jwtTokenPayload = jwtTokenUtils.decodeToken(authorizationHeader);

            // if user e-mail has been retrieved correctly from the token and if user is not already authenticated
            if (jwtTokenPayload.getEmail() != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                // authenticate user
                final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(jwtTokenPayload.getEmail(), null, Collections.singletonList(jwtTokenPayload.getRole()));

                // set authentication in security context holder
                SecurityContextHolder.getContext().setAuthentication(authentication);

            } else {
                log.error("Valid token contains no user info");
            }
        }
        // no token specified
        else {
            res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        }

        // pass request down the chain, except for OPTIONS requests
        if (!"OPTIONS".equalsIgnoreCase(req.getMethod())) {
            chain.doFilter(req, res);
        }

    }

}
Run Code Online (Sandbox Code Playgroud)

编辑 :

我意识到我可以使用本期authenticationManager提供的方法设法在我的 JWT 过滤器中获取,但我仍然需要在全局范围内公开,因为我在控制器中也需要它。AuthenticationManager

authenticationManager这是需要注入的身份验证控制器:

@RestController
@CrossOrigin
@Component
public class AuthController {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Autowired
    private AuthenticationManager authenticationManager;

    @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> authenticate(@RequestBody JWTRequest userRequest) {

        // try to authenticate user using specified credentials
        final Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userRequest.getEmail(), userRequest.getPassword()));

        // if authentication succeeded and is not anonymous
        if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {

            // set authentication in security context holder
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // get authorities, we should have only one role per member so simply get the first one
            final GrantedAuthority grantedAuthority = authentication.getAuthorities().iterator().next();

            // generate new JWT token
            final String jwtToken = jwtTokenUtils.generateToken(authentication.getPrincipal(), grantedAuthority);

            // return response containing the JWT token
            return ResponseEntity.ok(new JWTResponse(jwtToken));
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

    }

}
Run Code Online (Sandbox Code Playgroud)

Yan*_*n39 37

当地的AuthenticationManager

能够获取并将AuthenticationManager(您无法再从已弃用的) 传递到过滤器的解决方案WebSecurityConfigurerAdapter是拥有一个专门的配置器来负责添加过滤器。(这受到此处提供的解决方案的启发。编辑:现在正式在文档中)。

创建自定义 HTTP 配置器:

@Component
public class JWTHttpConfigurer extends AbstractHttpConfigurer<JWTHttpConfigurer, HttpSecurity> {

    private final JWTTokenUtils jwtTokenUtils;

    public JWTHttpConfigurer(JWTTokenUtils jwtTokenUtils) {
        this.jwtTokenUtils = jwtTokenUtils;
    }

    @Override
    public void configure(HttpSecurity http) {
        final AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        http.antMatcher("/graphql").addFilter(new JWTAuthorizationFilter(authenticationManager, jwtTokenUtils));
    }

}
Run Code Online (Sandbox Code Playgroud)

然后只需将其应用到安全配置中:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // JWT authorization filter
                .apply(new JWTHttpConfigurer(jwtTokenUtils)).and()
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}
Run Code Online (Sandbox Code Playgroud)

全球的AuthenticationManager

在某些情况下,您需要在全局范围内公开身份验证管理器,以便它可以在应用程序中的任何位置使用。

在 Spring 上下文中拥有bean 的一个AuthenticationManager解决方案是从导出身份验证配置中获取它AuthenticationConfiguration(归功于下面 Andrei Daneliuc 的回答):

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
}
Run Code Online (Sandbox Code Playgroud)

然后,如果您需要在过滤器链中检索它,则可以使用authenticationManager(http.getSharedObject(AuthenticationConfiguration.class)).

所以整个安全配置将是:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // match GraphQL endpoint
                .antMatcher("/graphql")
                // add JWT authorization filter
                .addFilter(new JWTAuthorizationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class)), jwtTokenUtils))
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}
Run Code Online (Sandbox Code Playgroud)

全局公开身份验证管理器的另一个解决方案是使用 custom AuthenticationManager,作为整个应用程序可用的 bean,它与我们的示例中的默认实现完全相同DaoAuthenticationProvider(即使用 customUserDetailsService从数据库获取用户详细信息,使用配置的密码验证PasswordEncoder,然后返回 aUsernamePasswordAuthenticationToken以显示Authentication):

@Component
public class CustomAuthenticationManager implements AuthenticationManager {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

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

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        final UserDetails userDetail = customUserDetailsService.loadUserByUsername(authentication.getName());
        if (!passwordEncoder().matches(authentication.getCredentials().toString(), userDetail.getPassword())) {
            throw new BadCredentialsException("Wrong password");
        }
        return new UsernamePasswordAuthenticationToken(userDetail.getUsername(), userDetail.getPassword(), userDetail.getAuthorities());
    }

}
Run Code Online (Sandbox Code Playgroud)

这样您就可以在添加过滤器时在安全配置中使用它:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // match GraphQL endpoint
                .antMatcher("/graphql")
                // add JWT authorization filter
                .addFilter(new JWTAuthorizationFilter(new CustomAuthenticationManager(), jwtTokenUtils))
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}
Run Code Online (Sandbox Code Playgroud)

它可以注入应用程序中的任何其他位置,即控制器中:

@RestController
@CrossOrigin
@Component
public class AuthController {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Autowired
    private CustomAuthenticationManager authenticationManager;

    @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> authenticate(@RequestBody JWTRequest userRequest) {

        // try to authenticate user using specified credentials
        final Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userRequest.getEmail(), userRequest.getPassword()));

        // if authentication succeeded and is not anonymous
        if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {

            // set authentication in security context holder
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // get authorities, we should have only one role per member so simply get the first one
            final GrantedAuthority grantedAuthority = authentication.getAuthorities().iterator().next();

            // generate new JWT token
            final String jwtToken = jwtTokenUtils.generateToken(authentication.getPrincipal(), grantedAuthority);

            // return response containing the JWT token
            return ResponseEntity.ok(new JWTResponse(jwtToken));
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

    }

}
Run Code Online (Sandbox Code Playgroud)

请注意,您可能还想使用自定义, 在引发AuthenticationEntryPoint时返回 401 而不是 500 。BadCredentialsException


小智 23

如果您希望 AuthenticationManager bean 位于 spring 上下文中,可以使用以下解决方案。

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
     return authenticationConfiguration.getAuthenticationManager();
}
Run Code Online (Sandbox Code Playgroud)

这种方法已经解决了我的问题,你可以在任何需要的地方注入 AuthenticationManager 。

  • 非常感谢您的回答,这并没有直接解决问题,因为我仍然需要通过其构造函数将身份验证管理器传递给 JWT 过滤器,它无法自动装配(在“super”调用中需要)。但我设法通过“authenticationManager(http.getSharedObject(AuthenticationConfiguration.class))”从“filterChain”方法获取,它似乎工作正常。我接受您的答案,并保留其他答案以获取更多详细信息和替代方案。最好的! (2认同)