Spring Boot (v3.4.3) API Returning 401 Unauthorized and throws an NPE

Discussão em 'Angular' iniciado por dstkv, Março 15, 2025.

  1. dstkv

    dstkv Guest

    I am working on a Spring Boot (WebFlux) backend and an Angular frontend. The client requests the user's conversations, but the API returns 401 Unauthorized, and the server logs show a NullPointerException.

    Backend (Spring Boot - WebFlux)

    I have a controller for the messages:

    package com.bluevenomeb.read_api.controller;

    import com.bluevenomeb.read_api.model.Conversation;
    import com.bluevenomeb.read_api.model.Message;
    import com.bluevenomeb.read_api.service.MessageService;

    import org.springframework.web.bind.annotation.*;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;

    import java.util.List;

    @RestController
    @RequestMapping("/messages")
    public class MessageController {
    private final MessageService messageService;

    public MessageController(MessageService messageService) {
    this.messageService = messageService;
    }

    @PostMapping
    public Mono<Message> sendMessage(@RequestParam String sender,
    @RequestParam String receiver,
    @RequestParam String content) {
    return messageService.sendMessage(sender, receiver, content);
    }

    @GetMapping("/sent/{sender}")
    public Flux<Message> getMessagesBySender(@PathVariable String sender) {
    return messageService.getMessagesBySender(sender);
    }

    @GetMapping("/received/{receiver}")
    public Flux<Message> getMessagesByReceiver(@PathVariable String receiver) {
    return messageService.getMessagesByReceiver(receiver);
    }

    @GetMapping("/conversation/{user}/{participant}")
    public Mono<Conversation> getConversation(@PathVariable String user,
    @PathVariable String participant) {
    return messageService.getConversation(user, participant);
    }

    @GetMapping("/conversations/{user}")
    public Flux<Conversation> getAllConversations(@PathVariable String user) {
    System.out.println("Getting all conversations for user: " + user);
    return messageService.getAllConversations(user);
    }
    }


    Inside the MessageService, we have this placeholder code:

    public Flux<Conversation> getAllConversations(String user) {
    return Flux.empty();
    }


    Here is the security configuration, followed by the requests filter:

    package com.bluevenomeb.read_api.config;

    import com.bluevenomeb.read_api.utils.security.JwtRequestFilter;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    import static org.springframework.security.config.Customizer.withDefaults;

    @Configuration
    public class SecurityConfig {
    JwtRequestFilter jwtRequestFilter;

    public SecurityConfig(JwtRequestFilter jwtRequestFilter) {
    this.jwtRequestFilter = jwtRequestFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
    .cors(withDefaults())
    .csrf(csrf -> csrf
    .ignoringRequestMatchers("/auth/**") // Disable CSRF for /auth/** endpoints
    )
    .authorizeHttpRequests(authz -> authz
    .requestMatchers("/auth/**", "/texts/**").permitAll()
    .anyRequest().authenticated()
    )
    .httpBasic(withDefaults())
    .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
    .allowedOrigins("http://localhost:4200")
    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
    .allowedHeaders("*")
    .allowCredentials(true);
    }
    };
    }
    }

    package com.bluevenomeb.read_api.utils.security;

    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.annotation.WebFilter;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.lang.NonNull;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.web.filter.OncePerRequestFilter;

    import java.io.IOException;
    import java.util.ArrayList;

    @WebFilter("/*")
    public class JwtRequestFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;

    public JwtRequestFilter(JwtUtil jwtUtil) {
    this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
    @NonNull FilterChain filterChain) throws ServletException, IOException {
    final String authorizationHeader = request.getHeader("Authorization");

    String username = null;
    String jwt = null;

    // Check if Authorization header exists and starts with "Bearer "
    if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
    jwt = authorizationHeader.substring(7); // Remove "Bearer "

    if (!jwtUtil.isTokenExpired(jwt)) {
    username = jwtUtil.extractEmail(jwt); // Extract the username from the JWT
    }
    }

    // Validate the token and set authentication context
    if (username != null && jwtUtil.validateToken(jwt, username)) {
    // Create an authentication object (you can use your custom user details if needed)
    User principal = new User(username, "", new ArrayList<>());
    // Create an authentication object and set it into the SecurityContext
    SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(principal,
    null, new ArrayList<>()));
    }

    // Continue the filter chain
    filterChain.doFilter(request, response);
    }
    }


    Here is the JwtUtil class:

    package com.bluevenomeb.read_api.utils.security;

    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.Jwts;
    import javax.crypto.SecretKey;
    import java.util.Date;

    public class JwtUtil {
    private final SecretKey secretKey;

    public JwtUtil(SecretKey secretKey) {
    this.secretKey = secretKey;
    }

    public String generateToken(String email) {
    return Jwts.builder()
    .subject(email)
    .issuedAt(new Date())
    .expiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour expiration
    .signWith(secretKey)
    .compact();
    }

    public Claims extractClaims(String token) throws ExpiredJwtException {
    return Jwts.parser()
    .verifyWith(secretKey)
    .build()
    .parseSignedClaims(token)
    .getPayload();
    }

    public String extractEmail(String token) {
    return extractClaims(token).getSubject();
    }

    public boolean isTokenExpired(String token) {
    try {
    return extractClaims(token).getExpiration().before(new Date());
    } catch (ExpiredJwtException e) {
    return true;
    } catch (Exception e) {
    System.out.println("Error parsing token: " + e.getMessage());
    return true;
    }
    }

    public boolean validateToken(String token, String username) {
    return username.equals(extractEmail(token)) && !isTokenExpired(token);
    }
    }


    And finally, the relevant Angular codes: ApiService

    get<T>(endpoint: string, params?: HttpParams, headers?: HttpHeaders): Observable<T> {
    const token = localStorage.getItem('token') || '';
    if (token) {
    headers = headers || new HttpHeaders();
    headers = headers.set('Authorization', `Bearer ${token}`);
    }
    return this.http.get<T>(`${this.apiUrl}/${endpoint}`, { params, headers })
    .pipe(catchError(this.handleError));
    }

    getAllConversations(userName: string): Observable<Conversation[]> {
    return this.get<Conversation[]>(`${Endpoints.MESSAGES}/conversations/${encodeURIComponent(userName)}`);
    }


    HomeComponent

    ngOnInit(): void {
    this.authService.user$.subscribe((user: User | null) => {
    if (user) {
    this.loadConversations(user);
    }
    });
    }

    private loadConversations(user: User) {
    this.apiService.getAllConversations(user.username)
    .pipe(take(1))
    .subscribe((conversations: Conversation[]) => {
    this.conversations = conversations;
    console.log("Conversations loaded for user " + user.email, conversations);
    });
    }


    I use SpringBoot v3.4.3, with MongoDB.

    Server Side error: java.lang.NullPointerException: Cannot invoke "java.lang.Throwable.getMessage()" because the return value of "org.springframework.core.NestedExceptionUtils.getMostSpecificCause(java.lang.Throwable)" is null at org.springframework.web.util.DisconnectedClientHelper.isClientDisconnectedException(DisconnectedClientHelper.java:120) at org.springframework.web.context.request.async.WebAsyncManager.lambda$startDeferredResultProcessing$6(WebAsyncManager.java:450) at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.lambda$onError$0(StandardServletAsyncWebRequest.java:195) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

    Client Side error: Failed to load resource: the server responded with a status of 401 () http://localhost:8080/messages/conversations/dstk

    Unauthorized request detected.

    ERROR Error: An unknown error occurred! at api.service.ts:63:29 handleError @ core.mjs:6662

    Questions: Why is my Spring Boot API returning 401 Unauthorized, even when the Authorization header is set? Could the NullPointerException be related to the authentication failure, or is it a separate issue?

    Plus: The token is valid during the request, as I logout and login again (which generates a new token), before testing.

    Thanks a lot for your time! Any help would be really appreciated.

    Continue reading...

Compartilhe esta Página