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 반환)
핵심 흐름 요약
Authorization: Bearer {token} 형식의 요청에서 토큰 추출
유효성 검사 → 아이디(이메일) 추출 → 사용자 정보 로드
SecurityContextHolder에 인증 객체 설정
인증된 사용자로 컨트롤러 요청 진행 가능
✅ 로그인 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
2
System.out.println("입력 비밀번호: " + request.getPassword());
System.out.println("저장된 비밀번호 : " + user.getPassword());
출력 결과:
1
2
입력 비밀번호: 1234
저장된 비밀번호 : $2a$10$L5Q0alHg8IRy8ZXzH4LC/O3KAHcrB4aGl9t0YiLKsHM9yGiizI3L2
- 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...
💡 배운 점
| 항목 | 내용 |
|---|---|
| BCrypt 해시는 동일 환경에서 생성/검증해야 함 | 반드시 애플리케이션 내부에서 encode() 필요 |
Spring Security는 .authenticationProvider() 명시 필요 | 커스텀 UserDetailsService를 사용할 경우 |
AuthenticationManager는 수동 등록해야 함 | Spring Boot 3.x 이상에선 자동 생성 안됨 |
🔚 마무리
Spring Security와 JWT 토큰 인증을 처음 접하면서 매뉴얼을 보면서 설정을 하는데도,,,, 디버깅을 하는데도,,,, 내부 라이브러리 어딘가에서 에러가 나는 상황이라 정말 막막했다.
하지만, 포기하지않고 한곳한곳씩 로그를 찍어보면서 결국 해결할 수 있었다.!!!
이번 경험을 통해 암호화된 값은 항상 encode → 저장 → matches로 검증이라는 사이클을 명확히 이해하게 되었다.
내일 화면에서 회원가입을 하고, 로그인하는 것을 처리하면서 다시 복습해보자!
