필터, AOP 활용 인증, 인가 기능 설계 - ttasjwi/board-system GitHub Wiki
사용자 인증은 어디서?

- Spring MVC 의 모든 웹 요청은 DispatcherServlet 을 거치게되고, 이 앞에 서블릿 필터가 있을 경우 서블릿 필터를 먼저 거친다.
- 서블릿 필터 기반의 인증/인가 프레임워크인 Spring Securiy를 활용하여 인증/인가 기능을 구현한다.
Spring Security 활용 인증

spring-boot-starter-security
를 추가하면 스프링부트가 서블릿 기반의 필터체인을 기본으로 등록해준다.
- 개발자가 커스텀 설정을 통해 커스텀 시큐리티 필터체인을 빈등록하면, 이를 우선시하여 사용하게된다.
internal class AccessTokenAuthenticationFilter(
private val accessTokenParsePort: AccessTokenParsePort,
private val timeManager: TimeManager,
) : OncePerRequestFilter() {
private val bearerTokenResolver: BearerTokenResolver = BearerTokenResolver()
public override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
// 이미 인증됐다면 통과
if (isAuthenticated()) {
filterChain.doFilter(request, response)
return
}
// 헤더를 통해 토큰을 가져옴. 없다면 통과
val tokenValue = bearerTokenResolver.resolve(request)
if (tokenValue == null) {
filterChain.doFilter(request, response)
return
}
// 토큰값을 통해 인증
val authentication = attemptAuthenticate(tokenValue)
// 인증 결과를 SecurityContextHolder 에 저장
saveAuthenticationToSecurityContextHolder(authentication)
// 통과
try {
filterChain.doFilter(request, response)
} finally {
SecurityContextHolder.getContextHolderStrategy().clearContext()
}
}
private fun isAuthenticated() = SecurityContextHolder.getContextHolderStrategy().context.authentication != null
private fun attemptAuthenticate(tokenValue: String): AuthUserAuthentication {
val accessToken = accessTokenParsePort.parse(tokenValue)
val currentTime = timeManager.now()
accessToken.throwIfExpired(currentTime)
return AuthUserAuthentication.from(accessToken.authUser)
}
private fun saveAuthenticationToSecurityContextHolder(authentication: AuthUserAuthentication) {
val securityContext = SecurityContextHolder.getContextHolderStrategy().createEmptyContext()
securityContext.authentication = authentication
SecurityContextHolder.getContextHolderStrategy().context = securityContext
}
}
- 우리 서비스에서는 커스텀 필터인 AccessTokenAuthenticationFilter 를 스프링 시큐리티 필터체인에 추가하였다.
- 이 곳에서 액세스토큰 기반의 인증을 이루어지게 하고, Spring Security 맥락에 맞게, SecurityContextHolder 에 사용자 인증 정보를 저장하게 한다.
- SecurityContextHolder 는 ThreadLocal 기반의 스레드 단위 인증 정보 저장소이다.
- 사용자 요청이 들어올 때마다 ThreadLocal 에 사용자 인증정보를 저장해두고 해당 요청-응답 사이클 내에서 인증정보를 사용할 수 있게한다.
AOP 활용 인가
@Aspect
class MethodAuthorizationAspect(
private val authUserLoader: AuthUserLoader
) {
companion object {
private val ADMIN_ROLES = listOf(Role.ADMIN, Role.ROOT)
}
@Before("@annotation(com.ttasjwi.board.system.common.annotation.auth.PermitAll)")
fun checkPermitAll() {
}
@Before("@annotation(com.ttasjwi.board.system.common.annotation.auth.RequireAuthenticated)")
fun checkAuthenticated() {
authUserLoader.loadCurrentAuthUser() ?: throw UnauthenticatedException()
}
@Before("@annotation(com.ttasjwi.board.system.common.annotation.auth.RequireAdminRole)")
fun checkAdminRole() {
val authUser = authUserLoader.loadCurrentAuthUser() ?: throw UnauthenticatedException()
if (authUser.role !in ADMIN_ROLES) {
throw AccessDeniedException()
}
}
@Before("@annotation(com.ttasjwi.board.system.common.annotation.auth.RequireRootRole)")
fun checkRootRole() {
val authUser = authUserLoader.loadCurrentAuthUser() ?: throw UnauthenticatedException()
if (authUser.role != Role.ROOT) {
throw AccessDeniedException()
}
}
}
- 각 API 마다, 사용자의 권한을 통제해야하는 경우가 있다.
- 어떤 API는 인증된 사용자만 접근 가능하고, 어떤 API는 누구나 접근 가능해야하고, 어떤 API는 관리자만 접근 가능해야한다.
- 하지만 이런 로직을 각 컨트롤러에서 직접 구현하는 것은 불편하고, 비슷한 로직을 여러 곳에서 공통적으로 요청 처리 전에 처리해야한다.
- 이런 공통 횡단관심사를 해결하기 위해
spring-boot-starter-aop
에서 제공하는 AOP 기능을 활용하여 인가작업을 처리했다.
AOP란?

- **AOP(관점 지향 프로그래밍)**은 여러 함수에서 공통적으로 관심을 갖는 **횡단 관심사(Cross-Cutting Concerns)**를 핵심 비즈니스 로직과 분리하여 모듈화할 수 있는 프로그래밍 방법론이다.
- 핵심 로직의 코드를 복잡하게 만들지 않고도, 트랜잭션 처리, 로깅, 보안 검사와 같은 공통적인 부가 기능을 손쉽게 추가할 수 있다.
- 예) 로직 시작 전에 트랜잭션 시작, 종료 후에 커밋/롤백
- 예) 로직 시작 전에 로깅 시작, 종료 후에 로깅 종료
- 예) 로직 시작 전에 사용자 접근 권한 체크
spring-boot-starter-aop
의존성을 추가하면, Spring은 빈 후처리기(BeanPostProcessor) 를 통해 개발자의 부가 로직(Advice)과 핵심 로직을 조합한 프록시 객체를 생성해 빈으로 등록한다. 이 프록시 객체는 Advice를 로직 실행 전후에 자동으로 실행시켜준다.
Spring AOP 용어
@Aspect
는 Spring AOP에서 Aspect(관점) 를 정의할 때 사용하는 애너테이션이다.
- 즉,
@Aspect
는 하나 이상의 Advice(부가 로직) 와 Pointcut(적용 지점) 을 포함하는 모듈이라는 점을 알리는 마커 어노테이션이다.
- 주요 용어
- Aspect: 부가 기능(Advice) + 포인트컷(Pointcut)을 함께 묶은 모듈
- Advice: 실제 실행될 부가 로직
- 실전적 의미:
@Before
, @After
, @Around
등이 걸려있는 메서드
- `Pointcut: Advice가 어디에 적용될지 결정하는 조건
- 실전적 의미:
@Before
, @After
, @Around
어노테이션 속성값으로 기술되는 표현식 -> execution(* com.example..*(..))
- JoinPoint: Advice가 적용될 수 있는 실제 실행 지점
- Advisor: Spring AOP 내부 개념으로, Pointcut + Advice를 함께 포함한 객체 (프록시를 만들 때 사용됨)
- Spring AOP는 프록시 기반이며, 이 프록시에 Advisor가 연결되어 Advice가 자동 실행된다.