[DDSP] 로그인 기능 (jwt)

이프·2024년 10월 5일

back-end

목록 보기
5/16

로그인 기능의 전반적인 흐름

로그인 기능은 대표적인 3가지 방법이 소개된다.
1. Cookie를 이용한 로그인
2. Session을 이용한 로그인
3. token을 이용한 로그인
우리는 token을 이용한 로그인을 채택했다.

이유는 아래와 같다.

  • Cookie와 Session에 대한 문제점
    • 해당 내용은 다른 블로그나 인터넷에 정리된 부분이 많음.
  • 배포 시 ec2 free tier를 사용할 것이다. 개발 단계에 t1.micro 의 1GB RAM 부하를 최소화하기 위해 stateless를 사용하는 것이다.

Presentation Layer

Controller

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberOpenController implements MemberOpenControllerDocs {

    private final MemberService memberService;

    @PostMapping("/login")
    public ResponseEntity<API<TokenResponse>> login(
        @RequestBody @Valid final MemberLoginRequest request
    ) {
        final API<TokenResponse> response = memberService.login(request);
        return ResponseEntity.ok(response);
    }

}

로그인은 Token 기능을 사용했으므로 Token Response를 응답한다.
로그인은 어떻게 보면 사용자 정보를 가져오는 것(GET)이다.
그런데 POST METHOD를 사용한 이유가 무엇일까?

  1. password가 노출되면 안된다. get을 사용한다면 http url에 노출이 된다.
  2. 내가 생각하는GET METHOD는 저수준 모듈 -> 저수준 모듈에 의존할 수 있어야 한다고 생각했다. 즉, DB에서만 가져올 수 있는 정보라면 GET METHOD를 사용하기 충분하다고 생각한다.
    • 역설로 로그인은 DB에서만 가져오는 정보가 아닌가??
      • 이에 대해서는 아니라고 할 수 있다. 이유는 아래와 같다.
      • 회원가입 시 PW는 Bcrypt 암호화를 한다.
      • 로그인 시 Token 정보를 발급해야한다.
      • 즉, DB에서만 가져오는 작업 외 우리 비즈니스 로직이 따로 필요하므로 Post Method를 통해 통신했다.

Business Layer

Member Service


    @Transactional(readOnly = true)
    public API<TokenResponse> login(final MemberLoginRequest request) {
        final Member member = authenticateMember(request.email(), request.password());
        member.validateStatus();

        final TokenResponse tokenResponse = tokenService.tokenGenerator(member);
        return API.of(MemberStatusType.MEMBER_LOGIN_SUCCESS, tokenResponse);
    }
    
    
	private Member authenticateMember(final String email, final String password) {
        return memberRepository.findByEmailValue(email)
            .filter(member -> encryptor.matches(password, member.getPassword()))
            .orElseThrow(() -> new NotFoundException(MemberExceptionType.NOT_FOUND_ACCOUNT));
    }

로그인 시 Email과 password 검증을 동시에 한다.
email 정보가 틀렸다는 예외를 주게 되면, 해커가 맞는 이메일 정보를 찾을 때까지 brute force로 확인할 수 있다. password 또한 같은 이유로 email과 password를 동시에 검증하고 Email과 Password를 다시 확인하라는 예외를 던진다.

member의 상태에 따라 휴면 회원인지 탈퇴처리 중인 회원인지에 대한 예외도 발생하는데 memberStatus는 객체의 변수이므로 member 도메인 내부에서 처리한다.


Token Service

@Service
@RequiredArgsConstructor
public class TokenService {

    private final AuthHelper authHelper;

    private static final String KEY_MEMBER_ID = "memberId";
    private static final String KEY_MEMBER_NICKNAME = "memberNickname";
    private static final String KEY_MEMBER_ROLE = "memberRole";
    private static final String KEY_MEMBER_IMAGE = "memberImageUrl";

    public TokenResponse tokenGenerator(final Member member) {
        return getTokenResponse(
                createAccessTokenData(member),
                createRefreshTokenData(member)
        );
    }
 
     private Map<String, Object> createAccessTokenData(final Member member) {
        final Map<String, Object> data = new HashMap<>();
        data.put(KEY_MEMBER_ID, member.getId());
        data.put(KEY_MEMBER_NICKNAME, member.getNickname());
        data.put(KEY_MEMBER_ROLE, member.getRole());
        data.put(KEY_MEMBER_IMAGE, member.getImageUrl());
        return data;
    }
    
    private Map<String, Object> createRefreshTokenData(final Member member) {
        final Map<String, Object> data = new HashMap<>();
        data.put(KEY_MEMBER_ID, member.getId());
        return data;
    }
 
 
 }

Token을 하나의 도메인으로 보고 Token Service 로직을 담고 있다. 로그인의 경우 accessToken과 refreshToken을 반환하는데 refreshToken으로 재발급한다면, 토큰만의 추가적인 역할이 있는 것이고 재사용성이나 확장을 생각해서 따로 도메인으로 분리했다.

accessToken으로 Resolver를 통해 사용자 정보를 가져오도록 하고 있다. 이 때, 클라이언트는 accessToken이 만료된다면 refreshToken을 재발급 받게된다. 그래서 재발급 시 재사용 될 수 있는 사용자 정보 관련 토큰에 대해서 관심사를 분리했다.


Auth Helper

public interface AuthHelper {

    String issueAccessToken(Map<String, Object> data);
    String issueRefreshToken(Map<String, Object> data);
    void validationTokenWithThrow(String token);
    String resolveToken(String token);
    AuthPayload parseToken(String token);

}

Token Service에서 사용하는 AuthHelper의 경우 interface로 추상화해서 구현체를 컴포넌트로 등록했다.

jjwt의 경우 version이 업그레이드 되면서 Deprecated되는 것들이 있다. 즉, jjwt 에서 추구하는 버전이 바뀌면서 우리도 AuthHelper 구현체의 코드를 바꿔야 할 수 있다. 혹은 jjwt 외에 더 좋은 토큰 관련 라이브러리가 출시 될 때 쉽게 교체할 수 있다.


회고

로그인의 경우 정말 다양한 방식으로 구현할 수 있는 것 같다. 이번에 토큰을 사용하면서

  1. 토큰 정보에 노출해주어야 할 정보가 무엇인지 선정하는 것
  2. 토큰 관련 테스트

이 두가지에 대해서 큰 난관을 느낀 것 같다.
토큰 관련 테스트는 Repository에서 확인할 수 있다.

profile
if (이런 시나리오는 어떨까?) then(테스트로 검증하고 해결) else(다음 시나리오 고민)

0개의 댓글