弹簧靴。请求无法到达控制器
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 和 CustomUserDetailsService 类:
@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;
CustomUserDetailsService.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 清理了构建和重建项目。感谢所有提供帮助的人!
【讨论】:
以上是关于弹簧靴。请求无法到达控制器的主要内容,如果未能解决你的问题,请参考以下文章