Spring Security : 세션 정책

이진욱·2024년 12월 11일

이론

목록 보기
7/24

Spring Security의 SessionManagementConfigurer에서 사용하는 createPolicy는 세션 생성 정책을 정의합니다. 이는 애플리케이션이 인증 및 세션 관리를 어떻게 처리할지 설정할 때 유용합니다.

createPolicy는 기본적으로 다음과 같은 옵션을 제공합니다.

SessionCreationPolicy 옵션들

ALWAYS

사용자가 요청을 보낼 때마다 항상 세션을 생성합니다.
새로운 인증이 발생하지 않더라도 세션이 존재하지 않으면 새 세션을 만듭니다.
예: 상태 유지를 위한 세션이 반드시 필요한 애플리케이션.

.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
)

NEVER

Spring Security가 세션을 절대 생성하지 않습니다.
기존 세션이 존재한다면 사용하지만, 새로운 세션은 생성하지 않습니다.
예: 세션 사용을 최소화하고 싶은 애플리케이션.

.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.NEVER)
)

IF_REQUIRED (기본값)

세션이 필요한 경우에만 생성합니다.
예: 인증 또는 권한 검사를 위해 세션이 필요할 때 생성.
대부분의 애플리케이션에서 기본 설정으로 적합합니다.

.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
)

STATELESS

애플리케이션이 세션을 전혀 사용하지 않습니다.
인증 정보를 완전히 토큰 기반(예: JWT)으로 처리합니다.
예: RESTful API와 같은 상태 비저장(stateless) 애플리케이션.

.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)

SessionCreationPolicy 사용 시 고려 사항


  • STATELESS

세션을 전혀 생성하지 않으므로 서버의 리소스 소비가 줄어듭니다.
RESTful 서비스에서 권장됩니다.
무상태 인증 방식(JWT, OAuth2 등)을 구현해야 합니다.

  • IF_REQUIRED

기본값으로, 대부분의 일반 웹 애플리케이션에서 사용됩니다.
상태 기반 인증 방식을 사용할 때 적합합니다.

  • ALWAYS

항상 세션을 생성하기 때문에 리소스 사용량이 증가할 수 있습니다.
특정 비즈니스 로직에서 상태를 지속적으로 유지해야 하는 경우에만 사용하세요.

  • NEVER

기존 세션이 존재하지 않는 경우 오류를 방지하려면 신중하게 사용해야 합니다.

결론

  • SessionCreationPolicy는 세션 생성 및 관리를 제어하는 설정입니다.
  • RESTful API에서는 STATELESS를, 일반 웹 애플리케이션에서는 기본값인 IF_REQUIRED를 사용하는 것이 일반적입니다.
  • 설정은 애플리케이션의 인증 요구 사항과 상태 관리 방식에 따라 선택해야 합니다.

상태 인증 방식

상태 기반 인증은 서버가 클라이언트의 인증 상태를 세션으로 관리하는 방식입니다. 서버가 사용자의 인증 정보를 서버 측에 저장하고, 이후 요청 시 이를 참조하여 사용자를 인증합니다. 이는 전통적인 웹 애플리케이션에서 많이 사용되는 인증 방식입니다.

동작 원리

1. 로그인 요청:
클라이언트가 사용자 자격 증명(예: 사용자 ID와 비밀번호)을 서버에 전송합니다.

2. 인증 성공:
서버는 자격 증명이 유효하면, 사용자 정보를 기반으로 세션을 생성하고 고유한 세션 ID를 생성합니다.
이 세션 ID는 클라이언트의 쿠키에 저장되어 서버로 전송됩니다.

3. 요청 처리:
이후 클라이언트는 요청마다 쿠키에 포함된 세션 ID를 서버에 전송합니다.
서버는 세션 저장소에서 해당 세션 ID를 조회하여 사용자를 인증합니다.

4. 로그아웃:
사용자가 로그아웃하면 서버는 세션을 삭제하고 클라이언트의 쿠키에서 세션 ID를 제거합니다.

특징

서버는 클라이언트의 인증 상태를 유지하기 위해 세션 저장소(메모리, 데이터베이스 등)를 사용합니다.
세션 데이터는 사용자 정보를 포함할 수 있으며, 이를 기반으로 사용자 권한 등을 관리합니다.

장점

  • 단순한 구현:
    세션 관리가 서버 측에서 이루어지므로 클라이언트 측에서 복잡한 처리가 필요하지 않습니다.
  • 높은 보안성:
    서버가 인증 상태를 제어하므로 클라이언트의 신뢰도를 낮게 설정할 수 있습니다.
  • 서버 제어:
    사용자의 세션을 서버에서 관리하고 만료시킬 수 있어 보다 강력한 관리가 가능합니다.

단점

  • 서버 부담 증가:
    많은 사용자가 동시 접속하면, 세션 저장소의 부하가 커지고 서버의 메모리나 스토리지를 많이 사용하게 됩니다.
  • 확장성 제한:
    다중 서버 환경에서는 세션을 공유해야 하므로, 서버 간 동기화 문제가 발생할 수 있습니다.
    이를 해결하기 위해 Redis와 같은 외부 세션 저장소를 도입해야 합니다.
  • 유연성 부족:
    RESTful API와 같은 무상태 서비스에서는 잘 맞지 않습니다. REST의 설계 원칙 중 하나인 무상태성(Stateless)과 상충됩니다.

로그인 처리 (Spring Security 기반)

@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password, HttpSession session) {
User user = userService.authenticate(username, password);
if (user != null) {
session.setAttribute("user", user); // 세션에 사용자 정보 저장
return "redirect:/home";
}
return "redirect:/login?error";
}

인증 처리

@GetMapping("/profile")
public String getProfile(HttpSession session) {
User user = (User) session.getAttribute("user"); // 세션에서 사용자 정보 가져오기
if (user != null) {
return "profile";
}
return "redirect:/login";
}





상태 기반 인증과 비교: 무상태 인증

  • 상태 기반 인증 (Stateful Authentication):
    서버가 상태를 유지하며 세션에 사용자 정보를 저장.
    일반적인 웹 애플리케이션에 적합.
  • 무상태 인증 (Stateless Authentication):
    서버는 상태를 유지하지 않음.
    인증 정보(예: JWT)를 클라이언트가 보관하며 요청마다 인증 정보를 포함.
    RESTful API 및 마이크로서비스 환경에 적합.

언제 상태 기반 인증을 사용할까?

  • 웹 애플리케이션 : 전통적인 웹 애플리케이션에서 적합합니다.
  • 사용자 수가 적은 환경 : 인증 정보를 서버가 관리하기 때문에 많은 트래픽이나 사용자를 처리하지 않아도 되는 환경에서 유용합니다.
  • 보안이 중요한 경우 : 서버에서 인증 상태를 제어하므로 더 강력한 보안 제어가 필요할 때 적합합니다.

요약

상태 기반 인증은 서버가 사용자 인증 상태를 관리하는 방식으로, 전통적인 웹 애플리케이션에서 많이 사용됩니다. 그러나 확장성과 RESTful 설계 원칙을 고려할 때, 무상태 인증(JWT 기반 인증 등)이 더 적합한 환경도 있습니다.

예제: JWT 기반 무상태 인증

1. JWT 생성 (로그인 시)
클라이언트가 서버에 로그인 요청을 보내면, 서버는 사용자 정보를 검증한 뒤 JWT를 생성해 반환합니다.

@RestController
@RequestMapping("/auth")
public class AuthController {
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        // 사용자 인증 로직 (DB에서 사용자 확인)
        User user = userService.authenticate(request.getUsername(), request.getPassword());
        if (user == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
        }
        // JWT 생성
        String token = jwtProvider.generateToken(user);
        // JWT 반환
        return ResponseEntity.ok(new LoginResponse(token));
    }
}

2. JWT 생성 도구
JWT를 생성하고 검증하는 클래스를 작성합니다.


@Component
public class JwtProvider {

    private final String secretKey = "your-secret-key";
    private final long expirationMs = 3600000; // 1시간

    public String generateToken(User user) {
        return Jwts.builder()
            .setSubject(user.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + expirationMs))
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    public String getUsernameFromToken(String token) {
        return Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
}

3. 요청 검증 필터
클라이언트 요청에 포함된 JWT를 검증하고, 인증 정보를 SecurityContext에 저장합니다

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtProvider jwtProvider;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String token = getTokenFromRequest(request);

        if (token != null && jwtProvider.validateToken(token)) {
            String username = jwtProvider.getUsernameFromToken(token);

            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

4. Spring Security 설정
JwtAuthenticationFilter를 필터 체인에 추가합니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll() // 로그인, 회원가입 등 공개 API
                .anyRequest().authenticated() // 그 외 요청은 인증 필요
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

5. 사용자 요청 처리
클라이언트는 로그인 후 서버로부터 받은 JWT를 Authorization 헤더에 포함시켜 요청을 보냅니다.

GET /api/private/data HTTP/1.1
Authorization: Bearer

6. API 컨트롤러
요청에서 JWT가 유효하면 SecurityContext에 저장된 사용자 정보에 접근할 수 있습니다.

@RestController
@RequestMapping("/api/private")
public class PrivateController {
<br>
    @GetMapping("/data")
    public ResponseEntity<String> getData(Authentication authentication) {
        String username = authentication.getName();
        return ResponseEntity.ok("Hello, " + username + "! This is a private API.");
    }
}

요약

클라이언트가 로그인하면 서버는 JWT를 발급합니다.
JWT는 클라이언트가 보관하며, 요청마다 Authorization 헤더에 포함시켜 서버로 전달됩니다.
서버는 JWT를 검증하여 인증 상태를 확인하고 요청을 처리합니다.
세션이나 상태를 서버에 저장하지 않으므로 확장성이 뛰어나며 RESTful 설계에 적합합니다.

장점

  • 서버 확장성과 성능 최적화.
  • RESTful API 설계에 부합.
  • 클라이언트와 서버 간의 명확한 책임 분리.
    단점
  • JWT가 탈취될 경우 보안 위험.
  • 토큰 만료 및 재발급 관리가 필요
profile
열심히 하는 신입 개발자

0개의 댓글