Spring boot 기반 REST API 제작 (5)

Asterisk
8 min readOct 28, 2018

--

PREV: Spring boot 기반 REST API 제작 (4)

이번 아티클에서는 Spring boot 기반 REST API 제작 (3) 에서 잠깐 언급되고 지나쳤던 Interceptor를 사용하여 인증을 수행하는 부분을 구현해볼까 한다.

Interceptor는 Handler(Controller)의 수행 전 후의 요청을 intercept하여 처리할 때 사용된다.
이렇게 들으면 Interceptor가 Handler에 적극적인 간섭을 하는 것 같지만 실제로는 아래의 그림과 같이 수행되는 순서가 정의되어 있을 뿐이다.

Spring MVC Request Lifecycle Diagram

우선 UserService에서 인증을 담당하는 부분을 분리하여 별도의 AuthenticationService를 만든다.

public interface AuthenticationService {

User authenticate(String token);
}

위의 인터페이스에 대한 구현은 다음과 같다.

@Service
public class AuthenticationServiceImpl implements AuthenticationService {

private final UserRepository userRepository;

@Autowired
public AuthenticationServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Override
public User authenticate(String token) {
try {
// authorization으로부터 type과 credential을 분리
String[] split = token.split(" ");
String type = split[0];
String credential = split[1];

if ("Basic".equalsIgnoreCase(type)) {
// credential을 디코딩하여 username과 password를 분리
String decoded = new String(Base64Utils.decodeFromString(credential));
String[] usernameAndPassword = decoded.split(":");

User user = userRepository.findByUsernameAndPassword(usernameAndPassword[0], usernameAndPassword[1]);
if (user == null)
throw new UnauthorizedException("Invalid credentials");
else
return user;

} else {
throw new UnauthorizedException("Unsupported type: " + type);

}

} catch (IllegalArgumentException | ArrayIndexOutOfBoundsException ex) {
throw new UnauthorizedException("Invalid credentials");
}
}
}

UserService에서 인증과 관련된 부분을 cmd+x, cmd+v (혹은 ctrl+x, ctrl+v) 해준것과 다름없다.

이제 Interceptor에서 AuthenticationService를 사용하여 인증을 수행하는 AuthenticationInterceptor를 만들어보자.

@Component
public class AuthenticationInterceptor extends HandlerInterceptorAdapter {
@Autowired
private final AuthenticationService authenticationService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

// 헤더로부터 토큰을 읽어서
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
// 인증과정을 수행하고
User user = authenticationService.authenticate(token);
// 그 결과를 request attribute으로 넘겨준다.
request.setAttribute("user", user);

return super.preHandle(request, response, handler);
}
}

요청이 매핑된 Handler(Controller)에 도달하기 전에 인증 과정을 먼저 수행해야하기 때문에 preHandle() 메서드를 재정의하였다.

이렇게 만들어진 AuthenticationInterceptor는 어플리케이션에 인터셉터로 등록해 주어야 동작하기 때문에 아래와 같이 구성을 정의해주어야 한다. addPathPatterns() excludePathPatterns() pathMatcher() 메서드를 통해 path 별로 Interceptor를 적용할 범위를 지정할 수 있는데 아래의 구성에서는 /users/me path에서만 적용되도록 지정하였다.

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

@Autowired
private AuthenticationInterceptor authenticationInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor)
// path가 /user/me인 요청에 대해서만 적용
.addPathPatterns("/users/me");
}
}

회원가입(POST /users)을 제외한
정보조회(GET /users/me),
정보갱신(PUT /users/me),
회원탈퇴(DELETE /users/me)에만 interceptor가 적용된다.

앞에서 작성한 interceptor에서 Authorization Header를 통해 전달받은 토큰으로 인증과정을 거쳐 User를 알아내어 request에 attribute로 넘겨주었다.

따라서 Controller에서는 더 이상 인증을 수행할 필요없이 전달받은 request attribute를 사용하면 되기 때문에 UserController의 일부 메서드들은 아래와 같이 수정한다.

// 자신의 정보를 반환
@GetMapping(value = "/me")
public User getMe(@RequestAttribute User user) {
return user;
}

// 자신의 비밀번호를 갱신한 뒤 그 결과를 반환
@PutMapping(value = "/me")
public User updatePassword(@RequestAttribute User user, @RequestParam String password) {
return userService.updatePassword(user.getId(), password);
}

// 탈퇴
@DeleteMapping(value = "/me")
public void withdraw(@RequestAttribute User user) {
userService.withdraw(user.getId());
}

위와 같이 인증 부분을 별도의 Service와 Interceptor로 분리한 결과,
인증 방식이 변경되어도 UserControllerUserService를 수정할 필요가 없어졌다.

여지껏 User라는 단일 resource만 사용되어서 크게 와닿지않을지 모른다. 하지만 인증을 통해 접근을 제한하여야하는 다양한 resource와 맵핑이 존재한다면 인증방식 혹은 정책의 변경이 가져오는 코드상의 변경점 또한 늘어난다. 그리고 우리 모두가 알고 있듯이 변경점이 늘어나면 오류가 발생할 확률 또한 늘어난다.

--

--