Загружаем каталог…
Загружаем каталог…
1️⃣ 인증/인가에 대해 정리하는 계기 JWT를 즐겨 사용하던 중 세션 기반 시스템 고도화에 참여하면서 무조건 JWT를 사용해야 하는 걸까?라는 의문이 들었습니다. 모든 개발자분들은 아시겠지만 트레이드 오프를 피할 정도로 우수한 기술은 없기에 어느 쪽이 좋다라기 보다는 무엇을 위해 무엇을 포기했는지를 정리하면서 의문에 대한 답을 찾아가보려고 합니다. 2️⃣ 인증(Authentication)과 인가(Authorization) 인증(Authentication) 인가(Authorization) 확인하는 것 요청자가 누구인지 요청자가 요청을 해도 되는지 실패 시 HTTP 401 Unauthorized 403 Forbidden Spring Security 관련 객체 AuthenticationManager, AuthenticationProvider AuthorizationManager, @PreAuthorize 순서 인가보다 먼저 인증 후 HTTP의 401 Unauthorized 는 이름과는 달리 인증 안 됨을 의미 하며, 구분해서 사용하지 않으면 클라이언트에서는 재로그인이 필요한 상태인지 권한이 없는 상태인지를 구분할 수 없게 됩니다. 3️⃣ 다양한 인증 방식 방식 상태 위치 즉시 폐기 서버 확장 주요 용도 Session 서버 쉬움 세션 저장소 필요 전통적 웹, 내부 시스템 JWT 클라이언트 어려움 쉬움 모바일, SPA, MSA Opaque Token + Introspection 인증 서버 쉬움 조회 비용 보안 민감 도메인 OAuth2/OIDC IdP에 위임 IdP 정책 IdP 의존 소셜 로그인, SSO API Key/Basic 서버 쉬움 쉬움 서버 간 통신, 내부 도구 여기서 중요한 점은 어떤 방식이 우수하니 정답이다라는 것이 없고 주어진 상황에 알맞는 방식을 선택해야 한다는 것입니다.(한 시스템 안에서도 사용자는 OIDC , 서버 간 호출은 API Key 처럼 섞어 쓰기도 함) 4️⃣ Session 방식의 동작 원리 [클라이언트] [서버] │ POST /login (id, pw) │ │ ───────────────────────────────▶ │ 1. 자격 증명 검증 │ │ 2. 세션 생성 (id=abc123, 사용자 정보 저장) │ Set-Cookie: JSESSIONID=abc123 │ │ ◀─────────────────────────────── │ │ │ │ GET /api/me │ │ Cookie: JSESSIONID=abc123 │ │ ───────────────────────────────▶ │ 3. 세션 ID로 저장소 조회 → 사용자 식별 │ 200 OK │ │ ◀─────────────────────────────── │ 클라이언트는 쿠키를 통해 세션 ID(의미 없는 랜덤 문자열) 를 가짐 사용자 정보는 전부 서버 가 가지며, 서버가 세션을 지우면 즉시 로그아웃 됨 즉시 폐기가 가능한 점이 강점 이며 최근에 많이 사용되는 JWT에서는 문제로 지적됨 5️⃣ Spring Security + Kotlin 구현 의존성(Gradle Kotlin DSL) dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-data-jpa") } 사용자 조회(UserDetailsService) @Service class CustomUserDetailsService( private val userRepository: UserRepository ): UserDetailsService { override fun loadUserByUsername(username: String): UserDetails { val user = userRepository.findByUsername(username) ?: throw UsernameNotFoundException("user not found") return User.builder() .username(user.username) .password(user.password) .roles(*user.roles.map { it.name }.toTypedArray()) .build() } } 보안 설정(SecurityFilterChain) @Configuration @EnableWebSecurity class SecurityConfig { @Bean fun passwordEncoder(): PasswordEncoder { return PasswordEncoderFactories.createDelegatingPasswordEncoder() } @Bean fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager { return config.authenticationManager } @Bean fun securityContextRepository(): SecurityContextRepository { return HttpSessionSecurityContextRepository() } @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { http { csrf { // SPA라면 쿠키 기반 CSRF 토큰을 JS에서 읽어 헤더로 전달 csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() } authorizeHttpRequests { authorize("/api/auth/login", permitAll) authorize("/api/admin/**", hasRole("ADMIN")) authorize(anyRequest, authenticated) } sessionManagement { sessionCreationPolicy = SessionCreationPolicy.IF_REQUIRED sessionConcurrency { maximumSessions = 1 // 동시 로그인 세션 1개로 제한 maxSessionsPreventsLogin = false // false: 기존 세션 만료(후발 로그인 우선) } } exceptionHandling { // 401: 인증 필요 authenticationEntryPoint = AuthenticationEntryPoint { _, res, _ -> res.status = HttpStatus.UNAUTHORIZED.value() } accessDeniedHandler = AccessDeniedHandler { _, res, _ -> res.status = HttpStatus.FORBIDDEN.value() | } logout { logoutUrl = "/api/auth/logout" invalidateHttpSession = true deleteCookies("JSESSIONID") logoutSuccessHandler = HttpStatusReturningLogoutSuccessHandler() } } return http.build() } } exceptionHandling 을 지정하지 않으면 REST API 에서도 로그인 페이지로 redirect 되어 SPA, 모바일에서는 의도하지 않은 동작이 될 수 있으므로 API 서버라면 401/403을 명시 해야 합니다. 로그인 API @RestController @RequestMapping("/api/auth") class AuthController( private val authenticationManager: AuthenticationManager, private val securityContextRepository: SecurityContextRepository ) { private val securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy() @PostMapping("/login") fun login( @RequestBody request: LoginRequest, req: HttpServletRequest, res: HttpServletResponse ): ResponseEntity<Unit> { val token = UsernamePasswordAuthenticationToken.unauthenticated(req.username, req.password) val authentication = authenticationManager.authenticate(token) // 세션 고정 공격 방어를 위해 로그인 성공 시 세션 ID 교체 if(request.getSession(false) != null) { request.changeSessionId() } val context = securityContextHolderStrategy.createEmptyContext() context.authentication = authentication securityContextHolderStrategy.context = context // Security 6부터는 명시적으로 저장해야 세션에 반영됨 securityContextRepository.saveContext(context, request, response) return ResponseEntity.ok().build() } } 직접 API 구성 시 중요한 점 saveContext 호출 : Spring Security 6부터 SecurityContext 가 자동으로 세션에 저장되지 않으므로 직접 저장이 필요 세션 고정(Session Fixation) 방어 : 기본 폼 로그인 필터는 로그인 성공 시 자동으로 세션 ID를 바꿔 주지만 직접 API를 구성할 때는 해당 로직이 동작하지 않으므로 명시적으로 changeSessionId() 호출 필요 쿠키 보안 설정 server: servlet: session: timeout: 30m cookie: http-only: true # JS통한 쿠기 접근 차단(XSS 세션 탈취 방지) secure: true # HTTPS에서만 전송 same-site: lax # 크로스 사이트 요청 시 쿠키 제한 서버가 여러 대일 경우(Spring Session + Redis) 기본적으로 세션은 WAS의 메모리에 저장되므로 서버가 2대 이상이 되면 문제가 발생합니다. 요청 1 -> 서버 A(로그인, 세션 생성) 요청 2 -> 서버 B(세션 없음: 401) 대표적인 해결책 방식 설명 평가 Sticky Session LB가 같은 사용자는 같은 서보로 고정 간단하지만 서버 장애 시 세션이 소실될 수 있고 부하가 한쪽으로 쏠림 Session Clustering WAS끼리 세션 복제 노드가 늘수록 복제 비용 증가 외부 세션 저장소 Redis 등에 세션 저장 서버는 무상태 유지되고 가장 일반적인 방법이지만 외부 저장소와의 결합도 증가 dependencies { implementation("org.springframework.session:spring-session-data-redis") implementation("org.springframework.boot:spring-boot-starter-data-redis") } spring: data: redis: host: localhost port: 6279 session: timeout: 30m @Configuration @EnableRedisHttpsession // 자동 구성으로 생략이 가능하지만 적용됨을 표시하기 위해 활용 class RedisSessionConfig 자동 구성을 통해 코드를 따로 작성할 필요 없이 세션 저장소가 Redis 로 변경됩니다.(서블릿 컨테이너의 HttpSession 을 Spring Session 이 가로채 Redis 에 저장) 중요한 점은 서버 무상태를 위해 JWT 를 활용하는데 Redis 세션 을 사용하는 순간 세션 방식으로도 무상태의 장점을 얻게 됩니다. 하지만, 앞서 잠깐 언급된 것 처럼 모든 요청이 외부 저장소인 Redis 를 통하므로 SPOF 이자 병목 후보가 될 수 있습니다. 6️⃣ Session 방식의 위협과 방어 위협 설명 방어 XSS 스크립트 주입으로 쿠키 탈취 HttpOnly 쿠기, 출력 이스케이프, CSP CSRF 로그인된 브라우저가 의도치 않은 요청 전송 CSRF 토큰, SameSite 쿠키 Session Fixation 공격자가 미리 알고 있는 세션 ID를 피해자에게 심음 로그인 시 세션 ID 교체 Session Hijacking 세션 ID 도청/탈취 HTTPS 강제, Secure 쿠기, 짧은 timeout Session은 쿠키를 브라우저가 자동으로 붙여주기 때문에 CSRF에 취약하고 JWT를 Authorization 헤더로 보내면 CSRF에는 강할 수 있지만 토큰 저장 위치(XSS)가 문제가 됩니다. 즉, 어느 인증 방식을 선택하더라도 위협에서 자유롭지 않다는 것은 인지하는 것이 중요합니다. 7️⃣ Anti Pattern 비밀번호를 평문/단순 해시로 저장하지 않고 BCrypt , Argon2 등 적응형 해시 + DelegatingPasswordEncoder 로 알고리즘 교체 가능성을 확보 로그인 성공 후 세션 ID를 교체하여 세션 고정 공격을 방지 API 서버에서 CSRF를 무작정 disable() 하지 않아야 하고 쿠키 기반이면 CSRF 방어가 필요 세션에 큰 객체를 저장하면 메모리 부하가 발생하고 직렬화/역직렬화 문제가 발생할 수 있기 때문에 세션에는 식별자와 최소 정보만 담는다 로그인 실패 시 메시지를 추상화하여 계정 존재 여부를 알 수 없도록 해야 함(ex: 아이디 또는 비밀번호가 일치하지 않습니다.) 로그인 API에 Rate Limiting 을 도입하여 무차별 대입 공격에 대비 필요 8️⃣ 고민해볼 수 있는 질문 세션 기반 인증에서 서버를 여러 대로 늘릴 때 발생할 수 있는 문제와 해결방법 세션 고정 공격이란? 그리고 해결 방법은?
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
인증/인가와 Session 방식(Spring Boot + Kotlin). 1️⃣ 인증/인가에 대해 정리하는 계기 JWT를 즐겨 사용하던 중 세션 기반 시스템 고도화에 참여하면서 무조건 JWT를 사용해야 하는 걸까?라는 의문이 들었습니다. 모든 개발자분들은 아시겠지만 트레이드 오프를 피할 정도로 우수한 기술은 없기에 어느 쪽이 좋다라기 보다는 무엇을 위해 무엇을 포기했는지를 정리하면서 의문에 대한 답을 찾아가보려고 합니다. 2️⃣ 인증(Authentication)과 인가(Authorization) 인증(Authentication) 인가(Authorization) 확인하는 것 요청자가 누구인지 요청자가 요청을 해도 되는지 실패 시 HTTP 401 Unauthorized 403 Forbidden Spring…
Открыть источник