JWT(JSON Web Token)는 로그인 후 서버가 사용자에게 발급하는 인증 토큰이다.
기존 Session 방식은 서버가 로그인 정보를 저장하지만,
JWT 방식은 사용자 정보와 권한을 토큰에 담아 클라이언트가 보관한다.
이후 요청마다 JWT를 보내면 서버는 토큰만 검증하여 로그인 여부를 판단한다.
로그인 요청
↓
Controller
↓
Service
↓
DB에서 회원 조회
↓
비밀번호 검증
↓
JWT 생성
↓
클라이언트에게 JWT 반환
↓
클라이언트가 JWT 저장
↓
이후 모든 요청에 JWT 포함
↓
JwtFilter가 JWT 검증
↓
SecurityContext에 인증 정보 저장
↓
Controller / Service 실행
사용자가 로그인 버튼을 누르면
POST /api/auth/login
으로 요청이 들어온다.
Controller
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(
@Valid @RequestBody LoginRequest request) {
LoginResponse loginResult = memberService.login(request);
return ResponseEntity.ok(loginResult);
}
Controller의 역할은 매우 단순하다.
실제 로그인 로직은 모두 Service에서 처리한다.
Service에서는 먼저 username이 존재하는지 확인한다.
Member member = memberRepository.findByUsername(loginRequest.getUsername())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저"));
실행 순서
입력한 username
↓
MemberRepository
↓
DB 조회
↓
Member 반환
회원이 없다면 로그인은 실패한다.
DB에는 평문 비밀번호가 저장되지 않는다.
예를 들어 사용자가 입력한 비밀번호가
1234
라고 해도,
DB에는 BCrypt로 암호화된 값만 저장된다.
$2a$10$......
비밀번호 비교는
passwordEncoder.matches(
loginRequest.getPassword(),
member.getPassword()
);
가 수행한다.
Spring Security가
입력한 비밀번호
↓
BCrypt 암호화
↓
DB의 암호화된 비밀번호와 비교
를 자동으로 수행한다.
일치하지 않으면
throw new IllegalArgumentException("비밀번호 불일치");
가 발생한다.
로그인이 성공하면 JWT를 생성한다.
String token = jwtUtil.generateToken(
member.getUsername(),
member.getRole().name()
);
예를 들어
username = sleepy
role = SELLER
이라면
generateToken("sleepy", "SELLER");
가 호출된다.
토큰 생성 메서드
public String generateToken(String username, String role)
에서 JWT를 만든다.
Date now = new Date();
토큰 발급 시간을 저장한다.
.setSubject(username)
내 프로젝트에서는 username을 저장한다.
subject = sleepy
.claim("role", role)
role = SELLER
.setIssuedAt(now)
.setExpiration(...)
설정한 시간이 지나면 토큰은 사용할 수 없다.
.signWith(secretKey, SignatureAlgorithm.HS256)
SECRET_KEY로 서명(Signature)을 만든다.
이 서명이 있기 때문에
를 하면 검증에 실패한다.
.compact();
실행 후
eyJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOiJzbGVlcHkiLCJyb2xlIjoiU0VMTEVSIn0
.
xxxxxxxxxxxxxxxx
와 같은 JWT가 생성된다.
Service는
return LoginResponse.builder()
.memberId(member.getId())
.accessToken(token)
.username(member.getUsername())
.email(member.getEmail())
.nickname(member.getNickname())
.role(member.getRole())
.build();
를 반환한다.
Controller는 JSON으로 변환하여 응답한다.
{
"memberId": 1,
"username": "sleepy",
"nickname": "슬리피",
"role": "SELLER",
"accessToken": "eyJhbGc..."
}
Flutter에서는
등에 저장한다.
로그인은 여기서 끝난다.
상품 등록 요청을 한다고 가정하면
POST /api/products
Header에 JWT를 함께 보낸다.
Authorization: Bearer eyJhbGc...
JWT가 신분증 역할을 한다.
Spring Security는 Controller보다 먼저 JwtFilter를 실행한다.
Authorization 헤더를 가져온다.
String header = request.getHeader("Authorization");
값은
Bearer eyJhbGc...
Bearer를 제거한다.
String token = header.substring(7);
최종적으로
eyJhbGc...
토큰만 남는다.
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
이 과정에서
을 모두 수행한다.
검증이 성공하면
String username = claims.getSubject();
String role = claims.get("role", String.class);
를 통해 사용자 정보를 가져온다.
List<GrantedAuthority> authorities =
List.of(new SimpleGrantedAuthority("ROLE_" + role));
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
username,
null,
authorities
);
SecurityContextHolder.getContext().setAuthentication(auth);
이 순간부터 Spring Security는
현재 요청은
username = sleepy
ROLE = SELLER
인 사용자가 로그인한 요청이라고 인식한다.
@PostMapping
public void create(Authentication authentication) {
String username = authentication.getName();
}
authentication.getName()으로 현재 로그인한 사용자의 username을 얻을 수 있다.
또한
@PreAuthorize("hasRole('SELLER')")
가 있다면
SecurityContext에 저장된 권한을 검사하여
ROLE_SELLER
가 있을 때만 메서드를 실행한다.
없다면
403 Forbidden
을 반환한다.
사용자
│
│ POST /login
▼
Controller
│
▼
MemberService.login()
│
├── DB에서 회원 조회
├── BCrypt 비밀번호 검증
├── JwtUtil.generateToken()
│ ├── username 저장
│ ├── role 저장
│ ├── 발급 시간 저장
│ ├── 만료 시간 저장
│ └── SECRET_KEY로 서명
│
▼
JWT 생성
│
▼
클라이언트에 반환
│
├── SecureStorage / SharedPreferences 저장
│
▼
이후 모든 요청
Authorization: Bearer JWT
│
▼
JwtFilter
│
├── Authorization 헤더 확인
├── Bearer 제거
├── JWT 서명 검증
├── 만료 시간 확인
├── username 추출
├── role 추출
└── SecurityContextHolder에 Authentication 저장
│
▼
Spring Security
│
├── @PreAuthorize 권한 검사
└── Authentication 객체 제공
│
▼
Controller → Service → Repository → DB