Post

8일차 Spring Security, JWT 토큰방식 로그인 구현

8일차 Spring Security, JWT 토큰방식 로그인 구현

🎯 오늘의 목표

MES 시스템에 사용자 인증 기능을 추가하기 위해 JWT 기반 로그인 기능을 구현하려고 한다.
우선 MySQL 사용자 테이블 구조부터 정리하고, 이후 Spring Boot에서 JWT 인증을 연동할 계획이다.


✅ 기존 user 테이블 삭제

MySQL에서는 user가 예약어로 쓰이기 때문에, 테이블명 충돌 및 혼란을 피하기 위해 기존 테이블을 삭제했다.

1
DROP TABLE user;

✅ users 테이블 새로 생성

JWT 인증을 위해 필요한 최소한의 사용자 정보만 담은 테이블이다. password 컬럼은 로그인 검증을 위해 반드시 필요하므로 추가해두었다.

1
2
3
4
5
6
7
8
CREATE TABLE users (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  email VARCHAR(100) NOT NULL UNIQUE, /* ID값 처럼 쓰일 정보 */
  password VARCHAR(255) NOT NULL,
  role VARCHAR(20),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

✅ 클래스 정의

User 엔티티 클래스 생성

  • Spring JPA를 사용해 users 테이블과 매핑되는 User 클래스 생성

UserRepository 인터페이스 생성

  • JPA를 사용해 이메일로 사용자를 조회할 수 있는 Repository 생성

JwtTokenProvider 클래스 생성

  • 로그인 성공 시 JWT Access Token 생성
  • 요청 헤더에 담긴 토큰에서 유저 정보 추출
  • 토큰의 유효성 검증

JWT 생성 및 검증을 담당하는 클래스로, 로그인 성공 시 토큰을 발급하고, 요청이 들어올 때 토큰을 검증하는 역할을 한다.

JWT 토큰 생성을 위해 JJWT(Json Web Token) Maven 의존성을 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.11.5</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.11.5</version>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId>
  <version>0.11.5</version>
  <scope>runtime</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Component
public class JwtTokenProvider {

    // 비밀 키 
    private final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    // 토큰 유효 시간 (1시간)
    private final long validityInMilliseconds = 60 * 60 * 1000;

    // 토큰 생성
    public String createToken(String email, String role) {
        Claims claims = Jwts.claims().setSubject(email);
        claims.put("role", role);

        Date now = new Date();
        Date expiry = new Date(now.getTime() + validityInMilliseconds);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expiry)
                .signWith(secretKey)
                .compact();
    }

    // 이메일 추출
    public String getEmail(String token) {
        return Jwts.parserBuilder().setSigningKey(secretKey).build()
                .parseClaimsJws(token).getBody().getSubject();
    }

    // 토큰 유효성 검사
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(secretKey).build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

SecurityConfig 클래스 생성

JWT 기반 인증 구조를 위해 Spring Security 설정을 커스터마이징한다.

설정 항목설명
csrf().disable()JWT는 세션을 사용하지 않기 때문에 CSRF 방어가 필요 없음
sessionManagement().stateless()JWT는 Stateless 인증 방식이므로 세션 저장이 불필요
authorizeHttpRequests()로그인, 회원가입 요청은 허용하고 나머지 요청은 인증 필요
addFilterBefore()JWT 인증 필터를 UsernamePasswordAuthenticationFilter 앞에 등록
passwordEncoder()비밀번호 암호화를 위해 BCryptPasswordEncoder 빈을 등록

Spring Security란?

Spring Security는 Spring 생태계에서 사용자 인증(Authentication), 권한 제어(Authorization),
암호화, 보안 필터 처리 등을 책임지는 필수 보안 프레임워크이다.

주요 특징

  • 로그인/로그아웃 처리 자동화
  • JWT, 세션, OAuth2 등 다양한 인증 방식 지원
  • 필터 기반의 유연한 구조 → JWT 인증에 최적화 가능
  • 비밀번호 암호화(BCrypt) 및 사용자 권한 체크 기능 포함

📌 즉, Spring Security는 보안 기능 전반을 책임지는 스프링의 공식 보안 모듈이다.

🛠 Spring Security 의존성 추가 (Maven)

org.springframework.security 관련 빨간줄이 뜰 경우, 아래 의존성을 pom.xml에 추가해야 한다:

1
2
3
4
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

CustomUserDetailsService 클래스 생성

  • Spring Security는 로그인 시 UserDetailsService.loadUserByUsername() 를 호출함
  • 조회된 사용자를 UserDetails 객체로 변환해서 반환

JwtAuthenticationFilter 클래스 생성

  • 매 요청마다 JWT 토큰이 Authorization 헤더에 있는지 확인
  • 토큰이 유효하면 사용자 정보를 SecurityContext에 등록
  • 그렇지 않으면 인증 없이 요청 통과 (Spring Security가 403 반환)

핵심 흐름 요약

  1. Authorization: Bearer {token} 형식의 요청에서 토큰 추출

  2. 유효성 검사 → 아이디(이메일) 추출 → 사용자 정보 로드

  3. SecurityContextHolder에 인증 객체 설정

  4. 인증된 사용자로 컨트롤러 요청 진행 가능


✅ 로그인 API (/login) 컨트롤러

  • AuthenticationManager를 통해 email/password 검증
  • 검증 성공 시 UserDetails 반환
  • JWT 생성 후 클라이언트에 전달

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@RequiredArgsConstructor
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenProvider jwtTokenProvider;
    private final CustomUserDetailsService userDetailsService;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
        );

        User user = (User) authentication.getPrincipal(); // org.springframework.security.core.userdetails.User

        String token = jwtTokenProvider.createToken(user.getUsername(), user.getAuthorities().iterator().next().getAuthority());

        return ResponseEntity.ok().body(token);
    }
}

DTO

1
2
3
4
5
6
7
@Getter
@Setter
public class LoginRequest {
    private String email;
    private String password;
}

🛠️ 트러블슈팅 기록

JWT 기반 로그인 구현 후, Postman으로 로그인 요청 시 아래와 같은 403 Forbidden 응답이 반복 발생:

1
Failed to authenticate since password does not match stored value

로그인 요청 내용:

1
2
3
4
5
POST /login
{
  "email": "test@example.com",
  "password": "1234"
}

디버깅 과정

  1. 입력 값과 저장된 비밀번호 확인
1
2
System.out.println("입력 비밀번호: " + request.getPassword());
System.out.println("저장된 비밀번호 : " + user.getPassword());

출력 결과:

1
2
입력 비밀번호: 1234
저장된 비밀번호 : $2a$10$L5Q0alHg8IRy8ZXzH4LC/O3KAHcrB4aGl9t0YiLKsHM9yGiizI3L2
  1. BCryptPasswordEncoder.matches()로 직접 비교해보니 false
1
2
3
PasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.matches("1234", "$2a$10$L5Q0alHg8IRy8ZXzH4LC/..."));
// 결과: false

** 원인: 저장된 해시 값이 현재 애플리케이션에서 생성된 값이 아님 → 해시 버전 불일치**

🛠️ 해결

1. 애플리케이션에서 직접 암호화된 비밀번호 생성

1
2
PasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("1234"));

→ 출력된 값:

1
$2a$10$X3HZ0UlZ9GmK/pfXeymJ9eZlRSEj4FL6dxRWqxz3dy3wCVjDHvnB6

2. 해당 값으로 DB 수동 업데이트

1
2
3
UPDATE users
SET password = '$2a$10$X3HZ0UlZ9GmK/pfXeymJ9eZlRSEj4FL6dxRWqxz3dy3wCVjDHvnB6'
WHERE email = 'test@example.com';

추가로 적용한 Spring Security 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder()); 
    return provider;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/signup").permitAll()
            .anyRequest().authenticated()
        )
        .authenticationProvider(authenticationProvider()) 
        .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService),
                         UsernamePasswordAuthenticationFilter.class)
        .build();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
}

🧠 결과

  • Postman에서 로그인 요청 성공
  • JWT 토큰 정상 발급:
1
eyJhbGciOiJIUzI1NiJ9...
  • 이후 인증이 필요한 API 접근 시 Authorization 헤더에 토큰 추가하여 사용 가능

💡 배운 점

항목내용
BCrypt 해시는 동일 환경에서 생성/검증해야 함반드시 애플리케이션 내부에서 encode() 필요
Spring Security는 .authenticationProvider() 명시 필요커스텀 UserDetailsService를 사용할 경우
AuthenticationManager는 수동 등록해야 함Spring Boot 3.x 이상에선 자동 생성 안됨

🔚 마무리

Spring Security와 JWT 토큰 인증을 처음 접하면서 매뉴얼을 보면서 설정을 하는데도,,,, 디버깅을 하는데도,,,, 내부 라이브러리 어딘가에서 에러가 나는 상황이라 정말 막막했다.

하지만, 포기하지않고 한곳한곳씩 로그를 찍어보면서 결국 해결할 수 있었다.!!!

이번 경험을 통해 암호화된 값은 항상 encode → 저장 → matches로 검증이라는 사이클을 명확히 이해하게 되었다.

내일 화면에서 회원가입을 하고, 로그인하는 것을 처리하면서 다시 복습해보자!

Backend Repository : GitHub

This post is licensed under CC BY 4.0 by the author.