본문 바로가기

Server/SpringBoot

[SpringBoot] JWT 로그인 구현하기(1)

JWT 방식 로그인을 선택하는 이유

1. 세션 기반 인증

JWT에 대해 알아보기 전에, 전통적으로 널리 사용되어 오고있는 세션 기반 인증 방식에 대해 알아보도록(까보도록) 하겠습니다.

세션 기반 인증은 로그인 후 서버에서 세션 ID를 생성서버 메모리에 저장합니다.

클라이언트는 쿠키에 응답받은 세션 ID를 저장합니다.

이후 요청마다 자동으로 쿠키를 전송하여 로그인한 사용자인지 식별합니다.

 

장점

- 구현이 비교적 쉬움

- 로그아웃, 강제 만료 등 로그인 상태를 서버에서 직접 관리

- 최초 쿠키만 세팅하면 클라이언트에서 신경쓸게 없음

 

단점

- 모든 로그인한 사용자의 상태를 서버 메모리에 저장

- 서버가 여러개인 환경에서는 세션을 공유해야 하는 문제 발생

- 쿠키 탈취 위험

- 웹브라우저에 의존적

 

2. JWT 방식

JWT 방식에서는 서버가 로그인 상태를 저장하지 않고, 클라이언트에서 토큰을 보관합니다.

서버에서는 토큰의 유효성만 검증하면 되기 때문에, 확장성 측면에서 유리합니다.

 

 


JWT란?

JSON 형태의 정보가 인코딩 된 토큰입니다.

 

구성은 다음과 같습니다.

{Header}.{Payload}.{Signature}

 

예시 :

{"alg": "HS256", "typ": "JWT"}.{"id": "1", "role": "ADMIN", "exp": 1730609000}.{signature}

 

Header: 토큰을 파싱하고 검증하기 위한 정보

Payload: 발급 대상, 권한, 유효시간 및 커스텀 데이터 등

Signature: 우리 서버에서 발급한게 맞는지 식별하기 위한 서명

 

실습을 진행하며 토큰이 정말 이렇게 생겼는지 같이 확인해 보겠습니다.

 

 


로그인

이제부터 로그인을 구현해 보겠습니다.

우선 로그인은 "인증"과 "인가"로 나뉩니다.

인증(Authentication)은 "누구세요?", 인가(Authorization)는 "너 뭐 돼?" 라고 보면 됩니다.

호텔 투숙객으로 예시를 들어보겠습니다.

카운터에서 호텔에 예약한 사람이 신분증을 제시하고 예약한 사람이 맞는지 확인하고 객실키를 받는 것이 인증입니다.

이 사람이 자기가 예약한 505호 방으로 키를 찍고 들어가는 것이 인가입니다. 504호에 키를 찍으면 권한이 없어서 들어갈 수 없겠죠?

인증

이제 로그인 중 "인증"을 먼저 구현해 보도록 하겠습니다.

시스템에서 인증 과정은 다음과 같습니다.

1. ID/PW를 통해 로그인 요청

2. ID/PW 검증 및 회원 식별

3. 토큰 발급

+ 클라이언트는 토큰을 스토리지에 저장 (클라이언트 구현은 실습에 없기 때문에, 저는 메모장에 저장하겠읍니다.)

 

AuthController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {

    private final AuthService authService;

    @PostMapping
    public ResponseEntity<AuthDto.LoginResponse> login(@RequestBody AuthDto.LoginRequest request) {
        return ResponseEntity.ok(authService.login(request));
    }
}

 

AuthDto.java

public class AuthDto {
    @Getter @Builder
    public static class LoginRequest {
        private String userId;
        private String userPw;
    }

    @Getter @Builder
    public static class LoginResponse {
        private String refreshToken;
        private String accessToken;
    }
}

 

AuthService.java

@Service
@RequiredArgsConstructor
public class AuthService {
    private String secretKey = "jaeng";
    private final static Long ACCESS_TOKEN_VALIDITY = 30 * 60 * 1000L; // 30분
    private final static Long REFRESH_TOKEN_VALIDITY = 12 * 60 * 60 * 1000L; // 12시간


    private final UserRepository userRepository;

    @Transactional
    public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
        User user = userRepository.findByUserId(request.getUserId()).orElseThrow(() -> loginFailException());

        if (!Objects.equals(user.getUserPw(), request.getUserPw())) {
            throw loginFailException();
        }

        return AuthDto.LoginResponse.builder()
                .accessToken(createToken(user.getId(), ACCESS_TOKEN_VALIDITY))
                .refreshToken(createToken(user.getId(), REFRESH_TOKEN_VALIDITY))
                .build();
    }

    private String createToken(Long sub, Long expiresIn) {
        Claims claims = Jwts.claims();
        claims.put("sub", String.valueOf(sub)); // 커스텀 데이터
        Date now = new Date();
        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 토큰 헤더 정보
                .setClaims(claims) // 페이로드
                .setIssuedAt(now) // 발급시간
                .setExpiration(new Date(now.getTime() + expiresIn)) // 만료시간
                .signWith(SignatureAlgorithm.HS256, secretKey) // 서명
                .compact();
    }

    private SecurityException loginFailException() {
        return new SecurityException("로그인 정보가 잘못되었습니다.");
    }
}

 

토큰에는 다음과 같은 정보가 포함됩니다.

1. Header : 토큰 타입 정보, 서명 알고리즘 정보 => { "typ": "JWT", "alg": "HS256" } 형태로 들어갑니다.

2. Payload : claims로 세팅한 데이터, 발급시각, 만료시각 => { "sub": "1", "iat": "..", "exp": ".." }, 인가에서 필요한 정보를 추가할 수 있습니다.

3. Signature : 시크릿키 값

 

결과 확인

회원 정보를 올바르게 입력하면 이렇게 토큰 정보를 응답받습니다.

 

토큰 내용은 jwt.io 에서 확인할 수 있습니다.

createToken에서 생성한 내용이 잘 들어 가있군요 후후.

 

여기까지는 로그인 중 인증 과정을 알아보았습니다.

다음편에서 본격적인 Spring Security 구성과 인가를 구현해 보겠습니다.