JWT를 이용한 인증 처리 3
- -
다음으로 JwtUtil을 사용할 UserService와 JwtInterceptor를 만들어보자.
소스 작성
User
먼저 사용자 정보를 가지고 다닐 DTO로 User를 생성한다.
package xyz.quietjun.jwttest.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private String email;
private String pass;
private String authToken; // 사용자 인증 정보 토큰
private String refreshToken; // authToken 갱신을 위한 토큰
}
UserService
다음은 비지니스 로직을 가지는 UserService를 생성해보자. 서비스 부분은 간단히 작성하기 위해서 class 기반으로 바로 작성했지만 당연히 interface 기반으로 시작하는 것이 좋다.
package xyz.quietjun.jwttest.model.service;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.quietjun.jwttest.jwt.JwtUtil;
import xyz.quietjun.jwttest.model.dto.User;
@Service
public class UserService {
@Autowired
private JwtUtil jwtUtil;
/**
* 로그인 계정은 편의상 email은 member@quietjun.xyz, pass는 1234로 하지만
* 실제로는 당연히 Repository를 통해서 처리
*
* @param email
* @param password
* @return
*/
public User signin(String email, String password) {
if (email.equals("member@quietjun.xyz") && password.equals("1234")) {
// 인증 성공 시 authToken 생성
String authToken = jwtUtil.createAuthToken(email);
return User.builder().email(email).authToken(authToken).build();
} else {
throw new RuntimeException("그런 사람은 없어요~");
}
}
/**
* 사용할 서비스
*
* @return
*/
public String getServerInfo() {
return String.format("여기는 은서파, 지금 시각은 %s", new Date());
}
}
Controller
다음은 위의 서비스를 사용하는 Controller이다. 로그인 성공 시 클라이언트 쪽으로 jwt-auth-token을 보내주고 있다는 점을 기억해주자. 나머지 부분은 크게 다를바가 없다.
package xyz.quietjun.jwttest.controller;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
import xyz.quietjun.jwttest.jwt.JwtUtil;
import xyz.quietjun.jwttest.model.dto.User;
import xyz.quietjun.jwttest.model.service.UserService;
@RestController
@RequestMapping("/api")
@Slf4j
@CrossOrigin(origins = "*")
public class UserRestController {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtService;
@PostMapping("/user/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody User user, HttpServletResponse res, HttpServletRequest req) {
Map<String, Object> resultMap = new HashMap<>();
User loginUser = userService.signin(user.getEmail(), user.getPass());
// 생성된 토큰 정보를 클라이언트에게 전달한다.
resultMap.put("jwt-auth-token", loginUser.getAuthToken());
// 실제로는 필요 없지만 정보 확인차 클라이언트로 보내본다.
Map<String, Object> info = jwtService.checkAndGetClaims(loginUser.getAuthToken());
resultMap.putAll(info);
return new ResponseEntity<Map<String, Object>>(resultMap, HttpStatus.ACCEPTED);
}
@GetMapping("/info")
public ResponseEntity<Map<String, Object>> getInfo(HttpServletRequest req) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("info", userService.getServerInfo());
return new ResponseEntity<Map<String, Object>>(resultMap, HttpStatus.ACCEPTED);
}
}
UserExceptionHandler
다음은 오류가 발생했을 때 정보를 처리하기 위한 UserExceptionHandler를 작성해보자. 여기서는 ExpiredJwtException만 처리해보자.
package xyz.quietjun.jwttest.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
@RestControllerAdvice
@Slf4j
public class UserExceptionHandler {
@ExceptionHandler(ExpiredJwtException.class)
public ResponseEntity<String> handleError(ExpiredJwtException e) {
log.error("jwt token expired", e);
return new ResponseEntity<String>(e.getMessage(), HttpStatus.UNAUTHORIZED);
}
}
JwtInterceptor
다음은 Controller 호출에 앞서 토큰 정보를 검사할 Interceptor이다. 요청 헤더에서 jwt-auth-token 값을 확인해서 valid 하면 그대로 진행하고 아니면 예외 처리해버렸다. 주의할 점은 cors 처리를 위를 위한 preflight(OPTIONS) 요청에는 header에 정보를 넘겨주지 않을 것이기 때문에 OPTIONS는 바로 통과시켜 준다.
package xyz.quietjun.jwttest.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import lombok.extern.slf4j.Slf4j;
import xyz.quietjun.jwttest.jwt.JwtUtil;
@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// preflight를 위한 OPTIONS 요청은 그냥 전달
if(request.getMethod().equals("OPTIONS")) {
return true;
}
// request의 헤더에서 jwt-auth-token으로 넘어온 녀석을 찾아본다.
String authToken = request.getHeader("jwt-auth-token");
log.debug("경로: {}, 토큰: {}", request.getServletPath(), authToken);
if (authToken != null) {
// 유효한 토큰이면 진행, 그렇지 않으면 예외를 발생시킨다.
jwtUtil.checkAndGetClaims(authToken);
return true;
} else {
throw new RuntimeException("인증 토큰이 없습니다.");
}
}
}
Interceptor 동작 설정
마지막으로 Interceptor의 사용을 설정해보자. @SpringBootApplication 클래스 자체가 @Configuration을 가지고 있기 때문에 WebMvcConfigurer를 implements 받고 addInterceptors 매서드를 재정의해주자.
package xyz.quietjun.jwttest;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import xyz.quietjun.jwttest.interceptor.JwtInterceptor;
@SpringBootApplication
public class JwttestApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(JwttestApplication.class, args);
}
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor).addPathPatterns("/api/**") // 기본 적용 경로
.excludePathPatterns(Arrays.asList("/api/user/**"));// 적용 제외 경로
}
}
동작 테스트
다음은 단위 테스트로 위 과정을 살펴보자.
package xyz.quietjun.jwttest;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWithIgnoringCase;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.MalformedJwtException;
import lombok.extern.slf4j.Slf4j;
import xyz.quietjun.jwttest.jwt.JwtUtil;
@SpringBootTest
@AutoConfigureMockMvc
@Slf4j
class ControllerTest {
@Autowired
MockMvc mock;
@Autowired
JwtUtil util;
@Test
@DisplayName("정보를 전달해서 로그인 후 토큰을 잘 얻을 수 있는지 확인한다.")
public void testLogin() throws Exception {
// given
Map<String, String> map = new HashMap<>();
map.put("email", "member@quietjun.xyz");
map.put("pass", "1234");
String content = new ObjectMapper().writeValueAsString(map);
// when
MockHttpServletRequestBuilder reqBuilder
= post("/api/user/signin").contentType("application/json").content(content);
ResultActions action = mock.perform(reqBuilder);
// then
action.andExpect(status().is(202))
.andExpect(jsonPath("$.sub", equalTo("authToken")))
.andExpect(jsonPath("$.user", equalTo(map.get("email"))));
}
@Test
@DisplayName("정상적인 토큰을 전달했을 때 원하는 정보를 잘 얻을 수 있는지 확인한다.")
public void testGetInfoSuccess() throws Exception {
// given
String token = util.createAuthToken("member@quietjun.xyz");
// when
MockHttpServletRequestBuilder reqBuilder
= get("/api/info").header("jwt-auth-token", token);
ResultActions action = mock.perform(reqBuilder);
// then
action.andExpect(status().is(202))
.andExpect(jsonPath("$.info", startsWithIgnoringCase("여기는 은서파, 지금 시각은 ")));
}
@Test
@DisplayName("비 정상적인 토큰을 전달했을 때 예외가 발생하는지 확인한다.")
public void testGetInfoFail() throws Exception {
// given
String token = "malfomed token";
// when
MockHttpServletRequestBuilder reqBuilder
= get("/api/info").header("jwt-auth-token", token);
// then
assertThrows(Exception.class, () -> {
mock.perform(reqBuilder);
});
}
}
'Spring security > 02.JWT활용' 카테고리의 다른 글
JWT를 이용한 인증 처리 6. Refresh Token 구현 (0) | 2022.05.05 |
---|---|
JWT를 이용한 인증 처리 5. refresh 토큰 개념 (0) | 2022.04.28 |
JWT를 이용한 인증 처리 4 (0) | 2022.04.28 |
JWT를 이용한 인증 처리 2 (7) | 2019.11.24 |
JWT를 이용한 인증 처리 1 (0) | 2019.11.22 |
소중한 공감 감사합니다