如何解决这种与 Spring Security 相关的循环依赖?

时间:2021-04-18 15:17:50

标签: java spring spring-boot spring-security

我有一个 Spring Boot/Spring Security 应用程序,我想将 JWT 身份验证与 Spring Security 提供的 JdbcUserDetailsManager 一起使用。

这是我目前所拥有的:

ExampleApplication.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(使用 UserDetailsS​​ervice)

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;
    }

}

SecurityConfiguration.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 需要有一个可以在 HttpSecurity 上设置的 JwtTokenFilter

免责声明:受 https://www.toptal.com/spring/spring-security-tutorial 启发的 JWT 相关代码

注意:我不确定在数据库中验证用户权限是否有意义,因为 JWT 的卖点之一是它是无状态的......我可以将权限放在 JWT 令牌中。

2 个答案:

答案 0 :(得分:0)

查看 @Lazy 注释,我遇到了同样的问题,但它的效果非常好。

尝试类似:

val recyclerView = view.recyclerView
recyclerView.adapter = adapter
recyclerView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
recyclerView.itemAnimator = SlideInUpAnimator().apply {
addDuration = 350

答案 1 :(得分:0)

如果使用@Autowired代替构造函数注入可以解决问题。

示例:

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtTokenFilter jwtTokenFilter;
    private final DataSource dataSource;

    public Security(DataSource dataSource) 
    {
        this.dataSource = dataSource;
    }
}