본문 바로가기

SPRING/SECURITY

[토이프로젝트] SpringSecurity6 + JWT + Redis 인증/인가 구현 (3)

Jwt 토큰 검증 로직 추가하기

  • 이제 회원가입과 로그인까지 완성했습니다.
  • 그렇다면 토큰을 사용해 검증하는 로직을 필터를 통해 구현해보겠습니다!

토큰으로부터 PayLoad 추출 함수

JwtProvider.java

package com.toy.filterloginpjt.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.toy.filterloginpjt.dto.SignInRequestDTO;
import com.toy.filterloginpjt.redis.RedisDao;
import com.toy.filterloginpjt.repository.UserRepository;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class JwtProvider {

    private final UserRepository userRepository;
    private final RedisDao redisDao;
    private final ObjectMapper objectMapper;

    @Value("${spring.jwt.secret-key}")
    private String JWT_KEY;

    @Value("${spring.jwt.life.atk}")
    private Long ATK_LIFE;

    @Value("${spring.jwt.life.rtk}")
    private Long RTK_LIFE;

		...

    // Jwt 유효성 검사를 위해 토큰에서 PayLoad 추출하기
    public PayLoad getPayLoad(String jwt) throws JsonProcessingException {
        SecretKey secretKey = Keys.hmacShaKeyFor(JWT_KEY.getBytes(StandardCharsets.UTF_8));
        String PayLoadStr = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(jwt)
                .getBody()
                .getSubject();
        return objectMapper.readValue(PayLoadStr, PayLoad.class);
    }

}

JwtAuthenticationFilter.java 작성하기

package com.toy.filterloginpjt.config.filter;

import com.toy.filterloginpjt.util.JwtProvider;
import com.toy.filterloginpjt.util.PayLoad;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.security.auth.Subject;
import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

    public JwtAuthenticationFilter(JwtProvider jwtProvider) {
        this.jwtProvider = jwtProvider;
    }

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

        if (authorization != null) {
            String jwt = authorization.substring(7);
            try {
                PayLoad payLoad = jwtProvider.getPayLoad(jwt);
                String requestURI = request.getRequestURI();
                String email = payLoad.getEmail();
                String authority = payLoad.getAuthority();

                // 여기서부터 중요!!
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        email, null, AuthorityUtils.commaSeparatedStringToAuthorityList(authority));

                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (JwtException e) {
                request.setAttribute("exception", e.getMessage());
            }
        }
        filterChain.doFilter(request, response);
    };

    /**
     * 로그인 시 해당 필터를 거치지 않도 설정합니다.
     * ( return 값이 true가 되는 조건은 필터를 거치지 않습니다.)
     */

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return request.getServletPath().equals("/api/v1/auth/signin");
    }

}

Spring Security Config에 필터 추가하기

package com.toy.filterloginpjt.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.toy.filterloginpjt.config.filter.CustomUsernamePasswordAuthenticationFilter;
import com.toy.filterloginpjt.config.filter.JwtAuthenticationFilter;
import com.toy.filterloginpjt.util.JwtProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Arrays;
import java.util.Collections;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtProvider jwtProvider;

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf((csrf) -> csrf.disable())
                .cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration config = new CorsConfiguration();
                        config.setAllowedOrigins(Collections.singletonList("*"));
                        config.setAllowedMethods(Collections.singletonList("*"));
                        config.setAllowCredentials(true);
                        config.setAllowedHeaders(Collections.singletonList("*"));
                        config.setExposedHeaders(Arrays.asList("Authorization"));
                        config.setMaxAge(3600L);
                        return config;
                    }
                }))
                .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), BasicAuthenticationFilter.class)
                .authorizeHttpRequests((requests) -> {
                    requests.requestMatchers(
                            "/api/v1/auth/signup",
                            "/api/v1/auth/signin",
                            "/h2-console/**").permitAll()
                            .anyRequest().authenticated();
                });

        http.headers(options -> options.frameOptions(frame -> frame.disable()));

        http.formLogin(formLogin -> formLogin.disable());
        http.httpBasic(Customizer.withDefaults());
        return (SecurityFilterChain)http.build();
    }
}

테스트용 Controller 추가하기

package com.toy.filterloginpjt.controller;

import com.toy.filterloginpjt.dto.SignInRequestDTO;
import com.toy.filterloginpjt.dto.SignInResponseDTO;
import com.toy.filterloginpjt.dto.SignUpRequestDTO;
import com.toy.filterloginpjt.dto.SignUpResponseDTO;
import com.toy.filterloginpjt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.apache.coyote.Response;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/api/v1/auth/signup")
    public ResponseEntity<SignUpResponseDTO> signUp(@RequestBody SignUpRequestDTO requestDTO) {
        SignUpResponseDTO responseDTO = userService.signUp(requestDTO);
        return new ResponseEntity<SignUpResponseDTO>(responseDTO, HttpStatus.CREATED);
    }

    @PostMapping("/api/v1/auth/signin")
    public ResponseEntity<SignInResponseDTO> signIn(@RequestBody SignInRequestDTO requestDTO) {
        SignInResponseDTO responseDTO = userService.signIn(requestDTO);

        return new ResponseEntity<SignInResponseDTO>(responseDTO, HttpStatus.ACCEPTED);
    }

    @GetMapping("/api/v1/auth/test")
    public ResponseEntity<String> test() {
        return new ResponseEntity<String>("성공!!", HttpStatus.ACCEPTED);
    }
}