개요
안녕하세요. 이번에는 프로젝트를 하시면서 로그인, 회원가입, 로그인 한 회원 식별을 스프링에서 어떻게 할 수 있는지에 대해 공부한 과정을 공유하고자 합니다!
보통 위 작업들을 하기 위해 스프링 시큐리티를 사용하셨던 경험이 있으실 텐데요, 스프링 시큐리티는 별도의 프레임워크다 보니 깊이 있게 학습하기 위해서는 스프링 시큐리티를 사용하지 않고 스프링만으로 해보는 게 좋겠다고 생각이 들었습니다. (멘토링해 주시는 선배님께서도 같은 생각을 가지고 계셨습니다.)
크게 쿠키, 세션, JWT 방식으로 회원가입 & 로그인을 해 보고, 이것을 어노테이션으로 편리하게 사용하는 등의 방식으로 개선시키는 내용을 작성하겠습니다.
그럼 첫 번째 글에서는 쿠키 방식을 이용한 회원가입 & 로그인에 대해 알아보겠습니다!
따라 할 내용이 많을 것 같아 코드를 제공드리겠습니다.
이곳에서 받으실 수 있습니다!
빠른 구현 설명을 위해 테스트는 작성하지 않았습니다 :)
쿠키
먼저 쿠키가 무엇일까요? 쿠키는 서버에서 사용자 브라우저에게 전달하는 작은 데이터입니다.
원래 HTTP는 무상태 (Stateless) 특성을 가지고 있어서 서버가 클라이언트의 상태를 보존하지 않습니다.
즉, 브라우저가 서버에 접근할 때마다 우리가 누구인지를 알려주어야 합니다. 이때 활용할 수 있는 방법 중 쿠키가 있습니다.
최초 시점에 회원이 로그인할 시 서버가 브라우저에게 쿠키를 주면, 앞으로 로그인 한 회원이 다른 페이지들을 탐색할 때마다 브라우저는 해당 쿠키를 가지고 돌아다니게 됩니다. 그럴 때마다 서버는 브라우저가 가진 쿠키 내용을 토대로 누구인지를 식별할 수 있습니다. 결과적으로 이를 통해 사용자는 매번 인증하지 않고도 상태를 유지할 수 있습니다. (= Stateless를 Stateful"스럽게" 이용하도록 보완, 여전히 서버는 브라우저에 대한 상태를 저장하지 않음 - 브라우저가 정보를 제공)
마치 여행객이 입국 시 여권에 인증을 받고, (다소 깐깐한 나라일 시) 돌아다닐 때마다 신분증을 검사받는 것과 비슷하다고 할 수 있습니다!
대략적으로 쿠키에 대해 공부해 봤으니, 실제 구현을 보겠습니다. 공통적인 내용 (Member 등)은 올려진 코드를 참고해 주세요!
AuthController
AuthController는 아래와 같습니다. (로그인/회원가입은 AuthController에서, 회원 식별 등 회원 기능에서는 MemberController에서 다루도록 하겠습니다.)
// import 표현은 생략
@RequiredArgsConstructor
@RequestMapping("/auth")
@RestController
public class AuthController {
private static final String COOKIE_NAME = "AUTH";
private static final int EXPIRATION_SECONDS = 60 * 60;
private final AuthService authService;
@PostMapping("/signup")
public ResponseEntity<MemberCreateResponse> createMember(@RequestBody @Valid final MemberCreateRequest request) {
Long memberId = authService.signup(request);
return ResponseEntity.ok()
.body(new MemberCreateResponse(memberId));
}
@PostMapping("/login/cookie")
public ResponseEntity<Void> loginWithCookie(@RequestBody @Valid final LoginRequest request,
final HttpServletResponse response) {
Member loginMember = authService.loginWithCookieAndSession(request);
Cookie loginCookie = generateCookieByMember(loginMember);
response.addCookie(loginCookie);
return ResponseEntity.ok()
.build();
}
private Cookie generateCookieByMember(final Member member) {
String cookieValue = generateCookieValueByMember(member);
Cookie cookie = new Cookie(COOKIE_NAME, cookieValue);
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge(EXPIRATION_SECONDS);
cookie.setHttpOnly(true);
return cookie;
}
private String generateCookieValueByMember(final Member member) {
return member.getNickname() + "." + member.getPassword();
}
}
- 회원가입 (/auth/signup) 시 MemberCreateRequest (username, nickname, password)를 기반으로 회원을 만들고, 만든 회원의 id만을 반환합니다.
- 쿠키 방식 로그인 (/auth/login/cookie) 시 LoginRequest (nickname, password)를 서비스에 전달하여 관련된 회원을 찾고, 회원의 속성을 이용해 쿠키를 만듭니다.
- 쿠키를 만들 때 Path (사용 가능한 경로), Secure (SSL 환경 암호화), MaxAge (유효 기간), HttpOnly (자바스크립트에서의 사용 방지) 등 추가적인 보안 및 설정 등을 붙여줍니다.
- 쿠키는 키-값 구조로 이루어져 있습니다. 키 이름을 AUTH, 값을 회원의 닉네임 + "." + 회원의 비밀번호로 하겠습니다. (원래는 비밀번호도 암호화 처리를 해야 하지만, 해당 글에서의 주제를 벗어난 것 같아 넘기겠습니다.)
- HttpServletResponse는 스프링 서블릿 환경에서 사용할 수 있는 브라우저와 서버 간의 요청-응답 시 활용된 객체를 뜻합니다. (반대로 요청을 의미하는 HttpServletRequest도 있습니다.) 이 객체에 만든 쿠키를 전달합니다.
AuthService
다음은 서비스 코드를 보겠습니다.
// import 표현은 생략
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class AuthService {
private final MemberRepository memberRepository;
@Transactional
public Long signup(final MemberCreateRequest request) {
validateIsNotUsedNickname(request.nickname());
MemberAuth memberAuth = new MemberAuth(request.nickname(), request.password());
Member newMember = new Member(request.username(), memberAuth);
Member createMember = memberRepository.save(newMember);
return createMember.getId();
}
private void validateIsNotUsedNickname(final String nickname) {
memberRepository.findByNickname(nickname)
.ifPresent(member -> {
throw new AlreadyUsedNicknameException();
});
}
private Member findMemberByNickname(final String nickname) {
return memberRepository.findByNickname(nickname)
.orElseThrow(MemberNotFoundException::new);
}
public Member loginWithCookieAndSession(final LoginRequest request) {
return findMemberByNickname(request.nickname());
}
}
- 저희는 이름 (username)은 동명이인을 고려하여 중복을 허용하고, 닉네임 (nickname)은 중복 처리하지 않을 것입니다. 닉네임이 고유하다는 점을 이용하여 쿠키를 만들 수 있습니다.
- 쿠키와 향후 다룰 세션에서는 회원 정보가 필요하기에 Member 자체를 반환하도록 할 것입니다.
- Member는 이름 (username)과 MemberAuth (밸류 타입, nickname과 password 내장)를 가지는 형태입니다.
실행 결과
회원가입
회원가입을 포스트맨으로 실행해 보면 아래와 같이 생성된 memberId가 나옵니다. (닉네임이 등록되지 않았던 것일 경우)
로그인
중요한 것은 로그인이겠죠! 로그인 시에는 쿠키가 저장됨을 볼 수 있습니다. 이제 여행객 (브라우저)이 인증 티켓 (쿠키)을 얻었습니다.
정리
어떠셨나요? 아직은 로그인 한 회원을 식별하는 로직이 작성되지 않아 와닿지 않으실 수도 있습니다. 헷갈리신다면 여행객이 인증 티켓을 받는 과정을 생각해 주세요!
다음 글에서는 세션을 이용한 로그인 방식을 알아보겠습니다. 감사합니다!
Reference
'🚀 팁 (기술 적용 방법 등)' 카테고리의 다른 글
[Github] Github의 Issue와 PR (Pull Request) 알아보기 (PR 병합 후 이슈가 자동으로 닫히게 하려면?) (0) | 2024.05.18 |
---|---|
[Spring REST Docs ✍️] 어렵게만 느껴졌던 REST Docs를 적용해보자! (2) (0) | 2024.03.05 |
[Spring MVC 🌐] 회원 식별을 해 보자! (2) - 세션 적용 방법 📦 (1) | 2024.02.10 |
[Spring REST Docs ✍️] 어렵게만 느껴졌던 REST Docs를 적용해보자! (1) (1) | 2024.02.07 |