본문 바로가기
BackEnd

JWT + Security로 로그인 구현해보기

by Jiwon_Loopy 2025. 9. 7.
반응형

 

1. 들어가기 전

새로운 프로젝트에 들어가게 되면서 Security와 JWT를 이용해 AccessToken, RefreshToken(미정)을  사용하여 로그인을 구현하는 작업을 맡게 되어 나의 프로젝트 회고를 작성해보기로 하였다,

 

 

2. 인증 vs 인가

🔑 인증(Authentication) vs 인가(Authorization)

  • 인증 (Authentication)
    👉 "너 누구야?"
    사용자가 누구인지 신원을 확인하는 과정
    • 예: 아이디/비밀번호 로그인, OAuth 로그인, 생체인증 등
    • 성공하면 Access Token 같은 인증 수단을 발급
  • 인가 (Authorization)
    👉 "너한테 이거 할 권한 있어?"
    인증된 사용자가 특정 자원이나 기능을 사용할 수 있는지 권한을 확인하는 과정
    • 예: 일반 유저는 게시물 작성 가능, 관리자는 회원 강제 탈퇴 가능

➡️ 쉽게 말하면:

  • 인증 = 신원 확인 (로그인)
  • 인가 = 권한 확인 (권한 검증)

 

3. 내가 생각한 방법

강사님과 이야기 후, 생각해 본 방법을 정리해보았다.

로그인,비밀번호 -> 확인여부 + JWT 토큰 같이 보내기 -> 다시 한 번 더 요청


로그인 -> 아이디 비밀번호입력 -> 리액트가 이미 발급받은 토큰 여부 체크 -> (사전에 알고있는)클라이언트 id, secret 받은 후 db에서 일치 여부 확인 -> 맞으면 토큰 생성 -> 토큰 프론트로 전송 -> 토큰이랑 (자사 서비스의) 사용자 아이디 비밀번호 같이 보냄 -> 필터에서 토큰 생성



사용자는 API키를 사용하겠다고 한 사용자 서비스 (별도의 프로그램, 서비스 자체)
회원은 자사 서비스를 이용하는 사람들


기존 방식 : 사용자ID, 비밀번호 방식으로 secret id password를 설정하고, 해당 아이디 비번으로 회원 가입 시 DB저장
이후 로그인 시 사용자 아이디, 비밀 번호 DB에 존재하는 경우 토큰 발급

문제점 > API 인증을 허가하는 부분의 토큰 방식과, 아이디 비번 인증 전용 토큰 방식은 다르다. (API 사용자와 회원을 혼동하면 안됨) => 하지만, 같다고 생각했었음

변경 방식 : 둘 모두 분리하여 토큰 사용 VS 아이디 비번은 단순하게 체킹하고, API 사용 권한 여부만 토큰으로 판별

변경 로직 (예상) :

- 회원 가입
	- 기존 방식 유지하되, ID, PW, 추가 회원 데이터(자사 서비스 로그인 용) 저장 및 SecrectKey, password (API 인증용) 발급해준 뒤 저장

- 로그인
	1. 사용자가 ID PW 입력
	2. 리액트는 이전 JWT 토큰 발급이력이 있는지 발급 여부 확인
		2-1. 있다면 사용자가 입력한 ID, PW와 함께 토큰을 서버로 보내 바로 로그인 시도(6번) (6으로 이동)
		2-2. 없다면 3으로 이동
	3. secertkey, password 리액트에서 전송, DB에서 확인 후에, API 인증 허가용 엑세스 토큰 발급
	=> (만료 여부 확인하여 refreshToken 까지 사용 가능하나 이 부분은 생략)
	4. 프론트에서 받은 토큰 브라우저 세션에 저장
	5. 세션에 저장한 엑세스 토큰과 ID, PW를 이용하여 전송 (2-1과 같음)
	6. 토큰 일치 여부 확인 후 로그인

 

중요한 점은, 사용자(클라이언트, 개발자, 툴 등 API를 실제로 구현하는 주체)와 회원(실제 서비스를 이용하는 사람들)을 같은 개념으로 두고 생각하면 혼용하기 쉽다는 것이다.

API키(토큰)를 승인해주는 주체와, 로그인하는 주체를 혼동하면 안된다.

 

나는, 두 개념을 합하여 jwt토큰 발급 및 시큐리티에 필요한 검증을 해주는 secret_id와 secret_password를 우리 서비스를 이용하는 가상의 회원 아이디, 비밀번호로 통합하기로 하였다. 다만 절대!!! 혼동하면 안된다. API인증과, 로그인은 엄연히 다르게 생각해야 한다.

 

 

API/회원 로그인 및 인증 로직 정리


1. 기존 방식

  • 사용자 ID, 비밀번호 방식으로 회원 가입 시 DB 저장
  • 로그인 시 입력한 ID, PW가 DB에 존재하면 토큰 발급
  • 문제점
    • API 인증용 토큰과 일반 회원 로그인용 토큰을 동일하게 사용
    • API 사용자와 일반 회원 혼동 가능

2. 변경 방식

  • 회원 로그인용과 API 인증용 토큰을 분리
  • ID/PW 검증은 단순 확인 용도
  • API 사용 권한 여부만 토큰으로 판별

3. 회원 가입

  1. 기존 방식 유지
    • ID, PW, 추가 회원 데이터 저장
  2. API 인증용 SecretKey, password 생성 후 DB 저장

4. 로그인 흐름

  1. 사용자 ID/PW 입력
  2. React 측 JWT 토큰 발급 여부 확인
    • 있음:
      • 사용자가 입력한 ID/PW + 기존 토큰 → 서버로 전송 → 토큰 검증
    • 없음:
      • SecretKey, password 전송 → DB에서 확인 → API 인증용 Access Token 발급
  3. 서버에서 발급한 토큰을 React에서 브라우저 세션에 저장
  4. 이후 로그인 시
    • 세션에 저장한 Access Token + ID/PW → 서버 전송
    • 서버에서 토큰 일치 여부 확인 → 로그인 처리 완료

 

5. 흐름 요약

회원 가입
  └─ ID, PW + API Secret 발급 → DB 저장

로그인
  ├─ React: 이전 토큰 확인
  │    ├─ 토큰 있음 → ID/PW + 토큰 → 서버 확인
  │    └─ 토큰 없음 → SecretKey/PW → 서버 확인 → Access Token 발급
  └─ Access Token 브라우저 세션 저장
      └─ 이후 요청 시 토큰 + ID/PW → 서버에서 검증

 

 

1️⃣ 회원/API 인증 비교 테이블

구분 기존 방식 변경 방식

목적 회원 로그인 + API 인증 동일 회원 로그인과 API 인증 분리
회원 가입 ID, PW 저장 ID, PW + API SecretKey, Password 발급 및 저장
로그인 DB ID/PW 확인 → 토큰 발급 ID/PW 단순 검증 + API 인증용 Access Token 발급
문제점 API/회원 토큰 혼동 구분 명확, 보안 향상

2️⃣ 회원 가입 프로세스

단계 설명

1 사용자가 회원 가입 시 ID, PW 입력
2 추가 회원 데이터와 함께 DB 저장
3 API 인증용 SecretKey, Password 생성 후 DB에 저장

3️⃣ 로그인 프로세스 (React + JWT)

flowchart TD
    A[사용자 ID/PW 입력] --> B{React: 이전 JWT 토큰 존재?}
    B -- Yes --> C[ID/PW + 기존 토큰 서버 전송]
    C --> D[서버: 토큰 일치 여부 확인 → 로그인]
    B -- No --> E[SecretKey + Password 서버 전송]
    E --> F[DB 확인 → Access Token 발급]
    F --> G[React 브라우저 세션에 토큰 저장]
    G --> H[ID/PW + 토큰 → 서버 전송]
    H --> D


4️⃣ 요약

  • 회원 로그인: ID/PW 단순 확인
  • API 인증: 토큰 기반, Access Token 사용
  • React + JWT: 클라이언트에서 토큰 관리, 서버는 Stateless
  • 로그아웃: 클라이언트에서 토큰 삭제 → 서버 세션 없음

 

 

 

구현 및 클래스 소개


1. DATA

 

  • Users (사용자 Entity) - 서비스에 필요한 사용자 엔티티, 회원 가입 시 DB 저장용
package team.shdsesc.stocksimul.auth.entity;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import team.shdsesc.stocksimul.auth.dto.UserDTO;
import team.shdsesc.stocksimul.auth.dto.UserRole;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Users {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String phoneNumber;

    private int level;

    @ElementCollection(fetch = FetchType.LAZY)
    @Builder.Default
    private List<String> tickerList = new ArrayList<>();

    @ElementCollection(fetch = FetchType.LAZY)
    @Builder.Default
    @Enumerated(EnumType.STRING)
    private Set<UserRole> roleSet = new HashSet<>();

    public static UserDTO toUsersEntity(Users users) {
        return new UserDTO(
                users.getUserId(),
                users.getEmail(),
                users.getPassword(),
                users.getPhoneNumber(),
                users.getLevel(),
                users.getTickerList(),
                users.getRoleSet().stream()
                        .map(role -> new SimpleGrantedAuthority(role.toString()))
                        .toList()
        );
    }

    public void addMemberRole(UserRole role) {
        roleSet.add(role);
    }
}

 

 

  • UserDTO - Security의 UserDetails를 상속받는 User를 확장한 DTO
package team.shdsesc.stocksimul.auth.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;
import java.util.List;

@Getter
@Setter
@ToString
public class UserDTO extends User {
    private Long userId;
    private String secretId;
    private String email;
    private String secretPassword;
    private String phoneNumber;
    private int level;
    private List<String> tickerList;

    public UserDTO(Long userId, String email, String password, String phoneNumber, int level, List<String> tickerList, Collection<? extends GrantedAuthority> authorities) {
        super(email, password, authorities);
        this.userId = userId;
        this.secretId = email;
        this.secretPassword = password;
        this.phoneNumber = phoneNumber;
        this.level = level;
        this.tickerList = tickerList;
    }
}

 

package team.shdsesc.stocksimul.auth.dto;

public enum UserRole {
    USER, ADMIN
}

 

 

  • UserRequestDTO - 회원 가입 시 요청 객체
package team.shdsesc.stocksimul.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
@AllArgsConstructor
public class UserRequestDTO {
    private String email;
    private String password;
    private String level;
    private List<String> tickerList;
}

 

 

2. Config

  • SecurityConfig - 시큐리티 설정을 제공하는 설정 파일
package team.shdsesc.stocksimul.auth.config;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import team.shdsesc.stocksimul.auth.filter.ApiLoginFilter;
import team.shdsesc.stocksimul.auth.filter.TokenCheckFilter;
import team.shdsesc.stocksimul.auth.handler.APILoginSuccessHandler;
import team.shdsesc.stocksimul.auth.service.UserDetailService;
import team.shdsesc.stocksimul.auth.util.JWTUtil;

@Configuration
@EnableWebSecurity
@Log4j2
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
    private final UserDetailService userDetailService;
    private final PasswordEncoder passwordEncoder;
    private final JWTUtil jwtUtil;

    @Autowired
    SecurityConfig(UserDetailService userDetailService, PasswordEncoder passwordEncoder, JWTUtil jwtUtil) {
        this.userDetailService = userDetailService;
        this.passwordEncoder = passwordEncoder;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        log.info("security config...............");

        http.authorizeHttpRequests(auth -> auth
                // .requestMatchers("/boards/register").hasAnyRole("BASIC","MANAGER","ADMIN")
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated());
        http.csrf(AbstractHttpConfigurer::disable); // CSRF 토큰 미사용 설정

        // CORS 설정
        http.cors(cors -> cors.configurationSource(corsConfigurationSource()));

        // 자체 로그인 설정으로 폼 로그인은 비활성화
        http.formLogin(AbstractHttpConfigurer::disable);
        http.httpBasic(AbstractHttpConfigurer::disable);

        // JWT 관련 설정
        // AuthenticationManager 설정
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userDetailService).passwordEncoder(passwordEncoder);

        // AuthenticationManager 객체 생성
        AuthenticationManager authenticationManager = authenticationManagerBuilder.build();
        http.authenticationManager(authenticationManager);

        // 필터
        // 토큰발급URL (http://localhost:8080/auth)
        ApiLoginFilter apiLoginFilter = new ApiLoginFilter("/auth");
        apiLoginFilter.setAuthenticationManager(authenticationManager);
        apiLoginFilter.setAuthenticationSuccessHandler(new APILoginSuccessHandler(jwtUtil));

        // 필터동작위치
        http.addFilterBefore(apiLoginFilter, UsernamePasswordAuthenticationFilter.class);

        // 토큰체크필터
        TokenCheckFilter tokenCheckFilter = new TokenCheckFilter(jwtUtil);
        http.addFilterBefore(tokenCheckFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowCredentials(true);
//        configuration.addAllowedOriginPattern("http://localhost:3000/"); // 모든 도메인 허용
        configuration.addAllowedOriginPattern("http://localhost:5173");
        configuration.addAllowedHeader("*"); // 모든 헤더 허용
        configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

 

securityFilterChain : 시큐리티를 사용할 떄, 접근 권한, authenticationManager에 유저 설정 및 유효성 검증 및 해당 경로로 들어오는 요청에 대해 시큐리티 필터 체인 적용, 폼 로그인 (서버 쪽) 설정

 

corsConfigurationSource : 도메인, HTTP 메서드, 헤더로 들어오는 요청 허용 여부 결정

 

 

 

  • QueryDslConfig - 쿼리 DSL 설정
package team.shdsesc.stocksimul.auth.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {

    @PersistenceContext
    private EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(em);
    }
}

 

 

 

  • PasswordEncoderConfig - 패스워드 인코딩 설정
package team.shdsesc.stocksimul.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

 

Repository

  • UserRepository - JPA 레포지토리와 Query DSL을 상속 받음
package team.shdsesc.stocksimul.auth.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import team.shdsesc.stocksimul.auth.entity.Users;


public interface UserRepository extends JpaRepository<Users, String>, UserRepositoryCustom {
}

 

 

  • UserRepositoryCustom - Query DSL 연동을 위한 인터페이스
package team.shdsesc.stocksimul.auth.repository;

import team.shdsesc.stocksimul.auth.entity.Users;

import java.util.Optional;

public interface UserRepositoryCustom {
    Optional<Users> findUserWithRolesByUserId(String email);
}

 

 

  • UserRepositoryImpl - Repository의 구현체
package team.shdsesc.stocksimul.auth.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import team.shdsesc.stocksimul.auth.entity.QUsers;
import team.shdsesc.stocksimul.auth.entity.Users;

import java.util.Optional;

@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public Optional<Users> findUserWithRolesByUserId(String email) {
        QUsers users = QUsers.users ;

        Users result = queryFactory
                .selectFrom(users)
                .leftJoin(users.roleSet).fetchJoin()
                .where(users.email.eq(email))
                .fetchOne();

        return Optional.ofNullable(result);
    }
}

 

 

Service

  • UserDetailService - 계정 관련 서비스(비즈니스 로직)를 처리
package team.shdsesc.stocksimul.auth.service;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import team.shdsesc.stocksimul.auth.dto.UserDTO;
import team.shdsesc.stocksimul.auth.dto.UserRole;
import team.shdsesc.stocksimul.auth.dto.UserRequestDTO;
import team.shdsesc.stocksimul.auth.entity.Users;
import team.shdsesc.stocksimul.auth.repository.UserRepository;

import java.util.Optional;

@Log4j2
@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Optional<Users> result = userRepository.findUserWithRolesByUserId(email);
        log.info("username:{}", email);
        Users users = result.orElseThrow(() -> new UsernameNotFoundException("Wrong ClientId or ClientSecret"));
        log.info("Users:{}", users);
        UserDTO dto = Users.toUsersEntity(users);
        log.info("loadUserByUsername:{}", dto);
        return dto;
    }

    public UserDTO registerUser(UserRequestDTO request) {
        Users users = Users.builder()
                .email(request.getEmail())
                .password(passwordEncoder.encode(request.getPassword()))
                .phoneNumber("010-1234-5678")
                .level(Integer.parseInt(request.getLevel()))
                .tickerList(request.getTickerList())
                .build();

        users.addMemberRole(UserRole.USER);
        userRepository.save(users);
        return Users.toUsersEntity(users);
    }

    public void logoutUser(HttpServletResponse response){
        // Refresh Token 쿠키 삭제
        ResponseCookie cookie = ResponseCookie.from("refreshToken", "")
                .path("/")
                .httpOnly(true)
                .secure(false) // 개발용
                .maxAge(0)     // 만료시킴
                .build();
        response.setHeader("Set-Cookie", cookie.toString());
    }
}

 

 

Util

  • 토큰 발급에 실질적으로 쓰이는 클래스
package team.shdsesc.stocksimul.auth.util;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
@Log4j2
public class JWTUtil {

    @Value("${jwt.secret.key}")
    private String key;

    // Access Token 생성
    public String generateAccessToken(Map<String, Object> valueMap, int minutes) {
        return generateToken(valueMap, minutes);
    }

    // Refresh Token 생성 (DB 저장 가능)
    public String generateRefreshToken(Map<String, Object> valueMap, int days) {
        return generateToken(valueMap, days * 60 * 24); // 일 단위 -> 분
    }

    private String generateToken(Map<String, Object> valueMap, int minutes) {
        Map<String, Object> headers = new HashMap<>();
        headers.put("typ", "JWT");
        headers.put("alg", "HS256");

        Map<String, Object> payloads = new HashMap<>(valueMap);

        return Jwts.builder()
                .setHeader(headers)
                .setClaims(payloads)
                .setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
                .setExpiration(Date.from(ZonedDateTime.now().plusMinutes(minutes).toInstant()))
                .signWith(SignatureAlgorithm.HS256, key.getBytes())
                .compact();
    }

    // 토큰 검증
    public Map<String, Object> validateToken(String token) throws Exception {
        log.info("토큰: " + token);
        return Jwts.parser()
                .setSigningKey(key.getBytes())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

 

Handler

  • APILoginSuccessHandler - 로그인 성공 시 토큰 발급 로직을 처리하는 핸들러
package team.shdsesc.stocksimul.auth.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import team.shdsesc.stocksimul.auth.util.JWTUtil;

import java.io.IOException;
import java.util.Map;

@Log4j2
public class APILoginSuccessHandler implements AuthenticationSuccessHandler {

    private final JWTUtil jwtUtil;

    public APILoginSuccessHandler(JWTUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("Login Success...."); // 로그인 성공하면
        // 토큰생성해서 서블릿으로 응답
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        Map<String, Object> claim = Map.of("email", authentication.getName());
        // Access Token 유효기간 1시간
        String accessToken = jwtUtil.generateAccessToken(claim, 60);
        // Refresh Token 유효기간 1일
        String refreshToken = jwtUtil.generateRefreshToken(claim, 1);

        // secure, httpOnly 옵션을 통해 XSS 공격 방지
        // SameSite 옵션을 통해 CSRF 공격 방지
        //  - None : 도메인 검증 X 어디서든 사용 가능하나 secure 옵션 필수
        //  - Lax : 외부 링크도 접근 허용 하지만 get 요청만 OK
        //  - Strict : 같은 도메인에서만 쿠키 전송 가능
        ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken)
                .maxAge(7 * 24 * 60 * 60)
                .path("/")
                // 브라우저에서 쿠키에 접근할 수 없도록 제한
                .httpOnly(true)
                // https 환경에서만 쿠키 발동
                .secure(false)
                // 동일 사이트과 크로스 사이트에 모두 쿠키 전송이 가능
                .sameSite("None")
                .build();

        response.setHeader("Set-Cookie", cookie.toString());
        Map<String, String> keyMap = Map.of("accessToken", accessToken);
        ObjectMapper om = new ObjectMapper();
        String json = om.writeValueAsString(keyMap);
        response.getWriter().print(json);
    }
}

 

httpOnly : 브라우저에서 접근 불가하게 하여, XSS(크로스 사이트 스크립팅) 공격방지

secure : https에서만 쿠키 허용, XSS 공격 방지

sameSite : CSRF 공격 방지

 

🍪 쿠키 보안 속성들

쿠키를 사용할 때 보안을 위해 설정하는 주요 옵션:

  1. Secure
    • true로 설정 시, HTTPS 연결에서만 쿠키 전송 가능
    • 평문 HTTP에서는 전송되지 않음
    • 예: Set-Cookie: token=abc; Secure
  2. HttpOnly
    • true로 설정 시, JavaScript에서 쿠키 접근 불가
    • 즉, document.cookie로 가져올 수 없음 → XSS 공격 방어
    • 예: Set-Cookie: token=abc; HttpOnly
  3. SameSite
    • CSRF 공격 방지를 위한 옵션
    • 동작 방식:
      • Strict: 완전히 동일한 도메인에서만 쿠키 전송
        (다른 사이트에서 링크 클릭해도 전송 안 됨)
      • Lax: 대부분의 경우 차단, 단 GET 요청(링크 클릭, 새 탭 열기 등) 은 허용
      • None: 모든 cross-site 요청 허용, 단 Secure 필수
    • 예: Set-Cookie: token=abc; SameSite=Strict

 

 

간단한 클래스 설명 까지만 진행해 보았다.

 

 

728x90
반응형