스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - (33) 로그인 처리 - 쿠키 세션 - 로그인 기능, 쿠키 로그인
인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술편을 학습하고 정리한 내용 입니다.
로그인 기능

일단 ID, PW 입력하는 부분부터 개발.

LoginService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequiredArgsConstructor
@Service
public class LoginService {
private final MemberRepository memberRepository;
/**
* @return null 로그인 실패
* */
public Member login(String loginId, String password) {
return memberRepository.findByLoginId(loginId)
.filter(member -> password.equals(member.getPassword()))
.orElse(null);
}
}
로그인 아이디로 멤버를 가져와서 패스워드가 같으면 멤버를 반환하고, 아니라면 null을 반환하는 메서드.
람다식을 사용.
LoginForm
1
2
3
4
5
6
7
8
9
@Data
public class LoginForm {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
LoginController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RequiredArgsConstructor
@Slf4j
@Controller
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm loginForm ) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Validated @ModelAttribute("loginForm") LoginForm form, BindingResult result) {
if (result.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
result.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 성공 처리 TODO
return "redirect:/";
}
}
폼으로 가는 메서드와, 로그인 처리를 하는 메서드 두 개가 있다.
1
2
3
4
5
6
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
result.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
이 부분이 핵심인데, 멤버가 null이라면, 즉 정보가 잘못 됐다면 우리가 배웠던 글로벌 오류 만드는 방법으로
BindingResult를 활용해 다시 이동 시킨다.
로그인 뷰 템플릿
resources/templates/login/loginForm.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>로그인</h2>
</div>
<form action="item.html" th:action th:object="${loginForm}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
</div>
<div>
<label for="loginId">로그인 ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{loginId}" />
</div>
<div>
<label for="password">비밀번호</label>
<input type="password" id="password" th:field="*{password}" class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{password}" />
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">로그인</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'"
th:onclick="|location.href='@{/}'|"
type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
뭐 어려운 건 없다. 상품 때와 크게 다르지 않다.

아무 값이나 입력하면 저렇게 글로벌 오류가 나온다.

사전에 등록한 아이디/비밀번호를 넣으면

지금 처리를 안 해서 그렇지 홈으로 잘 갔다.
로그인 처리하기 - 쿠키 사용
로그인 상태 유지하기
로그인 상태는 어떻게 유지할 수 있을까?
쿼리 파라미터를 계속 유지하면서 보내는 것은 매우 어렵고 번거로운 작업이다.
쿠키를 사용해 보자.
쿠키
서버에서 로그인을 성공하면 HTTP 응답에 쿠키를 담아서 브라우저에 전달하자.
그러면 브라우저는 앞으로 해당 쿠키를 지속해서 보내준다.

로그인이 완료되면 쿠키를 발급해 memberId=1을 사용자에게 만들어 주고,
사용자는 계속해서 그걸 이용해서 검증 과정을 거친다.

쿠키는 모든 요청에 자동으로 들어가진다.
쿠키에는 영속 쿠키와 세션 쿠키가 있다.
- 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지
- 세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료시 까지만 유지
브라우저 종료 시 로그아웃이 되길 기대하므로, 우리에게 필요한 것은 세션 쿠키이다.
구현
로그인 성공 시 세션 쿠키를 생성하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@PostMapping("/login")
public String login(@Validated @ModelAttribute("loginForm") LoginForm form, BindingResult result, HttpServletResponse response) {
if (result.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
result.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 성공 처리
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
1
2
3
// 로그인 성공 처리
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
로그인에 성공하면 쿠키를 생성하고 HttpServletResponse에 담는다. 쿠키 이름은 memberId이고, 값은 회원의 id를 담아둔다.
웹 브라우저는 종료 전까지 회원의 id를 서버에 계속 보내줄 것이다.

쿠키가 잘 들어간 걸 볼 수 있다.
이제 홈 화면을 처리해 보자.
홈 - 로그인 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
public class HomeController {
private final MemberRepository memberRepository;
//@GetMapping("/")
public String home() {
return "home";
}
@GetMapping("/")
public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {
if (memberId == null) {
return "home";
}
// 로그인
Member loginMember = memberRepository.findById(memberId);
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
}
- 기존
home()은 컨트롤러 안 타게 주석 처리 @CookieValue를 사용하면 편리하게 쿠키를 조회할 수 있다.- 로그인 하지 않는 사용자도 홈에 접근할 수 있기 때문에
required = false를 사용한다.
로직 분석
- 로그인 쿠키 (
memberId)가 없는 사용자는 기존home으로 보낸다. 추가로 로그인 쿠키가 있어도 회원이 없으면home으로 보낸다. - 로그인 쿠키 (
memberId)가 있는 사용자는 로그인 사용자 전용 홈 화면인loginHome.html로 보낸다. 추가로 홈 화면에 회원 관련 정보도 출력해야 해서member데이터도 모델에 담아서 전달한다.
홈 - 로그인 사용자 전용 화면
resources/templates/loginHome.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>홈 화면</h2>
</div>
<h4 class="mb-3" th:text="|로그인: ${member.name}|">로그인 사용자 이름</h4>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/items}'|">
상품 관리
</button>
</div>
<div class="col">
<form th:action="@{/logout}" method="post">
<button class="w-100 btn btn-dark btn-lg" onclick="location.href='items.html'" type="submit">
로그아웃
</button>
</form>
</div>
</div>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
th:text="|로그인: ${member.name}|": 로그인 성공 시 사용자 이름을 출력- 로그아웃 버튼 추가
로그아웃 기능
로그인 컨트롤러에 로그아웃 기능을 추가하자.
1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
memberId 쿠키를 cookie.setMaxAge(0) 로 아예 만료 시켜버리는 것이다.

로그인이 된 화면이고,
로그아웃 시

다음과 같이 memberId에 값이 없고, 만료 됐다.
쿠키와 보안 문제
쿠키를 사용해서 로그인 아이디를 전달해서 로그인을 유지할 수 있었다.
그런데 여기에는 심각한 보안 문제가 있다.
보안 문제
- 쿠키의 값은 임의로 변경 될 수 있다.
- 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.
- 실제 웹 브라우저 개발자 모드 → Application → Cookie 변경으로 확인
Cookie:member1→Cookie:member2이렇게 바꾸면 다른 사용자가 됨.

이렇게 강제로 바꾸고 새로 고침 하면?

이렇게 바뀐다.
- 쿠키에 보관된 정보는 훔쳐갈 수 있다.
- 만약 쿠키에 개인정보나, 신용카드 정보가 있다면?
- 이 정보가 웹 브라우저에 보관되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다.
- 쿠키에 정보가 나의 로컬 PC에서 털릴 수도 있고, 네트워크 전송 구간에서 털릴 수도 있다.
- 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.
- 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 계속 시도할 수 있다.
대안
- 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출하고, 서버서 토큰과 사용자 id를 매핑해서 인식한다. 그리고 서버에서 토큰을 관리한다.
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
- 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료 시간을 짧게(ex. 30분) 유지한다. 또는 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 된다.
댓글남기기