๊ฐ์
์๋ ํ์ธ์. ์ด๋ฒ์๋ ํ๋ก์ ํธ๋ฅผ ํ์๋ฉด์ ๋ก๊ทธ์ธ, ํ์๊ฐ์ , ๋ก๊ทธ์ธ ํ ํ์ ์๋ณ์ ์คํ๋ง์์ ์ด๋ป๊ฒ ํ ์ ์๋์ง์ ๋ํด ๊ณต๋ถํ ๊ณผ์ ์ ๊ณต์ ํ๊ณ ์ ํฉ๋๋ค!
๋ณดํต ์ ์์ ๋ค์ ํ๊ธฐ ์ํด ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ์ จ๋ ๊ฒฝํ์ด ์์ผ์ค ํ ๋ฐ์, ์คํ๋ง ์ํ๋ฆฌํฐ๋ ๋ณ๋์ ํ๋ ์์ํฌ๋ค ๋ณด๋ ๊น์ด ์๊ฒ ํ์ตํ๊ธฐ ์ํด์๋ ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ์คํ๋ง๋ง์ผ๋ก ํด๋ณด๋ ๊ฒ ์ข๊ฒ ๋ค๊ณ ์๊ฐ์ด ๋ค์์ต๋๋ค. (๋ฉํ ๋งํด ์ฃผ์๋ ์ ๋ฐฐ๋๊ป์๋ ๊ฐ์ ์๊ฐ์ ๊ฐ์ง๊ณ ๊ณ์ จ์ต๋๋ค.)
ํฌ๊ฒ ์ฟ ํค, ์ธ์ , 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๊ฐ ๋์ต๋๋ค. (๋๋ค์์ด ๋ฑ๋ก๋์ง ์์๋ ๊ฒ์ผ ๊ฒฝ์ฐ)
๋ก๊ทธ์ธ
์ค์ํ ๊ฒ์ ๋ก๊ทธ์ธ์ด๊ฒ ์ฃ ! ๋ก๊ทธ์ธ ์์๋ ์ฟ ํค๊ฐ ์ ์ฅ๋จ์ ๋ณผ ์ ์์ต๋๋ค. ์ด์ ์ฌํ๊ฐ (๋ธ๋ผ์ฐ์ )์ด ์ธ์ฆ ํฐ์ผ (์ฟ ํค)์ ์ป์์ต๋๋ค.
์ ๋ฆฌ
์ด๋ ์ จ๋์? ์์ง์ ๋ก๊ทธ์ธ ํ ํ์์ ์๋ณํ๋ ๋ก์ง์ด ์์ฑ๋์ง ์์ ์๋ฟ์ง ์์ผ์ค ์๋ ์์ต๋๋ค. ํท๊ฐ๋ฆฌ์ ๋ค๋ฉด ์ฌํ๊ฐ์ด ์ธ์ฆ ํฐ์ผ์ ๋ฐ๋ ๊ณผ์ ์ ์๊ฐํด ์ฃผ์ธ์!
๋ค์ ๊ธ์์๋ ์ธ์ ์ ์ด์ฉํ ๋ก๊ทธ์ธ ๋ฐฉ์์ ์์๋ณด๊ฒ ์ต๋๋ค. ๊ฐ์ฌํฉ๋๋ค!