1. 프로젝트 생성, 도메인 정의
강의의 login 프로젝트를 가져와서 학습한다. 패키지 구조를 살펴보면, 크게 domain과 web으로 나눠져 있는 것을 볼 수 있다.
도메인
여기서 도메인이란, 화면, UI, 기술 인프라 등을 제외한 시스템이 구현해야하는 핵심 비즈니스 로직이 담긴 영역을 의미한다. 나중에 web이 다른 기술로 바뀌더라도 domain은 유지되어야 개선이 가능하다. 따라서 domain은 web을 의존, 참조하지 않도록 설계하는 것이 매우 중요하다.
2. 회원 가입 기능 개발
홈화면 작성
우선 home 화면을 만든다. templates 폴더에 home.html 파일을 복사한다.
회원 가입 기능 개발
이제 회원 가입 버튼을 누르면 회원가입이 되도록 만들어본다. 우선 회원 정보 클래스인 Member와 회원 정보를 담을 수 있는 MemberRepository 클래스를 작성한다. 눈여겨 볼만한 부분은 MemberRepository의 findByLoginId 메서드의 스트림과 람다 문법이다. Optional을 적용해서 loginId로 조회하는 정보가 null일 경우 처리하는 로직을 filter 메서드를 적용하여 작성한다.
java/hello/login/domain/member/Member.java
1
2
3
4
5
6
7
8
9
10
11
12
|
@Data
public class Member {
private Long id;
@NotEmpty
private String loginId; //로그인 ID (Long으로 NotEmpty 적용불가)
@NotEmpty
private String name; //사용자 이름
@NotEmpty
private String password;
}
|
cs |
java/hello/login/domain/member/MemberRepository.java
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
|
@Slf4j
@Repository
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<>(); //static 사용
private static long sequence = 0L; //static 사용
public Member save(Member member) {
member.setId(++sequence);
log.info("save:member={}", member);
store.put(member.getId(), member);
return member;
}
public Member findById(Long id) {
return store.get(id);
}
public Optional<Member> findByLoginId(String loginId) {
return findAll().stream()
.filter(m -> m.getLoginId().equals(loginId))
.findFirst();
}
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
|
cs |
컨트롤러와 View 파일도 만든다. addMemberForm.html View 파일은 강의 자료에서 복사해온다.
java/hello/login/web/member/MemberController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/add")
public String addForm(@ModelAttribute("member") Member member) {
return "members/addMemberForm";
}
@PostMapping("/add")
public String save(@Validated @ModelAttribute("member") Member member, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "members/addMemberForm";
}
memberRepository.save(member);
return "redirect:/";
}
}
|
cs |
회원가입을 해보면 검증코드와 View로의 포워딩이 잘되는 것을 확인할 수 있다. 추가로, static으로 되어있는 메모리에 데이터를 미리 넣어놓기 위해서 @Postconstruct를 이용한다.
java/hello/login/TestDataInit.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Component
@RequiredArgsConstructor
public class TestDataInit {
private final ItemRepository itemRepository;
private final MemberRepository memberRepository;
/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
Member member = new Member();
member.setLoginId("test");
member.setPassword("test!");
member.setName("테스터");
memberRepository.save(member);
}
}
|
cs |
Repository의 save 메서드에서 log.info가 작동하여 서버 실행 시 바로 테스트 정보가 저장되는 것을 확인할 수 있다.
3. 로그인 기능 개발
기본적인 로그인 기능은 사용자가 Form 데이터에 입력한 password 값과 메모리에 저장되어있는 회원 id에 해당하는 password값이 같은지 보는 것이다.
입력값을 받아오기 위한 loginForm 클래스를 만든다.
java/hello/login/web/login/LoginForm.java
1
2
3
4
5
6
7
8
9
|
@Data
public class LoginForm {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
|
cs |
그리고 검증을 위한 login 메서드를 만든다. 여기서도 Optional 결과에 filter(), orElse() 메서드를 활용하였다.
java/hello/login/domain/login/LoginService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Service
@RequiredArgsConstructor
public class LoginService {
private final MemberRepository memberRepository;
/**
* @return null 로그인 실패
*/
public Member login(String loginId, String password) {
return memberRepository.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
}
|
cs |
마지막으로 로그인 컨트롤러를 추가한다. LoginService의 login 메서드를 활용하고, 에러 발생 시 bindingResult에 reject 메서드를 활용한다. 단순 필드 에러가 아니기 때문에 reject 메서드를 사용하는 것이다.
java/hello/login/web/login/LoginController.java
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
|
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리 TODO
return "redirect:/";
}
}
|
cs |
다만 이렇게만 개발하면, 로그인을 완료해도 loginForm 화면으로 돌아갈뿐 로그인이 유지되지는 않는다. 이를 위해 다음 섹션의 쿠키를 이용한다.
4. 쿠키 사용하여 로그인 상태 유지하기
로그인과 쿠키
로그인에서 쿠키 작동 원리
로그인에 성공하면 서버에서 set-Cookie를 통해 브라우저에 쿠키를 보내준다. 웹 브라우저는 받아온 쿠키를 저장하고 모든 요청에 해당 쿠키를 실어서 서버에 보내게 된다. 그러면 서버에서는 쿠키를 확인하여 모든 로직을 처리하게 된다.
쿠키의 종류
영속 쿠키 : 만료날짜 입력 시 적용되며, 해당 날짜까지 유지
세션 쿠키 : 만료날짜 생략 시 적용되며, 브라우저 종료 시까지만 유지
세션쿠키 방식으로 개발해보자.
쿠키 사용
로그인 컨트롤러에서 로그인이 성공하면 memberId를 쿠키값으로 넘겨주도록 한다. 이를 위해서 HttpServletResponse를 파라미터로 받아와야 한다. Cookie의 두번째 파라미터는 String 값이여야 하기때문에 String.valueOf()을 사용한 것도 참고하자.
java/hello/login/web/member/MemberController.java
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(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
|
cs |
이렇게 작성 후 로그인에 성공하면 Network의 Response Headers 내에 Set-Cookie의 형태로 서버에서 쿠키가 생성되고 브라우저로 전송된 것을 확인할 수 있다.
이후 새로고침 등으로 요청을 보내면, Request Headers에 Cookie값이 들어가있다.
Application 탭의 Cookies 메뉴에서도 확인할 수 있다.
5. 로그아웃 처리
로그아웃은 쿠키의 MaxAge값을 0으로 만들어서 처리한다. 우선 로그인 실패와 성공 시 home 화면을 분기하여 로그아웃 버튼이 나오도록 해본다. 로그인의 성공 여부는 "memberId" 이름을 가진 쿠키를 갖고 있는지에 따라 결정된다. HttpServletRequest를 통해 cookie값을 가져올 수도 있지만, 여기서는 @CookieValue 어노테이션을 활용한다. 그리고 required=false로 지정하여 쿠키값이 없는 사용자도 컨트롤러로 접근할 수 있도록 해준다.
java/hello/login/web/HomeController.java
1
2
3
4
5
6
7
8
9
10
11
|
@GetMapping("/")
public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {
//로그인
Member loginMember = memberRepository.findById(memberId);
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
|
cs |
loginHome.html View로 접속 시 다음과 같은 화면이 출력되도록 한다.
이제 로그아웃 버튼을 눌렀을때 쿠키가 만료되도록 LoginController에 메서드를 추가한다.
java/hello/login/web/login/LoginController.java
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);
}
|
cs |
네트워크의 Response Headers에서 cookie가 만료된 것을 확인할 수 있다.
단순히 이런 방식으로 쿠키를 활용하면 쿠키가 도난되거나 변조되어 보안에서 심각한 문제가 발생할 수 있다. 다음 글에서는 쿠키의 문제점과 세션을 활용하는 방식에 대해서 학습해본다.
참조
1. 인프런_스프링 MVC 2편 - 백엔드 웹개발 핵심 기술_김영한 님 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
'Programming-[Backend] > Spring' 카테고리의 다른 글
[스프링 웹MVC-2] 12. 로그인 - 서블릿 필터 (0) | 2021.09.01 |
---|---|
[스프링 웹MVC-2] 11. 로그인 - 세션 활용 (0) | 2021.08.29 |
[스프링 웹MVC-2] 9. 검증(Validation) - groups, 객체 분리 적용, API 전송에서의 성공, 실패 케이스 (0) | 2021.08.29 |
[스프링 웹MVC-2] 8. 검증(Validation) - Bean Validation 사용하기 (0) | 2021.08.24 |
[스프링 웹MVC-2] 7. 검증(Validation) - 오류코드, 메시지 처리 (0) | 2021.08.22 |