Java - Spring &&n SpringBoot

spring - 로그인 기능 구현하기 (2) 세션으로 로그인 하기

TerianP 2022. 1. 18.
728x90

오늘은 이전 시간에 이어서 페이지 로그인을 구현해보겠습니다.

다만 이번에는 그냥 쿠키를 활용하는 방식이 아니라 세션을 통해서 로그인 가능하도록 만들어 보겠습니다. 이전과 가장 큰 차이점은 보안!! 입니다.

 

1. 세션 Session

- Cookie, 보안에 취약한 너는 버린다! => Session 

세션과 쿠키의 가장 큰 차이점은 아무래도 '보안' 에 집중된다. 쿠키는 기본적으로 위, 변조가 가능하기 때문에 잘못했을 경우 해커에게 쿠키값이 털려서 회원정보, admin 으로 로그인이라도하는 날에는 DB까지 전부 털리는 경우가 있기 때문이다.

 

Session은 서버 - 클라이언트간 통신 시 쿠키의 취약점을 많이 줄 일 수 있다. 그렇다면 세션은 어떻게 동작할까? 아래 그림을 살펴보자

  • 회원이 로그인 페이지에서 로그인을 시도한다.
  • 로그인 정보가 POST 방식으로 서버에 전달된다.
  • 서버는 전달받은 로그인 정보를 DB 의 내용과 비교 후 확인한다. 만약 일치하는 정보가 있다면 세션 저장소에서 임의의 SessionID(토큰)을 생성 후 해당 회원의 정보가 저장된 객체(admin_member) 와 매핑한다.
  •  서버는 세션 저장소에서 매핑된 내용 중 SessionID 를 쿠키값으로 삼아서 쿠키 저장소에 전달한다.
  • 이후 클라이언트와 서버가 통신하며 회원을 확인, 조회할때는 SessionID 를 서버로 전달하고 서버에서는 세션 저장소에 저장된(매핑된) 내용을 비교하여 회원을 확인하고 클라이언트 웹 브라우저에 필요 내용을 전달한다.
  • 쿠키와 세션이 완전히 별개의 개념은 아니다. 생각해보면 긴밀하게 연결되어있다고 본다. 즉, 기존의 쿠키값에 회원의 특정 정보(예를 들어 memberCode)를 저장하고 이것이 서버와 클라이언트 사이를 왔다갔다하면서 인증하는 방식이었다면, 세션은 같은 쿠키라도 랜덤한 세션 토큰을 사용하여 서버에는 세션 저장소와 클라이언트에는 세션 저장소에 이를 저장하고 데이터 송, 수신시 이를 활용하여 회원을 인증하는 방식이다.

 

 

2. 세션 직접 만들어서 활용하기

1) 이제 세션을 직접 만들어서 사용해보겠습니다. 추가 수정되는 코드는 다음과 같습니다

  • SessionManager : 세션을 생성하여 세션 저장소에 토큰과 로그인 객체를 맵핑하여 저장하고, 세션을 삭제하기 위해 사용되는 세션 관리자
  • HomeController : 홈 페이지 접속 시 세션 토큰 유무에 따라 접속을 관리하는 Controller
  • LoginController : 로그인 시 세션을 생성하여 home으로 보내주거나 로그아웃 시 세션을 삭제하는 역할

- SessionManager

package HJproject.Hellospring.Session;

import org.springframework.stereotype.Component;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Component
// 세션을 관리해주는 기능
public class SessionManager {

    public static final String SESSION_COOKIE_NAME = "mySessionId";

    // 동시성 문제가 있는 경우 ConcurrentHashMap 를 사용해야 함.
    // 동시성 문제란 한마디로 '동시에' 스레드가 많이 발생하는 경우가 있는 것을 의미함 => 로그인은 동시에 많이 발생할 수 있음
    private Map<String, Object> sessionStore = new ConcurrentHashMap<>();

    /*
    *  세션 생성
    *  1. SessionId 생성 (임의의 추정 불가능한 랜덤 값)
    *  2. 세션 저장소에 SessionId 와 보관할 값 저장
    *  3. SessionId 로 응답 쿠키를 생성해서 클라이언트에 전달
    * */

    public void createSession(Object value, HttpServletResponse response){

        // SessionId 생성, 값을 세션에 저장
        String SessionId = UUID.randomUUID().toString(); // UUID 를 활용한 SessionId 생성
        sessionStore.put(SessionId, value); // 세션 저장소에 SessionId 와 보관할 값 저장

        // 쿠키 생성 : 쿠키 이름은 SESSION_COOKIE_NAME , 값은 SessionId
        Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, SessionId);
        response.addCookie(mySessionCookie);
    }


/*    세션 조회 방법 1 : getSession 메서드 안에 쿠키를 찾는 내용 포함

    public Object getSession(HttpServletRequest request){
        Cookie[] cookies = request.getCookies(); // 쿠키는 배열로 반환된다.

        if(cookies == null){
            return null;
        }


        // 배열이기 때문에 안에 있는 값을 for 문으로 찾아옴
        // for 문이 돌면서 getName 햇을 때 SESSION_COOKIE_NAME 와 동일한 값이 있다면
        // 쿠키 저장소에서 해당 쿠키의 value 를 가져옴
       for (Cookie cookie : cookies){
            if(cookie.getName().equals(SESSION_COOKIE_NAME)){
                return sessionStore.get(cookie.getValue());
            }
        }

        return null;
    }*/

    /* 세션 조회 방법 2 : findCookie 를 따로 생성해서 해당 메서드를 사용해 쿠키값을 찾아옴 */

    public Object getSession(HttpServletRequest request){
        // SessionCookie 에 findCookie 를 메서드를 사용해서 찾아온 SESSION_COOKIE_NAME 를 저장함
        Cookie Sessioncookie = findCookie(request, SESSION_COOKIE_NAME);
        if(Sessioncookie == null){
            return null;
        }
        return sessionStore.get(Sessioncookie.getValue());
    }

    public Cookie findCookie(HttpServletRequest request, String cookieName){
        // 여기서 CookieName 는 SESSION_COOKIE_NAME 의미

        Cookie[] cookies = request.getCookies();

        if(cookies == null){
            return null;
        }
        return Arrays.stream(cookies) // arrays 를 스트림을 바꿔줌
                .filter(cookie -> cookie.getName().equals(cookieName))
                /*
                findfirst 와 findAny 둘중 하나를 쓸 수 있는데
                1. findAny : 순서 상관X! 빨리 나오면 꺼내옴
                2. findfrist : 순서 중요! 순서에 따라서 돌다가 맞으면 꺼내옴
                 */
                .findAny()
                .orElse(null);
    }

    /*
    * 3. 세션 만료 : Session 만료는 그냥 지워버리면 된다
    */
    public void expireCookie(HttpServletRequest request){
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);

        // findCookie 로 세션을 가져와서 해당 값이 null 이 아니면 세션 스토어에 저장, 매핑된 값을 삭제!
        if(sessionCookie != null){
            sessionStore.remove(sessionCookie.getValue());
        }
    }
}

 

- HomeController

package HJproject.Hellospring.Controller;


import HJproject.Hellospring.Session.SessionManager;
import HJproject.Hellospring.domain.member.Member;
import HJproject.Hellospring.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;


@Controller
@RequiredArgsConstructor
public class HomeController {

    private final MemberRepository memberRepository;
    private final SessionManager sessionManager;

//    @GetMapping("/") // 가장 기본 페이지 의미, 즉 루트 페이지 위치
//    public String home(){
//        return "newspringhome"; // springhome.html 을 찾아서 연다.
//    }

//    @GetMapping("/") : 쿠키를 활용한 로그인 홈페이지 정의
    // @CookieValue 를 통해서 쿠키값을 가져올 수 있다.
    // required = true 의 경우 쿠키값이 없으면 접속 불가능, false 는 없어도 접근 가능
    public String LoginHome(@CookieValue(name = "memberCode", required = false) Long memberCode, Model model) {

        // 로그인 안했을 시
        if(memberCode == null){
            System.out.println("memberCode : "+memberCode);
            return "newspringhome";
        }

        /* 로그인 시도 시 */
        // loginMember 가 Optional 로 감싸져있기 때문에 .get() 으로 한번 가져와야 안에있는 값을 가져올 수 있음
        Optional<Member> loginMember = memberRepository.findByCode(memberCode);

        // 로그인 할 때 가져오는 Code 값이 null 인 경우
        if(loginMember.get().getCode() == null){
            System.out.println("getCode : "+ loginMember.get().getCode());
            return "newspringhome";

        }else if(loginMember.get().getCode() == 0){
            model.addAttribute("member", loginMember.get());
            System.out.println("관리자 로그인 성공");
            return "newspringhome_admin";

        }else {
            model.addAttribute("member", loginMember.get());
            System.out.println("로그인 성공");
            return "newspringhome_login";
        }
    }

    @GetMapping("/") // 세션을 사용한 로그인
    public String LoginHomeWithSession(HttpServletRequest request, Model model) {

        // 세션 관리자를 통한 회원 정보 확인 : 타입이 Member 임으로 라는 캐스팅 필요
        Member member = (Member) sessionManager.getSession(request);

        // 로그인 안했을 시
        if(member == null){
            System.out.println("memberCode : "+member);
            return "newspringhome";
        }

        /* 로그인 시도 시 */
        System.out.println("회원 코드 확인 : " + member.getCode());
        Cookie cookie = sessionManager.findCookie(request, SessionManager.SESSION_COOKIE_NAME);

        // 각각 로그인을 시도했으나 회원 코드가 없는 경우, 회원코드가 0 인 경우, 회원 코드가 0 이 아닌 경우
        if(member.getCode() == null){
            System.out.println("쿠키 : "+cookie.getValue());
            return "newspringhome";

        }else if(member.getCode() == 0){
            model.addAttribute("member", member);
            System.out.println("관리자 로그인 성공");
            System.out.println("쿠키 : "+cookie.getValue());

            return "newspringhome_admin";

        }else {
            model.addAttribute("member", member);
            System.out.println("일반 회원 로그인 성공");
            System.out.println("쿠키 : "+cookie.getValue());
            return "newspringhome_login";
        }


    }

}

 

- LoginController

package HJproject.Hellospring.Controller;

import HJproject.Hellospring.Session.SessionManager;
import HJproject.Hellospring.domain.member.Member;
import HJproject.Hellospring.domain.login.LoginForm;
import HJproject.Hellospring.service.LoginService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@Controller
@RequiredArgsConstructor // 클래스의 final 필드에 대한 생성자를 자동으로 생성
public class LoginController {

    private final LoginService loginService;
    private final SessionManager sessionManager; // @Component 필요

    @GetMapping("/login")
    public String LoginForm(@ModelAttribute("LoginForm")LoginForm form){
        return "members/login";
    }

    /* @PostMapping("/login") : 세션 없이 쿠키로만 로그인 */
    /* HttpServletResponse : 아래의 쿠키값을 생성 후 클라이언트에게 보낼때 response 에 넣어서 보내야함 */
    public String login(@ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse httpServletResponse) {
        if (bindingResult.hasErrors()) {
            return "members/login";
        }

        // loginForm 을 통해 값들을 가져옴
        Member LoginMember = loginService.login(form.getLoginid(), form.getLoginpw());

        if (LoginMember == null) { // login 메서드에서 던져주는 값이 null 이면 로그인 실패
//            bindingResult.reject("login Fail", "아이디 또는 비밀번호가 맞지 않습니다");
            System.out.println("로그인 실패");
            return "members/login";
        }

        // null 이외의 값 즉 member 객체라면 로그인 성공 처리
        // 쿠키에 시간 정보를 주지 않았기 때문에 세션 쿠키로 인식됨 -> 브라우저 종료시 모두 종료
        Cookie CookieCode = new Cookie("memberCode", String.valueOf(LoginMember.getCode()));
        httpServletResponse.addCookie(CookieCode);
        System.out.println("쿠키 정보 전달 완료 : "+ CookieCode);
        return "redirect:/";
    }

    // @PostMapping("/logout")
    // 쿠키를 삭제하려면 쉽게 그냥 쿠키 생명주기, 시간을 0으로 만들어 버리면 됨
    public String logout(HttpServletResponse httpServletResponse){
        Cookie cookie = new Cookie("memberCode", null);
        cookie.setMaxAge(0);
        httpServletResponse.addCookie(cookie);
        return "redirect:/";
    }

    @PostMapping("/login") // 세션을 활용한 로그인
    public String loginWithSession(@ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse httpServletResponse) {
        if (bindingResult.hasErrors()) {
            return "members/login";
        }

        // loginForm 을 통해 값들을 가져옴
        Member LoginMember = loginService.login(form.getLoginid(), form.getLoginpw());

        if (LoginMember == null) { // login 메서드에서 던져주는 값이 null 이면 로그인 실패
//            bindingResult.reject("login Fail", "아이디 또는 비밀번호가 맞지 않습니다");
            System.out.println("로그인 실패");
            return "members/login";
        }

        // null 이외의 값 즉 member 객체라면 로그인 성공 처리
        // createSession 메서드에 value : LoginMember 과 응답 : httpServletResponse 넘겨주면서 세션을 생성
        sessionManager.createSession(LoginMember, httpServletResponse);
        return "redirect:/";
    }

    @PostMapping("/logout") // 세션을 활용한 로그아웃
    // 세션 종료를 위해서는 sessionManager 에 만들어두었던 expireCookie 를 사용하자
    public String logoutWithSession(HttpServletRequest request){
        sessionManager.expireCookie(request);
        return "redirect:/";
    }

}

2) 세션 적용 확인!!

실제 세션을 적용하면 기존의 쿠키값에 memberCode 가 찍혔던 것에 반해 이번에는 랜덤한 값이 찍히는 것을 확인 할 수 있다.

set-Cookie 에 랜덤한 세션값이 생성되는 것이 보인다.
조금 더 자세히보면 이렇게 보인다.

 


이렇게 세션을 통해 로그인을 시도하고 실제로 로그인 시 랜덤한 세션 토큰이 붙어나오는 것을 확인 할 수 있었다.

이번에는 사실 내가 직접 SessionManager 을 만들어서 세션 관리자 클래스가 세션을 생성하고 세션 저장소에 저장하고 하는 기능을 담당했다면 다음에는 HTTP 서블릿에서 지원해주는 Session 기능을 활용해서 로그인해보도록 하겠다.

댓글