서론.
해당 프로젝트에서는 오로지
카카오 소셜 로그인
만 사용한다. ( ID/PW 사용안함! )
이전 포스팅✅에서는 JWT 를 직접 생성해 응답 헤더에 넣어보았다. 이번에는 요청 헤더에 들어온 JWT의 유효성을 검사하고, 인증된 사용자의 정보를 불러오도록 하자.
기본 의존성 :
Spring Web
Spring boot DevTools
Lombok
Spring Security
MySQL Driver
spring Data JPA
추가한 의존성 :
jackson-databind
jackson-datatype-jsr310
java-jwt
프로젝트를 진행하던 당시 나는 이부분에서 삐끗했다.😕 응답 헤더에 토큰이 들어간 상태로 프론트에 잘 넘어갔는데 정보를 읽어올 수가 없다는 것이다.
열심히 구글링을 해보니 이 설정이 빠져 있었다.
@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);
}
}
.addAllowedHeader("*")
.addExposedHeader("*")
꼭꼭 설정해주자!!⭐⭐
만들어둔 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);
}
}
@Component
어노테이션을 달아준다.
UserRepository
를 @Autowired
한다.
doFilterInternal
메소드를 오버라이드 한다.
요청 헤더의 Authorization
항목 값을 가져와 jwtHeader
변수에 담는다.
만약 jwtHeader
가 null
이거나 Bearer
로 시작하지 않으면 return;
으로 이후 로직을 실행시키지 않고 넘긴다.
jwtHeader
가 제대로 된 형식이라면 토큰 앞의 Bearer
를 떼어내 token
변수에 담는다.
token
을 비밀 키로 복호화하는 동시에 개인 클레임
에 넣어뒀던 id
값을 가져온다. 이 코드 자체가 인증과정이므로 이어지는 catch
문에서 Exception
처리를 해준다.
userCode
에 값이 잘 담겼다면 request.setAttribute("userCode", userCode)
로 값을 넘긴다.
filterCahin
에 request
response
값을 넘긴다.
.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);
}
}
JwtRequestFilter
에서 Exception 발생시 request 에 추가한 요소를 불러와 담아준다.
스테이터스, 콘텐트 타입, 오류 메세지를 담아 응답해주는 메소드를 만들어 사용한다.
※ 예외처리에 관한 문제는 코드에 오류가 있을 수도 있다. 토큰 만료 기간을 길게 잡아 놔서 직접 테스트해보지 못했다 ! ! ※
위 클래스를 모두 만들었다면 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);
}
.ExceptionHandling()
으로 클래스를 등록해준다.
.addFilterBefore()
메소드로 UsernamePasswordAuthenticationFilter
직전에 실행되도록 설정한다.
인증된 사용자 정보를 받아 올 수 있는 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);
}
}
새 메소드를 만들어서 HttpServletRequest
를 파라미터로 받아온다.
UserService
에 해당 메소드를 만든다.
ResponseEntity
를 이용해 바디 값에 인증된 사용자 정보를 넘겨준다.
컨트롤러에서 사용될 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;
}
}
마찬가지로 HttpServletRequest
를 파라미터로 받는다.
해당 request
에는 JwtRequestFilter
를 거쳐 인증이 완료된 사용자의 userCode
가 요소로 추가되어 있을 것이므로 이를 활용한다.
가져온 userCode
로 DB에서 사용자 정보를 가져와 User
객체에 담는다.
User
객체를 반환한다.
도움 받은 영상 :
https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9
마치며.
여기까지 잘 따라왔다면 요청 헤더에 들어 있는 JWT로 유효성 검증을 거쳐 사용자 정보까지 잘 불러올 수 있을 것이다.
응애 개발자가 여러 시행착오를 겪으면서 짜본 코드로 절대 완벽하지 않은 코드임을 알아줬으면 한다. 또한 잘못된 정보가 있을 수도 있으니 지적은 달게 받도록 하겠다.
이상 시리즈를 마치며, 많은 도움이 되었길 바란다 ! ! ~🥳💖
안녕하세요, 게시글을 참고하여 카카오 로그인을 구현중입니다. 근데 자꾸 The token was expected to have 3 parts, but got 1. 이러한 에러가 발생하며 이 에러가 발생하는 부분의 코드는
userCode = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(token)
.getClaim("id").asLong();
이쪽인것 같습니다 ㅜㅜ 어떻게 해결하셨나요?