스프링 시큐리티는 쉬운 인증 인가 구현과 보안을 구현해주는 프레임워크라고 널리 알려져있다. 그러나 나는 항상 개발을 하면서 스프링 시큐리티에 대한 반감이 있었다. 그래서 이번엔 그 반감의 원인을 파악해보고, 과연 스프링 시큐리티를 사용하는 것이 올바른 생각인지 확인해보겠다.
나는 왜 반감이 있는가?
반감의 이유를 한줄로 정리하자면, 스프링 시큐리티가 해주는게 없다고 생각해서이다. 내가 스프링 시큐리티가 해주는게 없다고 느낀 이유는 JWT 인증 방식만 사용해왔기 때문이다.
스프링 시큐리티와 JWT를 사용해서 인증 인가를 구현하고자 한다면, 정말 스프링 시큐리티가 해주는게 없다고 느껴진다. 직접 JWT 필터를 구현해서, 약 12개나 되는 필터 사이에 끼워서 사용한다. 이 과정에서 스프링 시큐리티에 대한 팀원 전체의 이해도가 필요하다. 만약 전체가 이해를 한다고 생각해도, 스프링 시큐리티를 사용할 때의 코드량이 더 많아서 좋지 않다고 느껴진다.
코드가 더 많아진다는 나의 근거
일단 기본적인, JWT 발급 및 해독 로직은 동일하다.
차이가 나는 부분은
스프링 시큐리티를 쓰면 추가되는 것
1. Spring Security 설정 파일
2. JWT 처리 필터 추가 로직
3. UserDetails 및 UserDetailsService 구현
개인적으로 구현하면 추가해야하는 것
1. Interceptor 구현
2. SecurityContextHolder와 비슷한 역할을 하는 Repository
3. 컨트롤러별 접근 권한을 관리하기 위한 어노테이션
정도로 보고있는데, 시큐리티를 사용하고 추가해야하는 것이 훨씬 복잡하고, 장황한 코드라고 생각한다.
그리고 스프링 시큐리티가 지원하는 기본적인 보안과 관련된 것들은 JWT가 아닌 세션을 사용할 때나 유용하다. 스프링 시큐리티 홈페이지에 가보면
- Protection against attacks like session fixation, clickjacking, cross site request forgery, etc
다음과 같은 글을 볼 수 있는데, 모두 세션과 관련된 보안이다. 그렇기에 security config 파일에서 csrf.disable() 코드를 어렵지 않게 볼 수 있다.
사용하지 않는다면, 어떻게 인증, 인가를 처리할 것인가?
스프링 시큐리티 필터체인 중, 우리가 유일하게 필요로하는 JWT 필터와 동일한 작업을 하는 Interceptor로 해결할 수 있다.
그렇다면, Spring Security에서 Filter로 지원하는 것을 굳이 Interceptor로 지원하려고 하는지 알아보자.
Filter를 사용하면, DispathcerServlet전에 작동함으로, 어떤 Controller를 호출할 것인지 알 수 없다. 그렇기에, Spring Security 설정 파일에 다음과 같이 Path로 관리할 수 밖에 없는 것이다.
그러나 Interceptor를 사용하면, 어떤 컨트롤러의 어떤 메서드를 호출할 것인지 알 수 있고,
다음과 같이 어노테이션으로 관리할 수 있게 된다. 그렇기에 Interceptor를 구현했다. (약간의 성능 저하가 예상되지만 크게 고려할 만큼의 수치로 예상되진 않는다.)
코드를 보자면 아래와 같다.
@Configuration
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final JwtParser jwtParser;
private final AuthUpdater authUpdater;
private final AuthReader authReader;
private final UserReader userReader;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod hm) {
if (hm.hasMethodAnnotation(LoginOrNot.class)) {
String bearer = request.getHeader(AUTHORIZATION);
if (bearer == null) {
authUpdater.updateCurrentUser(null);
} else {
String jwt = BearerTokenExtractor.extract(bearer);
Long userId = jwtParser.getIdFromJwt(jwt);
User user = userReader.readUser(userId);
authUpdater.updateCurrentUser(user);
}
}
if (hm.hasMethodAnnotation(LoginRequired.class)) {
if (authReader.getCurrentUser() == null) {
throw new TokenNotExistException();
}
}
if (hm.hasMethodAnnotation(AdminOnly.class)) {
User currentUser = authReader.getCurrentUser();
shouldUserAdmin(currentUser);
}
}
return true;
}
private static void shouldUserAdmin(User currentUser) {
if (currentUser.getRole() != Role.ADMIN) {
throw new UserIsNotAdminException();
}
}
}
그렇다면, JWT를 가지고 접근한 유저가 누군지 알아보기 위해서, Spring Security Context Holder와 같은 작업을 하는 것을 구현해야한다.
그 작업을 하는 클래스를 나는 AuthRepository라고 이름을 지었고, 코드는 아래와 같다.
@Repository
@RequestScope
public class AuthRepository {
private User currentUser;
public User getCurrentUser() {
if (currentUser == null) {
throw new UserNotLoginException();
}
return currentUser;
}
public User getNullableCurrentUser() {
return currentUser;
}
public void updateCurrentUser(User currentUser) {
this.currentUser = currentUser;
}
}
주목해야할 부분은 @RequestScope인데, 그 이유는 스프링 빈은 싱글톤이기 때문에, 저 AuthRepository의 빈이 여러 요청에서 존재하다가, current의 유저의 값에 모순이 발생할 수 있다. 그렇기에, Spring Security Context와 같이 각각의 요청을 생명 주기로하는 빈을 생성하면, 값에 모순 없이 사용할 수 있다.
더 자세히 알아보고 싶다면, https://github.com/sickgyun/sickgyun-server/tree/master/src/main/java/com/sickgyun/server/auth 깃허브에 코드가 있으니 편하게 확인하면 좋을 것 같다.
일단 나의 현재 생각은 이렇다. 내 말이 무조건 맞는 말도 아니고, 세션을 사용한다면 Spring Security는 너무 편리한 프레임워크이다. 저의 글에 잘못된 내용이나, 다른 생각을 가지고 있다면 주저하지 말고, 댓글을 달아주기를 간절히 요청드립니다!!
그리고 같이 고민해주신 호현님께 감사의 말씀드립니다!
'기술고민' 카테고리의 다른 글
Fixture Monkey를 써야 할까? (0) | 2024.04.16 |
---|---|
bootJar 와 jar 각자 어떤 책임이 있을까? (0) | 2024.04.14 |
SonarCloud, Qodana 둘중 무엇을 골라야할까? (1) | 2024.04.05 |
int와 Integer중에 무엇을 골라야할까? (4) | 2024.03.28 |
???: 빌더 패턴은 필수 값을 받지 못하잖아요. (3) | 2024.03.26 |