有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java如何解决与Spring安全相关的循环依赖关系?

我有一个Spring Boot/Spring Security应用程序,我想在其中使用JWT auth和Spring Security提供的JdbcUserDetailsManager

以下是我目前掌握的情况:

示例应用程序。java

package org.example.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

JwtTokenFilter。java(使用UserDetailsService)

package org.example.app.auth;

import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

import static org.springframework.util.ObjectUtils.isEmpty;

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    private final JwtTokenUtil jwtTokenUtil;
    private final UserDetailsService userDetailsService;

    public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, JdbcUserDetailsManager jdbcUserDetailsManager) {
        this.jwtTokenUtil = jwtTokenUtil;
        this.userDetailsService = jdbcUserDetailsManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        // Get authorization header and validate
        final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (isEmpty(header) || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        // Get jwt token and validate
        final String token = header.split(" ")[1].trim();
        if (!jwtTokenUtil.validate(token)) {
            chain.doFilter(request, response);
            return;
        }

        // Get user identity and set it on the spring security context
        String username = jwtTokenUtil.getUsername(token);
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        UsernamePasswordAuthenticationToken
                authentication = new UsernamePasswordAuthenticationToken(
                userDetails, null,
                userDetails == null ?
                        List.of() : userDetails.getAuthorities()
        );

        authentication.setDetails(
                new WebAuthenticationDetailsSource().buildDetails(request)
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }

}

JwtTokenUtil。java(对于本例来说并不重要)

package org.example.app.auth;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

import static java.lang.String.format;

@Component
public class JwtTokenUtil {
    Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);

    private final String jwtSecret = "aYEFtKMCn0xCg5caH1nnFuHfdAB0lBOvdonxq80VqOGNnG6QcyagXWOLrUdqJnzexUXYceMhGNFNYsA" +
            "6rblSibUEh0yRsJ3XO1um1iMdoekOPzj4zKlokcu9TxTbz5DHYVLkqX3q9JrLgbLZFXD8ynOHfRHRL5Ge64iFZBVm9X517fwZrNornOm" +
            "K2L7hUz10SgZpxAz6";
    private final String jwtIssuer = "example.org";

    public String generateAccessToken(User user) {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        Key key = Keys.hmacShaKeyFor(keyBytes);
        return Jwts.builder()
                .setSubject(format("%s", user.getUsername()))
                .setIssuer(jwtIssuer)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 1000)) // 1 hour
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();
    }

    public String getUsername(String token) {
        JwtParser jwtParser = Jwts.parserBuilder()
                .setSigningKey(jwtSecret).build();
        Claims claims = jwtParser
                .parseClaimsJws(token)
                .getBody();

        return claims.getSubject().split(",")[1];
    }

    public boolean validate(String token) {
        try {
            JwtParser jwtParser = Jwts.parserBuilder()
                    .setSigningKey(jwtSecret).build();
            jwtParser.parseClaimsJws(token);
            return true;
        } catch (SecurityException ex) {
            logger.error("Invalid JWT signature - {}", ex.getMessage());
        } catch (MalformedJwtException ex) {
            logger.error("Invalid JWT token - {}", ex.getMessage());
        } catch (ExpiredJwtException ex) {
            logger.error("Expired JWT token - {}", ex.getMessage());
        } catch (UnsupportedJwtException ex) {
            logger.error("Unsupported JWT token - {}", ex.getMessage());
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims string is empty - {}", ex.getMessage());
        }
        return false;
    }

}

安全配置。java(使用JwtTokenFilter)

package org.example.app.config;

import org.example.app.auth.JwtTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.sql.DataSource;

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    private final JwtTokenFilter jwtTokenFilter;
    private final DataSource dataSource;

    public SecurityConfiguration(JwtTokenFilter jwtTokenFilter, DataSource dataSource) {
        this.jwtTokenFilter = jwtTokenFilter;
        this.dataSource = dataSource;
    }

    public void configureHttpSecurity(HttpSecurity http) {
        http.addFilterBefore(
                jwtTokenFilter,
                UsernamePasswordAuthenticationFilter.class
        );
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Autowired
    @Bean
    public JdbcUserDetailsManager userDetailsManager(AuthenticationManager authenticationManager,
                                                     AuthenticationManagerBuilder authenticationManagerBuilder)
            throws Exception {
        JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcUserDetailsManagerConfigurer =
                authenticationManagerBuilder.jdbcAuthentication().dataSource(dataSource);

        JdbcUserDetailsManager jdbcUserDetailsManager = jdbcUserDetailsManagerConfigurer.getUserDetailsService();
        jdbcUserDetailsManager.setAuthenticationManager(authenticationManager);

        return jdbcUserDetailsManager;
    }
}

当应用程序尝试启动时,我看到以下内容:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  jwtTokenFilter defined in file [/example-app/build/classes/java/main/org/example/app/auth/JwtTokenFilter.class]
↑     ↓
|  securityConfiguration defined in file [/example-app/build/classes/java/main/org/example/app/config/SecurityConfiguration.class]
└─────┘

我想知道如何解决这种周期性依赖,同时牢记:

  • JwtTokenFilter需要一个UserDetailsManager,因为我想在数据库中查找该用户,看看该用户是否仍然存在
  • SecurityConfiguration需要有一个JwtTokenFilter,可以在HttpSecurity上设置

免责声明:JWT相关代码的灵感来自https://www.toptal.com/spring/spring-security-tutorial

注:我不确定在数据库中验证用户权限是否有意义,因为JWT的卖点之一是它是无状态的。。。我可以把当局放在JWT代币里


共 (2) 个答案

  1. # 1 楼答案

    如果使用@Autowired而不是构造函数注入,就可以解决这个问题

    例如:

    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
        @Autowired
        private JwtTokenFilter jwtTokenFilter;
        private final DataSource dataSource;
    
        public Security(DataSource dataSource) 
        {
            this.dataSource = dataSource;
        }
    }
    
  2. # 2 楼答案

    看看@Lazy注释,我也遇到了同样的问题,它就像一个符咒

    试试这样:

    public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, @Lazy JdbcUserDetailsManager jdbcUserDetailsManager) {
            this.jwtTokenUtil = jwtTokenUtil;
            this.userDetailsService = jdbcUserDetailsManager;
    }