简体   繁体   中英

Unauthorized error: Full authentication is required to access this resource with spring.security v.6

So I am new to Spring Security and I tried to build a Spring Boot app with REST services and also implement Spring Security with JWT. I have followed a security tutorial for the most part and I got the app running but when I try to call an authentication(/auth/**) endpoint I just get the 401 Unauthorized error. Since spring has updated to v 3.0.0 and now WebSecurityConfigurerAdapter is deprecated I have changed the configurations accordingly.

This is my configuration file

import com.example.myshroombackend.security.JwtAuthenticationEntryPoint;
import com.example.myshroombackend.security.JwtAuthenticationFilter;
import com.example.myshroombackend.service.AuthServiceImpl;

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.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity
public class SecurityConfig{

    @Autowired
    AuthServiceImpl authService;

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Bean JwtAuthenticationFilter jwtAuthenticationFilter(){
        return  new JwtAuthenticationFilter();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

        authProvider.setUserDetailsService(authService);
        authProvider.setPasswordEncoder(passwordEncoder());

        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception {
        return authConfiguration.getAuthenticationManager();
    }
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/js/**", "/images/**");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf()
                .and()
                .cors()
                .disable()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests()
                .requestMatchers("/auth/**").permitAll()  //every /auth/login or /auth/register are permitted
                .anyRequest().authenticated(); //all other requests must be authenticated ==>
                // you first have to authenticate yourself to perform the other requests

            http.authenticationProvider(authenticationProvider());
 http.addFilterBefore(jwtAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }





}

my custom filter


import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    AuthServiceImpl authService;
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,@NotNull FilterChain filterChain) throws ServletException, IOException {

        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtTokenProvider.validateToken(jwt)) {
                String username = jwtTokenProvider.getUserNameFromToken(jwt);

                UserDetails userDetails = authService.loadUserByUsername(username);

                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userDetails,
                                null,
                                userDetails.getAuthorities());

                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}", e);
        }

        filterChain.doFilter(request, response);

    }

    private String parseJwt(HttpServletRequest request) {
        String jwt = jwtTokenProvider.getJwtFromCookies(request);
        return jwt;
    }

}

my token service


import com.example.myshroombackend.exception.JwtAuthenticationException;
import com.example.myshroombackend.service.AuthServiceImpl;
import com.example.myshroombackend.service.UserDetailsImpl;
import io.jsonwebtoken.*;
import jakarta.servlet.http.Cookie;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.util.WebUtils;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Optional;

@Component
@Slf4j
public class JwtTokenService {
    @Value("${application.secret")
    private String jwtSecret;

    @Value("${application.jwtCookieName}")
    private String jwtCookie;

    private AuthServiceImpl authService;
    private static final Instant EXPIRATIONTIME = Instant.now().plus(20, ChronoUnit.HOURS);

    public String generateToken(String userName) {
        Instant now = Instant.now();
        return Jwts.builder()
                .setSubject(userName)
                .setIssuedAt(Date.from(now))
                .setExpiration(Date.from(EXPIRATIONTIME))
                .signWith(SignatureAlgorithm.HS512, jwtSecret.getBytes(StandardCharsets.UTF_8))
                .compact();
    }

    public boolean validateToken(String token) {

        if ((token != null) && (!"".equals(token))) {

            Jws<Claims> claimsJws;
            try{
            claimsJws = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
            if (claimsJws != null) {
                final String userId = Optional
                        .ofNullable(claimsJws.getBody().get(new String("id")))
                        .map(Object::toString)//
                        .orElseThrow(
                                () -> new AuthenticationCredentialsNotFoundException("No username given in jwt"));
            }
            return true;
        } catch (ExpiredJwtException e) {
                throw new RuntimeException(e);
            } catch (UnsupportedJwtException e) {
                throw new RuntimeException(e);
            } catch (MalformedJwtException e) {
                throw new RuntimeException(e);
            } catch (SignatureException e) {
                throw new RuntimeException(e);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (AuthenticationCredentialsNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
        return false;
    }

    public String generateToken(Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        return generateToken(user.getUsername());

    }

    public String getJwtFromCookies(HttpServletRequest request) {
        Cookie cookie = WebUtils.getCookie(request, jwtCookie);
        if (cookie != null) {
            return cookie.getValue();
        } else {
            return null;
        }
    }


    public String generateTokenFromUsername(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() +  10000000))
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) {
        String jwt = generateTokenFromUsername(userPrincipal.getUsername());
        ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/auth").maxAge(24 * 60 * 60).httpOnly(true).build();
        return cookie;
    }


    public String getUserNameFromToken(final String token) throws JwtAuthenticationException {
        String userName = null;
        try {
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(jwtSecret.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token);
            if (claimsJws != null) {
                userName = claimsJws.getBody().getSubject();
            }
        } catch (final SignatureException | MalformedJwtException | UnsupportedJwtException ex) {
            log.error("Unsupported jwt token {} with exception {}",
                    token,
                    ex.getMessage());
            throw new JwtAuthenticationException(ex);
        } catch (final ExpiredJwtException ex) {
            log.error("Expired jwt token {}",
                    ex.getMessage());
            throw new JwtAuthenticationException(ex);
        } catch (final AuthenticationCredentialsNotFoundException ex) {
            log.error("An error occured while trying to create authentication based on jwt token, missing credentials {}",
                    ex.getMessage());
            throw new JwtAuthenticationException(ex);
        } catch (final Exception ex) {
            log.error("Unexpected exception occured while parsing jwt {} exception: {}",
                    token,
                    ex.getMessage());
            throw new JwtAuthenticationException(ex);
        }
        return userName;
    }

}

also my RestController


import com.example.myshroombackend.dto.LoginRequestDto;
import com.example.myshroombackend.entity.Rank;
import com.example.myshroombackend.entity.UserEntity;
import com.example.myshroombackend.repository.UserRepository;
import com.example.myshroombackend.security.JwtTokenService;
import com.example.myshroombackend.service.UserDetailsImpl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;


@RestController
@RequestMapping("/auth")
@CrossOrigin
public class AuthController {

    private final UserRepository userRepository;
    private AuthenticationManager authenticationManager;
    private JwtTokenService provider;
    private PasswordEncoder encoder;

    public AuthController(UserRepository userRepository, AuthenticationManager authenticationManager, JwtTokenService provider) {
        this.userRepository = userRepository;
        this.authenticationManager = authenticationManager;
        this.provider = provider;
    }

    @PostMapping("/login")
    public ResponseEntity<LoginRequestDto> loginUser(@RequestBody LoginRequestDto dto) {
        Authentication authentication = authenticationManager
                .authenticate(new UsernamePasswordAuthenticationToken(dto.getUserName(), dto.getPassword()));

        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        SecurityContextHolder.getContext().setAuthentication(authentication);

        ResponseCookie jwtCookie = provider.generateJwtCookie(userDetails);

        return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
                .body(new LoginRequestDto(
                        userDetails.getUsername()));
    }

    @PostMapping("/register")
    public ResponseEntity<?> registerUser( @RequestBody LoginRequestDto dto) {

        Optional<UserEntity> optionalUserEntity = userRepository.findByUserName(dto.getUserName());

        if (optionalUserEntity.isPresent()) {
            return ResponseEntity.badRequest().body("User already in database");
        }


        // Create new user's account
        UserEntity userEntity = new UserEntity();
        userEntity.setUserName(dto.getUserName());
        userEntity.setPassword(encoder.encode(dto.getUserName()));
        userEntity.setRank(Rank.BEGINNER);

        userRepository.save(userEntity);

        return ResponseEntity.ok("User registered successfully!");
    }

}

I use requestMatchers() instead of antMatchers() that is no longer in this security version and authorizeHttpRequests() instead of authorizeRequests() . I don't know if the problem is from there or it's something else

When authorizeHttpRequests is used instead of authorizeRequests , then AuthorizationFilter is used instead of FilterSecurityInterceptor .

And as I understand, prior to Spring Security 6, AuthorizationFilter and FilterSecurityInterceptor were performing authorization checks on the first request only, but since Spring Security 6, it seems to be applied to every request (ie on all dispatcher types) at least for AuthorizationFilter ( FilterSecurityInterceptor is now deprecated).

So my guess is that it actually works while your request is non-dispatched but then fail as soon as your request is forwarded to another resource.

Maybe try to explicitly indicate that you do not want to apply the authorization rules to all dispatcher types, either by using shouldFilterAllDispatcherTypes or dispatcherTypeMatchers .

For example to grant all access on requests with dispatcher type ASYNC or FORWARD :

http
...
.authorizeHttpRequests()
.dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.FORWARD).permitAll()
...

Of course you can also customize it to require a specific role ( hasRole ) instead of permitAll if needed.

See the documentation for more information.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM