Spring Security + JWT 로그인

code++·2026년 7월 14일

JWT 인증이란?

JWT(JSON Web Token)는 로그인 후 서버가 사용자에게 발급하는 인증 토큰이다.

기존 Session 방식은 서버가 로그인 정보를 저장하지만,

JWT 방식은 사용자 정보와 권한을 토큰에 담아 클라이언트가 보관한다.

이후 요청마다 JWT를 보내면 서버는 토큰만 검증하여 로그인 여부를 판단한다.


전체 흐름

로그인 요청
      ↓
Controller
      ↓
Service
      ↓
DB에서 회원 조회
      ↓
비밀번호 검증
      ↓
JWT 생성
      ↓
클라이언트에게 JWT 반환
      ↓
클라이언트가 JWT 저장
      ↓
이후 모든 요청에 JWT 포함
      ↓
JwtFilter가 JWT 검증
      ↓
SecurityContext에 인증 정보 저장
      ↓
Controller / Service 실행

1. 로그인 요청

사용자가 로그인 버튼을 누르면

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에서 처리한다.


2. 회원 조회

Service에서는 먼저 username이 존재하는지 확인한다.

Member member = memberRepository.findByUsername(loginRequest.getUsername())
        .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저"));

실행 순서

입력한 username
        ↓
MemberRepository
        ↓
DB 조회
        ↓
Member 반환

회원이 없다면 로그인은 실패한다.


3. 비밀번호 검증

DB에는 평문 비밀번호가 저장되지 않는다.

예를 들어 사용자가 입력한 비밀번호가

1234

라고 해도,

DB에는 BCrypt로 암호화된 값만 저장된다.

$2a$10$......

비밀번호 비교는

passwordEncoder.matches(
        loginRequest.getPassword(),
        member.getPassword()
);

가 수행한다.

Spring Security가

입력한 비밀번호
        ↓
BCrypt 암호화
        ↓
DB의 암호화된 비밀번호와 비교

를 자동으로 수행한다.

일치하지 않으면

throw new IllegalArgumentException("비밀번호 불일치");

가 발생한다.


4. JWT 생성

로그인이 성공하면 JWT를 생성한다.

String token = jwtUtil.generateToken(
        member.getUsername(),
        member.getRole().name()
);

예를 들어

username = sleepy
role = SELLER

이라면

generateToken("sleepy", "SELLER");

가 호출된다.


5. JwtUtil에서 토큰 생성

토큰 생성 메서드

public String generateToken(String username, String role)

에서 JWT를 만든다.

현재 시간 저장

Date now = new Date();

토큰 발급 시간을 저장한다.

Subject 저장

.setSubject(username)

내 프로젝트에서는 username을 저장한다.

subject = sleepy

권한 저장

.claim("role", role)
role = SELLER

발급 시간

.setIssuedAt(now)

만료 시간

.setExpiration(...)

설정한 시간이 지나면 토큰은 사용할 수 없다.

Signature 생성

.signWith(secretKey, SignatureAlgorithm.HS256)

SECRET_KEY로 서명(Signature)을 만든다.

이 서명이 있기 때문에

  • username 변경
  • role 변경
  • 토큰 위조

를 하면 검증에 실패한다.

JWT 완성

.compact();

실행 후

eyJhbGciOiJIUzI1NiJ9
.
eyJzdWIiOiJzbGVlcHkiLCJyb2xlIjoiU0VMTEVSIn0
.
xxxxxxxxxxxxxxxx

와 같은 JWT가 생성된다.


6. 로그인 응답

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..."
}

7. 클라이언트가 JWT 저장

Flutter에서는

  • SecureStorage
  • SharedPreferences

등에 저장한다.

로그인은 여기서 끝난다.


8. 이후 모든 요청

상품 등록 요청을 한다고 가정하면

POST /api/products

Header에 JWT를 함께 보낸다.

Authorization: Bearer eyJhbGc...

JWT가 신분증 역할을 한다.


9. JwtFilter 실행

Spring Security는 Controller보다 먼저 JwtFilter를 실행한다.

Authorization 헤더를 가져온다.

String header = request.getHeader("Authorization");

값은

Bearer eyJhbGc...

Bearer를 제거한다.

String token = header.substring(7);

최종적으로

eyJhbGc...

토큰만 남는다.


10. JWT 검증

Claims claims = Jwts.parserBuilder()
        .setSigningKey(secretKey)
        .build()
        .parseClaimsJws(token)
        .getBody();

이 과정에서

  • 토큰 서명 검증
  • SECRET_KEY 검증
  • 토큰 위조 여부 확인
  • 만료 시간 확인
  • JWT 형식 확인

을 모두 수행한다.

검증이 성공하면

String username = claims.getSubject();
String role = claims.get("role", String.class);

를 통해 사용자 정보를 가져온다.


11. SecurityContext에 저장

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

인 사용자가 로그인한 요청이라고 인식한다.


12. Controller 실행

@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

정리

  • Controller : 로그인 요청을 받아 Service를 호출한다.
  • Service : 회원 조회, 비밀번호 검증, JWT 생성까지 담당한다.
  • JwtUtil : JWT 생성과 검증을 담당한다.
  • JwtFilter : 요청마다 JWT를 검사하고 SecurityContext에 인증 정보를 저장한다.
  • SecurityContextHolder : 현재 요청의 로그인 정보를 저장한다.
  • Spring Security : SecurityContext를 이용하여 인증과 권한 검사를 수행한다.
profile
일상

0개의 댓글