Spring-React : 카카오 소셜 로그인 + JWT (5) JWT 유효성을 검사하고 인증된 사용자 정보 불러오기

우진·2022년 4월 28일
0
post-thumbnail
post-custom-banner

(5) JWT 유효성을 검사하고 인증된 사용자 정보 불러오기


서론.

해당 프로젝트에서는 오로지 카카오 소셜 로그인 만 사용한다. ( ID/PW 사용안함! )

이전 포스팅✅에서는 JWT 를 직접 생성해 응답 헤더에 넣어보았다. 이번에는 요청 헤더에 들어온 JWT의 유효성을 검사하고, 인증된 사용자의 정보를 불러오도록 하자.


사용한 의존성 🍃

기본 의존성 :
Spring Web
Spring boot DevTools
Lombok
Spring Security
MySQL Driver
spring Data JPA


추가한 의존성 :
jackson-databind
jackson-datatype-jsr310
java-jwt



1. corsFilter 설정 확인하기


프로젝트를 진행하던 당시 나는 이부분에서 삐끗했다.😕 응답 헤더에 토큰이 들어간 상태로 프론트에 잘 넘어갔는데 정보를 읽어올 수가 없다는 것이다.
열심히 구글링을 해보니 이 설정이 빠져 있었다.


@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*"); //(1)
        config.addExposedHeader("*"); //(2)
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/api/**", config);
        return new CorsFilter(source);
    }
}
  1. .addAllowedHeader("*")
  2. .addExposedHeader("*") 꼭꼭 설정해주자!!⭐⭐



2. JWT 유효성 검사하기


2-1. JwtRequestFilter 만들기


만들어둔 jwt 패키지에 OncePerRequestFilter를 상속받는 유효성 체크용 필터를 만든다.
해당 필터는 이름에서도 짐작 가능 하듯, 한번의 요청마다 한번씩 실행되는 필터이다. 프론트 측에서 요청 헤더에 토큰을 넣어 보내면 이 필터가 검증해 줄 것이다.


//(1)
@RequiredArgsConstructor
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired //(2)
    UserRepository userRepository;

    @Override //(3)
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    	//(4)
        String jwtHeader = ((HttpServletRequest)request).getHeader(JwtProperties.HEADER_STRING);

        //(5)
        if(jwtHeader == null || !jwtHeader.startsWith(JwtProperties.TOKEN_PREFIX)) {
            filterChain.doFilter(request, response);
            return;
        }

        //(6)
        String token = jwtHeader.replace(JwtProperties.TOKEN_PREFIX, "");

        Long userCode = null;

		//(7)
        try {
            userCode = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(token)
                    .getClaim("id").asLong();

        } catch (TokenExpiredException e) {
            e.printStackTrace();
            request.setAttribute(JwtProperties.HEADER_STRING, "토큰이 만료되었습니다.");
        } catch (JWTVerificationException e) {
            e.printStackTrace();
            request.setAttribute(JwtProperties.HEADER_STRING, "유효하지 않은 토큰입니다.");
        }

		//(8)
        request.setAttribute("userCode", userCode);

		//(9)
        filterChain.doFilter(request, response);
    }
}
  1. @Component 어노테이션을 달아준다.

  2. UserRepository@Autowired 한다.

  3. doFilterInternal 메소드를 오버라이드 한다.

  4. 요청 헤더의 Authorization 항목 값을 가져와 jwtHeader 변수에 담는다.

  5. 만약 jwtHeadernull 이거나 Bearer 로 시작하지 않으면 return; 으로 이후 로직을 실행시키지 않고 넘긴다.

  6. jwtHeader 가 제대로 된 형식이라면 토큰 앞의 Bearer 를 떼어내 token 변수에 담는다.

  7. token 을 비밀 키로 복호화하는 동시에 개인 클레임에 넣어뒀던 id 값을 가져온다. 이 코드 자체가 인증과정이므로 이어지는 catch 문에서 Exception 처리를 해준다.

  8. userCode 에 값이 잘 담겼다면 request.setAttribute("userCode", userCode) 로 값을 넘긴다.

  9. filterCahinrequest response 값을 넘긴다.


2-2. CustomAuthenticationEntryPoint 클래스 만들기

.ExceptionHandling() 으로 이 클래스를 등록하면 앞선 인증 과정에서 401(UnAuthorized) 에러가 발생했을 때 이 클래스가 호출된다.


@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
    	//(1)
        String exception = (String) request.getAttribute(JwtProperties.HEADER_STRING);
        String errorCode;

        if(exception.equals("토큰이 만료되었습니다.")) {
            errorCode = "토큰이 만료되었습니다.";
            setResponse (response, errorCode);
        }

        if(exception.equals("유효하지 않은 토큰입니다.")) {
            errorCode = "유효하지 않은 토큰입니다.";
            setResponse(response, errorCode);
        }
    }

	//(2)
    private void setResponse(HttpServletResponse response, String errorCode) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(JwtProperties.HEADER_STRING + " : " + errorCode);
    }
}
  1. JwtRequestFilter 에서 Exception 발생시 request 에 추가한 요소를 불러와 담아준다.

  2. 스테이터스, 콘텐트 타입, 오류 메세지를 담아 응답해주는 메소드를 만들어 사용한다.


※ 예외처리에 관한 문제는 코드에 오류가 있을 수도 있다. 토큰 만료 기간을 길게 잡아 놔서 직접 테스트해보지 못했다 ! ! ※


2-3. SecurityConfig 설정하기


위 클래스를 모두 만들었다면 SecurityConfig 에 추가해줘야한다.


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .httpBasic().disable()
                .formLogin().disable()
                .addFilter(corsFilter);

        http.authorizeRequests()
                .antMatchers(FRONT_URL+"/main/**")
                .authenticated()
                .anyRequest().permitAll()

                .and()
                //(1)
                .exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint());

		//(2)
        http.addFilterBefore(new JwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
    }
  1. .ExceptionHandling() 으로 클래스를 등록해준다.

  2. .addFilterBefore() 메소드로 UsernamePasswordAuthenticationFilter 직전에 실행되도록 설정한다.



3. 인증된 사용자 정보 가져오기


3-1. UserController 수정하기


인증된 사용자 정보를 받아 올 수 있는 api를 추가한다.


@RestController 
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService; 

   @GetMapping("/oauth/token")
   public ResponseEntity getLogin(@RequestParam("code") String code) {

		. . . (생략)

       return ResponseEntity.ok().headers(headers).body("success");
   }

	@GetMapping("/me")
    public ResponseEntity<Object> getCurrentUser(HttpServletRequest request) { //(1)

		//(2)
        User user = userService.getUser(request);
        
		//(3)
        return ResponseEntity.ok().body(user);
    }
}
  1. 새 메소드를 만들어서 HttpServletRequest 를 파라미터로 받아온다.

  2. UserService 에 해당 메소드를 만든다.

  3. ResponseEntity 를 이용해 바디 값에 인증된 사용자 정보를 넘겨준다.



3-2. UserService 수정하기


컨트롤러에서 사용될 getUser() 메소드를 만든다.


@Service
public class UserService {

    @Autowired
    UserRepository userRepository;
    

. . . (생략)

    public User getUser(HttpServletRequest request) { //(1)
    	//(2)
        Long userCode = (Long) request.getAttribute("userCode");

		//(3)
        User user = userRepository.findByUserCode(userCode);

		//(4)
        return user;
    }

    
}
  1. 마찬가지로 HttpServletRequest 를 파라미터로 받는다.

  2. 해당 request 에는 JwtRequestFilter 를 거쳐 인증이 완료된 사용자의 userCode 가 요소로 추가되어 있을 것이므로 이를 활용한다.

  3. 가져온 userCode 로 DB에서 사용자 정보를 가져와 User 객체에 담는다.

  4. User 객체를 반환한다.



도움 받은 영상 :
https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9


마치며.

여기까지 잘 따라왔다면 요청 헤더에 들어 있는 JWT로 유효성 검증을 거쳐 사용자 정보까지 잘 불러올 수 있을 것이다.
응애 개발자가 여러 시행착오를 겪으면서 짜본 코드로 절대 완벽하지 않은 코드임을 알아줬으면 한다. 또한 잘못된 정보가 있을 수도 있으니 지적은 달게 받도록 하겠다.

이상 시리즈를 마치며, 많은 도움이 되었길 바란다 ! ! ~🥳💖

profile
백 개발을 시작한 응애개발자
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 4월 17일

안녕하세요, 게시글을 참고하여 카카오 로그인을 구현중입니다. 근데 자꾸 The token was expected to have 3 parts, but got 1. 이러한 에러가 발생하며 이 에러가 발생하는 부분의 코드는
userCode = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(token)
.getClaim("id").asLong();
이쪽인것 같습니다 ㅜㅜ 어떻게 해결하셨나요?

1개의 답글