弹簧靴。请求无法到达控制器

Posted

技术标签:

【中文标题】弹簧靴。请求无法到达控制器【英文标题】:Spring boot. Request can't reach controller 【发布时间】:2021-09-18 14:25:50 【问题描述】:

我正在使用 Spring Boot 和 Spring Security 创建一个 API。我已经创建了一些基本的身份验证机制。目前在请求授权方面面临一些未知问题。 这是我的配置类:

// removed for brevity

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 

    private final CustomUserDetailsService customUserDetailsService;
    private final JwtTokenFilter jwtTokenFilter;
    private final CustomAuthenticationProvider customAuthenticationProvider;

    public SecurityConfiguration(CustomUserDetailsService customUserDetailsService,
                                 JwtTokenFilter jwtTokenFilter,
                                 CustomAuthenticationProvider customAuthenticationProvider) 
        this.customUserDetailsService = customUserDetailsService;
        this.jwtTokenFilter = jwtTokenFilter;
        this.customAuthenticationProvider = customAuthenticationProvider;
    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        // todo: provide an authenticationProvider for authenticationManager
        /* todo:
            In most use cases authenticationProvider extract user info from database.
            To accomplish that, we need to implement userDetailsService (functional interface).
            Here username is an email.
        * */
        auth.userDetailsService(customUserDetailsService);
        auth.authenticationProvider(customAuthenticationProvider);
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        // Enable CORS and disable CSRF
        http = http.cors().and().csrf().disable();

        // Set session management to Stateless
        http = http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and();

        // Set unauthorized requests exception handler
        http = http
                .exceptionHandling()
                .authenticationEntryPoint(
                        (request, response, ex) -> 
                            response.sendError(
                                    HttpServletResponse.SC_UNAUTHORIZED,
                                    ex.getMessage()
                            );
                        
                )
                .and();

        // Set permissions and endpoints
        http.authorizeRequests()
                .antMatchers("/api/v1/auth/**").permitAll()
                .antMatchers("/api/v1/beats/**").hasRole("ADMIN")
                .anyRequest().authenticated();

        http.addFilterBefore(jwtTokenFilter,
                UsernamePasswordAuthenticationFilter.class);
    

    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

    // Used by spring security if CORS is enabled.
    @Bean
    public CorsFilter corsFilter() 
        UrlBasedCorsConfigurationSource source =
                new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    

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

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() 
        return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
    

为了检查用户是否有权访问资源,我使用来自 JWT 有效负载的信息。为此,我有一个过滤器类:

// removed for brevity
@Component
public class JwtTokenFilter extends OncePerRequestFilter 

    private final static Logger logger = LoggerFactory.getLogger(JwtTokenFilter.class);
    private final JwtTokenUtil jwtTokenUtil;
    private final CustomUserDetailsService customUserDetailsService;

    public JwtTokenFilter(JwtTokenUtil jwtTokenUtil,
                          CustomUserDetailsService customUserDetailsService) 
        this.jwtTokenUtil = jwtTokenUtil;
        this.customUserDetailsService = customUserDetailsService;
    

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException 
        final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (header == null || header.isEmpty() || !header.startsWith("Bearer ")) 
            logger.error("Authorization header missing");
            filterChain.doFilter(request, response);
            return;
        
        final String token = header.split(" ")[1].trim();
        if (!jwtTokenUtil.validate(token)) 
            filterChain.doFilter(request, response);
            return;
        
        UserDetails userDetails = customUserDetailsService.loadUserByUsername(token);
        if (userDetails == null)
            throw new ServletException("Couldn't extract user from JWT credentials");
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        authentication.setDetails(
                new WebAuthenticationDetailsSource().buildDetails(request)
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    

为了表示 UserDetails,我实现了 CustomUserDetails 和 CustomUserDetailsS​​ervice 类:

@Data
@NoArgsConstructor
public class CustomUserDetails implements UserDetails 
    private Long userId;
    private Long profileId;
    private String email;
    private String password;
    private String fullName;
    private String nickname;
    private String avatar;
    private String phoneNumber;
    private ProfileState profileState;
    private Collection<? extends GrantedAuthority> grantedAuthorities;

    public static CustomUserDetails fromUserAndProfileToMyUserDetails(Profile profile) 
       CustomUserDetails customUserDetails = new CustomUserDetails();
       customUserDetails.setUserId(profile.getUser().getId());
       customUserDetails.setEmail(profile.getUser().getEmail());
       customUserDetails.setPassword(profile.getUser().getPassword());
       customUserDetails.setProfileId(profile.getId());
       customUserDetails.setFullName(profile.getFullName());
       customUserDetails.setNickname(profile.getNickname());
       customUserDetails.setAvatar(profile.getAvatar());
       customUserDetails.setPhoneNumber(profile.getPhoneNumber());
       customUserDetails.setProfileState(profile.getState());
       return customUserDetails;
    

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() 
        return grantedAuthorities;
    

    @Override
    public String getPassword() 
        return password;
    

    @Override
    public String getUsername() 
        return nickname;
    

    @Override
    public boolean isAccountNonExpired() 
        return false;
    

    @Override
    public boolean isAccountNonLocked() 
        return false;
    

    @Override
    public boolean isCredentialsNonExpired() 
        return false;
    

    @Override
    public boolean isEnabled() 
        return false;
    

CustomUserDetailsS​​ervice.java:

@Component
public class CustomUserDetailsService implements UserDetailsService 
    private Logger logger = LoggerFactory.getLogger(CustomUserDetailsService.class);
    private final ProfileRepository profileRepository;
    private final JwtTokenUtil jwtTokenUtil;

    public CustomUserDetailsService(ProfileRepository profileRepository, JwtTokenUtil jwtTokenUtil) 
        this.profileRepository = profileRepository;
        this.jwtTokenUtil = jwtTokenUtil;
    

    @Override
    public UserDetails loadUserByUsername(String token) throws UsernameNotFoundException 
        if (token == null || token.isEmpty()) throw new IllegalArgumentException("Token cannot be null or empty");
        try 
            final String nickname = jwtTokenUtil.getNickname(token);
            Profile profile = profileRepository
                    .findByNickname(nickname)
                    .orElseThrow(() -> new UsernameNotFoundException(
                            String.format("User: %s not found", token)
                    ));
            logger.info(String.format("Extracted Profile: %s", profile));

            CustomUserDetails customUserDetails = CustomUserDetails.fromUserAndProfileToMyUserDetails(profile);
            List<GrantedAuthority> authorities = new ArrayList<>(Collections.emptyList());
            authorities.add(new SimpleGrantedAuthority(profile.getType().getValue()));
            customUserDetails.setGrantedAuthorities(authorities);
            return customUserDetails;
         catch (Exception e) 
            logger.error("Wasn't able to load user ``. Exception occurred ``", token, e.getMessage());
            return null;
        
    

这是我要访问的控制器:

@RestController
@RequestMapping("/api/beats")
public class BeatController 
    private static final Logger logger = LogManager.getLogger(BeatController.class);

    private final BeatService beatService;

    public BeatController(BeatService beatService) 
        this.beatService = beatService;
    

    @GetMapping("id")
    public Object getBeat(@PathVariable Long id) 
        try 
            return beatService.findById(id);
         catch (Exception e) 
            logger.error("Can't find beat with id " + id);
            return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
        
    

    @GetMapping
    public Object getBeats(@RequestParam String filter, @RequestParam String page) 
        try 
            return beatService.findAll();
         catch (Exception e) 
            logger.error("Can't find beats");
            return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
        
    

    @PostMapping
    public Object createBeat(@RequestBody BeatDto beatDto) 
        try 
            beatDto.setId(null);
            return beatService.save(beatDto);
         catch (Exception e) 
            logger.error("Can't create new Beat");
            return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
        
    

    @PutMapping("id")
    public Object updateBeat(@PathVariable Long id, @RequestBody BeatDto newBeat) 
        try
            BeatDto oldBeat = beatService.findById(id);
            if (oldBeat != null) 
                newBeat.setId(id);
             else 
                throw new Exception();
            
            return  beatService.save(newBeat);
         catch (Exception e) 
            return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
        
    

    @DeleteMapping("id")
    public Object deleteBeat(@PathVariable Long id) 
        try 
            return beatService.deleteById(id);
         catch (Exception e) 
            return new ResponseEntity<>(new DefaultResponseDto("failed", e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
        
    

所以,我提出了一个请求,为它提供了正确的授权标头和访问令牌。它从 DB 获取用户并获取 GrantedAuthority。最后的步骤是:

    它在 SecurityContext 中设置身份验证对象。 在 FilterChain 中走得更远。

但它没有到达控制器,也没有抛出任何异常。只用 403 回复我。可能是我忘记了要设置的东西,或者问题可能出在其他地方?请指导我。

【问题讨论】:

为什么要编写自己的JwtFilter 而不是使用官方的 Spring Security OAuth2 JWT 支持? 还有UserDetailsService#loadByUsername 用于在身份验证后从数据源加载用户。不要传递令牌。你的实现让很多事情倒退了。过滤器用于提取令牌,然后将令牌发送到使用某种 JWTvalidator 验证令牌的 authenticationManager。验证令牌后,身份验证管理器调用传入用户名的UserDetailsService 以获取 UserDetails 对象,然后身份验证管理器将其放入安全上下文中。 【参考方案1】:

所以终于弄清楚了问题所在。对我有帮助的主要建议:

    CustomUserDetails 服务中所有返回false 的方法都返回true。 (来自 M. Deinum 的建议) 使用:logging.level.org.springframework.security=TRACE 打开 Spring 框架安全日志。 这有助于我跟踪 FilterChain 抛出的异常。 感谢 Marcus Hert da Coregio。

为了解决问题,我做了哪些更改?首先我更新了 BeatController 中的@RequestMapping mismatch。堆栈跟踪显示,虽然它从数据库中正确获取用户角色,但它与我的角色和我在配置类中编写的角色不匹配。默认情况下,它会在我们提供的实际角色名称之前添加“ROLE_”前缀。我认为定义这个 bean 会改变这种行为:

    GrantedAuthorityDefaults grantedAuthorityDefaults() 
        return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
    

原来它对前缀行为没有影响,所以它在我提供的“ADMIN”角色名称之前添加了“ROLE_”。验证请求时添加“ROLE_”前缀修复问题:

来自

authorities.add(new SimpleGrantedAuthority(profile.getType().getValue()));

authorities.add(new SimpleGrantedAuthority("ROLE_" + profile.getType().getValue()));

此外,我使用 gradle 清理了构建和重建项目。感谢所有提供帮助的人!

【讨论】:

以上是关于弹簧靴。请求无法到达控制器的主要内容,如果未能解决你的问题,请参考以下文章

弹簧靴和百里香叶

弹簧靴和 h2。无法更改数据库名称

弹簧靴 | JSONB Postgres |异常:无法加载类 [jsonb]

弹簧靴服务角度

弹簧靴找不到 Thymeleaf 视图

带有弹簧靴的百里香叶缓存