尽管在登录 Springboot 时有 200 ok 响应,但仍出现 403 错误
403 error despite having a 200 ok response on login Springboot
我 运行 遇到了一个奇怪的情况。
这是我所知道的:
为了让它登录,我需要将凭据作为 x-www-form-urlencoded
POST 请求发送。用户提供正确的凭据,它通过并向用户提供访问令牌和刷新令牌,如果没有,它将失败并且不提供任何这些令牌 - 这是人们所期望的。
现在,登录后我尝试访问资源 - 在本例中是我拥有的用户列表 - 它因 403 禁止错误而失败。我目前正在使用 PostMan 测试 API。我在登录后发出的初始 POST
请求是使用访问令牌的 Bearer Token
授权。它失败。在 Spring Boot:
上调试我的代码后
@Slf4j
public class AuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if(request.getServletPath().equals("api/login") || request.getServletPath().equals("/api/token/refresh")) {
filterChain.doFilter(request, response);
} else {
String authorizationHeader = request.getHeader(AUTHORIZATION);
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
try {
String token = authorizationHeader.substring("Bearer ".length());
// TODO: Refactor this to a utility class.
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String username = decodedJWT.getSubject();
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
stream(roles).forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role));
});
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (Exception exception) {
log.error("Error logging in: " + exception.getMessage());
Map<String, String> error = new HashMap<>();
error.put("error_message", exception.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
我发现 token
return 为空。好的...让我们尝试将其发送为 x-www-form-urlencoded
。 token
拿起发送的令牌...太棒了!在 filterChain.doFilter(request, response);
之后一路进入代码并在 Spring Boots 代码中到达 requiresAuthentication()
... 这个 returns false
很好.. .这个真的好看
但是,return 邮递员 - Status: 403 Forbidden
。哦 - 怎么样?查看 Spring 引导的日志,我选择了以下与我的 url 入口点相关的内容:
2022-04-16 08:36:47.267 DEBUG 21000 --- [nio-8102-exec-4] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1683533175<open>)] after transaction
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.s.s.a.dao.DaoAuthenticationProvider : Failed to authenticate since no credentials provided
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.a.c.c.C.[Tomcat].[localhost] : Processing ErrorPage[errorCode=0, location=/error]
2022-04-16 08:36:47.285 DEBUG 21000 --- [nio-8102-exec-4] o.s.security.web.FilterChainProxy : Securing GET /error
作为新手,我不确定应该如何解决这个问题。
编辑:
这是我当前的安全配置:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Lazy
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.cors();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/login/**", "/api/login/**", "/api/token/refresh/**").permitAll();
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/users/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/user/save/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/role/assign-to-user/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().anyRequest().authenticated();
http.addFilter(new AuthenticationFilter(authenticationManagerBean()));
http.addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
protected CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("*"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
source.registerCorsConfiguration("/**", config);
return source;
}
}
身份验证过滤器:
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
log.info("Username: \"" + username + "\", password: \"" + password + "\"");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authenticationToken);
}
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication
) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
// TODO: Generate secret key that is a little more secure than this.
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
String accessToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
String refreshToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.sign(algorithm);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", accessToken);
tokens.put("refresh_token", refreshToken);
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}
}
我注意到 AuthorizationFilter 创建了权限,但没有在 UsernamePasswordAuthenticationToken 中设置它们。您的安全匹配器需要权限。
我 运行 遇到了一个奇怪的情况。
这是我所知道的:
为了让它登录,我需要将凭据作为 x-www-form-urlencoded
POST 请求发送。用户提供正确的凭据,它通过并向用户提供访问令牌和刷新令牌,如果没有,它将失败并且不提供任何这些令牌 - 这是人们所期望的。
现在,登录后我尝试访问资源 - 在本例中是我拥有的用户列表 - 它因 403 禁止错误而失败。我目前正在使用 PostMan 测试 API。我在登录后发出的初始 POST
请求是使用访问令牌的 Bearer Token
授权。它失败。在 Spring Boot:
@Slf4j
public class AuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if(request.getServletPath().equals("api/login") || request.getServletPath().equals("/api/token/refresh")) {
filterChain.doFilter(request, response);
} else {
String authorizationHeader = request.getHeader(AUTHORIZATION);
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
try {
String token = authorizationHeader.substring("Bearer ".length());
// TODO: Refactor this to a utility class.
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String username = decodedJWT.getSubject();
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
stream(roles).forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role));
});
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (Exception exception) {
log.error("Error logging in: " + exception.getMessage());
Map<String, String> error = new HashMap<>();
error.put("error_message", exception.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
我发现 token
return 为空。好的...让我们尝试将其发送为 x-www-form-urlencoded
。 token
拿起发送的令牌...太棒了!在 filterChain.doFilter(request, response);
之后一路进入代码并在 Spring Boots 代码中到达 requiresAuthentication()
... 这个 returns false
很好.. .这个真的好看
但是,return 邮递员 - Status: 403 Forbidden
。哦 - 怎么样?查看 Spring 引导的日志,我选择了以下与我的 url 入口点相关的内容:
2022-04-16 08:36:47.267 DEBUG 21000 --- [nio-8102-exec-4] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1683533175<open>)] after transaction
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.s.s.a.dao.DaoAuthenticationProvider : Failed to authenticate since no credentials provided
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.a.c.c.C.[Tomcat].[localhost] : Processing ErrorPage[errorCode=0, location=/error]
2022-04-16 08:36:47.285 DEBUG 21000 --- [nio-8102-exec-4] o.s.security.web.FilterChainProxy : Securing GET /error
作为新手,我不确定应该如何解决这个问题。
编辑:
这是我当前的安全配置:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Lazy
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.cors();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/login/**", "/api/login/**", "/api/token/refresh/**").permitAll();
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/users/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/user/save/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/role/assign-to-user/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().anyRequest().authenticated();
http.addFilter(new AuthenticationFilter(authenticationManagerBean()));
http.addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
protected CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("*"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
source.registerCorsConfiguration("/**", config);
return source;
}
}
身份验证过滤器:
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
log.info("Username: \"" + username + "\", password: \"" + password + "\"");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authenticationToken);
}
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication
) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
// TODO: Generate secret key that is a little more secure than this.
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
String accessToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
String refreshToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.sign(algorithm);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", accessToken);
tokens.put("refresh_token", refreshToken);
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}
}
我注意到 AuthorizationFilter 创建了权限,但没有在 UsernamePasswordAuthenticationToken 中设置它们。您的安全匹配器需要权限。