[Spring Boot 3.x] 2. JWT 토큰과 쿠키를 이용한 로그인 구현

2025. 2. 24. 22:55·Spring Boot/Sprng Boot Sample Code

 

1. 개발환경

카테고리 도구/기술
프로그래밍 언어 Java 17
프레임워크 Spring Boot 3.4.2
빌드 도구 gradle
데이터베이스 MySQL
ORM 도구 Spring Data JPA
IDE IntelliJ
버전 관리 Git, GitHub, GitLab
패키지 관리 Maven Central
API 문서화 Swagger
로깅 SLF4J

 

2. build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.2'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JWT
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

    // Security
    implementation 'org.springframework.boot:spring-boot-starter-security'

    // MySQL db
    runtimeOnly 'com.mysql:mysql-connector-j'

    // JPA 사용
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // Web 스타터 - 아파치 톰켓, Web MVC, JSON 직렬화&역직렬화, validation
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // getter, setter 등의 어노테이션
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 테스트 관련
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

3. 디렉토리 구조

📂 src
 ├── 📂 main
 │   ├── 📂 java
 │   │   └── 📂 com.example.springboot3template
 │   │       ├── 📄 Springboot3templateApplication.java
 │   │       │
 │   │       ├── 📂 auth
 │   │       │   ├── 📂 application          # 🌟 애플리케이션 서비스 계층
 │   │       │   │   ├── 📂 dto
 │   │       │   │   │   ├── 📂 mapper
 │   │       │   │   │   ├── 📂 req
 │   │       │   │   │   │   ├── 📄 LoginReq.java
 │   │       │   │   │   │   ├── 📄 SignUpReq.java
 │   │       │   │   │   ├── 📂 res
 │   │       │   │   ├── 📂 service
 │   │       │   │   │   ├── 📄 AuthService.java
 │   │       │   │
 │   │       │   ├── 📂 domain               # 🌟 도메인 계층 (엔티티)
 │   │       │   │   ├── 📂 entity
 │   │       │   │   │   ├── 📄 User.java
 │   │       │   │   │   ├── 📄 UserRoleEnum.java
 │   │       │   │
 │   │       │   ├── 📂 infrastructure       # 🌟 인프라 계층 (데이터 저장소)
 │   │       │   │   ├── 📂 repository
 │   │       │   │   │   ├── 📄 UserRepository.java
 │   │       │   │
 │   │       │   ├── 📂 presentation         # 🌟 프레젠테이션 계층 (컨트롤러)
 │   │       │   │   ├── 📂 controller
 │   │       │   │   │   ├── 📄 AuthController.java
 │   │       │   │
 │   │       ├── 📂 common
 │   │       │   ├── 📂 security
 │   │       │   │   ├── 📄 JwtProvider.java
 │   │       │   │   ├── 📄 SecurityConfig.java
 │   │
 │   ├── 📂 resources
 │   │   ├── 📄 application-local.yml
 │   │   ├── 📄 application.yml
 │   │   ├── 📂 static
 │   │   ├── 📂 templates

 

4. 코드 흐름

1. postman

포스트맨으로 아래와 같이 로그인 요청

POST
http://localhost:8080/api/v1/auth/login
application/json

{
  "username" : "testuser",
  "password" : "test1234"
}

 

2. AuthController

컨트롤러에서  @RequestBody 어노테이션을 통해 JSON을 LoginReq라는 DTO로 변환한다.

    // 로그인
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginReq req, HttpServletResponse res) {
        authService.login(req, res);
        return ResponseEntity.ok().build();
    }

 

3. LoginReq 

@Data
public class LoginReq {

    private String username;

    private String password;

}

 

4. AuthService

받아온 정보를 주입해 로그인 서비스 로직 실행 repository를 통해 사용자가 있는지 비밀번호가 일치하는지 조회한 후 문제가 없다면 jwtProvider를 통해 토큰을 생성하고 생성된 토큰을 쿠키에 저장한다.

    // 로그인
    @Transactional
    public void login(LoginReq req, HttpServletResponse res) {
        String username = req.getUsername();
        String password = req.getPassword();

        // 사용자 확인
        User user = userRepository.findByUsername(username).orElseThrow(
            () -> new IllegalArgumentException("등록된 사용자가 없습니다.")
        );

        // 비밀번호 확인
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        // JWT 생성 및 쿠키에 저장 후 Response 객체에 추가
        String token = jwtProvider.createToken(user.getUsername(), user.getRole());
        jwtProvider.addJwtToCookie(token, res);
    }

 

5. JwtProvider

.setSubject(username) : 누구에 대한 토큰인지 명시

.claim(AUTHORIZATION_KEY, role) : key - value 형태로 원하는 값을 저장할 수 있다. 

.setIssuedAt() : 발급 시간

.setExpiration() : 만료 시간

.signWith(key, signatureAlgorithm) : 암호화 알고리즘

.compact() : JWT문자열 생성

    // 사용자 권한 값의 KEY
    public static final String AUTHORIZATION_KEY = "auth";
    // Token 식별자
    public static final String BEARER_PREFIX = "Bearer ";
    // 토큰 만료시간
    private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
    
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
            Jwts.builder()
                .setSubject(username) // 사용자 식별자값(ID)
                .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                .setIssuedAt(date) // 발급일
                .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                .compact();
    }

 

JWT 토큰은 변조를 방지하기 위해 서명을 하는데 이 때 인코딩된 시크릿키를 디코딩한 후 서명 알고리즘에 사용할 수 있는 키 객체를 생성한다.

    @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
    private String secretKey;
    
    private Key key;
    
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        if (secretKey == null || secretKey.isEmpty()) {
            throw new IllegalStateException("Secret key is not set, in JwtProvider");
        }
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

 

생성한 토큰은 쿠키에 저장해 사용한다.

 

.setPath("/") : 쿠키의 유효 경로를 설정

.setSecure(true) : 쿠키가 HTTPS 연결을 통해서만 전송되도록 설정

.setMaxAge(3600 * 1000) : 쿠키의 만료시간을 설정, 일반적으로 토큰과 같은 시간으로 설정한다.

    // JWT Cookie 에 저장
    public void addJwtToCookie(String token, HttpServletResponse res) {

        token = substringToken(token);

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
        cookie.setPath("/");
        cookie.setSecure(true);
        cookie.setMaxAge(3600 * 1000); // 60분

        // Response 객체에 Cookie 추가
        res.addCookie(cookie);
        log.info("쿠키가 response에 추가되었습니다.");
    }

 

토큰 앞의 bearer 은 OAuth 2.0 인증 프로토콜에서 사용되는 인증 방식 중 하나로 "소지자가 권한을 가진" 이라는 뜻으로 HTTP 헤더에 포함되어 요청을 인증하는데 사용된다. 하지만 쿠키에서는 실질적인 토큰 값만이 필요하기 때문에 bearer과 뒤의 공백 1자리까지 총 7자리가 필요없기 때문에 쿠키에 넣는 과정에서 삭제한다.

    // JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        log.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

 

6. SecurityConfig

회원가입과 로그인의 경우 시큐리티에서 걸리지 않아야 하기 떄문에 

.requestMatchers("/api/v1/auth/login").permitAll() 을 통해 통과 시켜준다.

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
@RequiredArgsConstructor
public class SecurityConfig {

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        // Session 비활성화
        http.sessionManagement((sessionManagement) ->
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        // 요청 권한 설정
        http.authorizeHttpRequests((authorizeHttpRequests) ->
            authorizeHttpRequests
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                // api 요청 허가
                .requestMatchers("/api/v1/auth/login", "/api/v1/auth/sign-up").permitAll()
                // HTML 페이지 접근을 인증 없이 허용
//                .requestMatchers("/").permitAll()
                .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        return http.build();
    }

}

 

 

 

 

 

 

 

 

저작자표시 비영리 변경금지 (새창열림)

'Spring Boot > Sprng Boot Sample Code' 카테고리의 다른 글

Spring Boot 에서 네이버 클라우드 Object Storage 에 파일 업로드  (1) 2025.03.03
[SpringBoot 3.x] 4. @PreAuthorize 어노테이션을 이용한 API 인가 설정  (0) 2025.03.03
[Spring Boot 3.x] 3. JWT 토큰 인증(filter) & API 테스트  (0) 2025.03.01
[Spring Boot 3.x] 1. 회원가입 구현  (1) 2025.02.02
build.gradle 이해하기  (1) 2025.01.30
'Spring Boot/Sprng Boot Sample Code' 카테고리의 다른 글
  • [SpringBoot 3.x] 4. @PreAuthorize 어노테이션을 이용한 API 인가 설정
  • [Spring Boot 3.x] 3. JWT 토큰 인증(filter) & API 테스트
  • [Spring Boot 3.x] 1. 회원가입 구현
  • build.gradle 이해하기
Yun-seul
Yun-seul
재능이 없으면 열심히라도 하자
  • Yun-seul
    윤슬
    Yun-seul
  • 전체
    오늘
    어제
    • 분류 전체보기 (24)
      • Minecraft 모드 개발 (2)
      • Java (1)
        • Java 기본 (1)
      • Spring Boot (9)
        • Sprng Boot Sample Code (9)
      • Docker (1)
      • Kubernetes (7)
        • Common (3)
        • GKE(Google Kubernetes Engin.. (4)
        • EKS(Elastic Kubernetes Serv.. (0)
      • Redis (0)
      • AWS (0)
      • Git (0)
      • Reflection (1)
      • Troubleshooting (3)
      • Performance Tuning (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    전역예외처리
    onceperrequestfilter
    jwt토큰 #로그인 #회원가입 #쿠키 #보안설정
    쿠버네티스
    globalexception
    임시디렉토리
    에러코드관리
    @value #null #어노테이션 #springboot #springioc #컨테이너
    djava.io.tmpdir
    GKE
    필터2번
    unable to create tempdir. java.io.tmpdir is set to
    kubernetes
    커스텀익셉션
    재요청
    어세스토큰
    methodargumentnotvalidexception
    rfc6750
    docker
    SpringBoot
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Yun-seul
[Spring Boot 3.x] 2. JWT 토큰과 쿠키를 이용한 로그인 구현
상단으로

티스토리툴바