스프링 mvc - 로그인 처리

meluu_·2024년 1월 22일
0

스프링

목록 보기
22/27
post-thumbnail

🌿 시작하기 앞서


스프링 부트 3.2.1 버전을 기준으로 작성됨

//패키지 구조 
* domain
  * item
  * member
  * login
  
* web
  * item
  * member
  * login

domain : 화면, UI, 기술 인프라 등등의 영역을 제외한 시스템이 구현해야 하는 핵심 비지니스 업무 영역

의존관계
web -> O domain
domain -> X web


🔐 로그인 기능


@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);
    }
}

@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());
        log.info("login? {}", loginMember);
        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }
        
        //로그인 성공 처리 TODO
        return "redirect:/";
    }
}

로그인 성공시 home 화면
실패시 로그인 창으로 다시 이동

로그인 유지

쿠키를 사용하여 로그인 상태를 유지한다.
쿠키에 대한 자세한 내용은 HTTP를 참조

쿠키 종류

영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지
세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지

 //로그인 성공 처리
 
 //쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)	
    Cookie idCookie = new Cookie("memberId",
	String.valueOf(loginMember.getId()));
	response.addCookie(idCookie);
    

쿠키 이름은 memberId

✔️ @CookieValue

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {

    private final MemberRepository memberRepository;

    @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";
    }
}
  • name 인 쿠키를 받는다.
  • required : true 시 name 속성을 가진 쿠키가 없으면 예외 발생시킴
  • @Cookievalue는 쿠키를 생성해주진 않는다.

✔️ 로그아웃

    @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);
    }

쿠키 유효시간을 0으로 설정했기에 로그아웃시 쿠키가 바로 삭제된다.


✔️ 쿠키와 보안 문제

클라이언트가 쿠키를 변경할 수 있다.

대안

  • 쿠키에 중요한 값이 아닌 임의의 토큰(랜덤 값) 노출
  • 서버에서 토큰과 사용자 id 매핑하여 인식
  • 서버에서 토큰 관리
  • 만료시간 짧게 유지
  • 해킹 의심시 해당 토큰 강제 제거

☑️ 세션


✔️ 세션 동작 방식

중요한 정보를 보관하고 연결을 유지하는 방법

핵심

  • 회원과 관련된 정보는 클라이언트에게 전혀 전달하지 않는다.
  • 오직 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에 전달

✔️ 세션 만들기


세션관리 3가지 기능

  • 세션 생성
    • sessionId 생성 (임의의 추정 불가능한 랜덤 값)
    • 세션 저장소에 sessionId와 보관할 값 저장
    • sessionId로 응답 쿠키를 생성하여 클라이언트에 전달
  • 세션 조회
    • 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관할 값 조회
  • 세션 만료
    • 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거

// 세션 관리
@Component
public class SessionManager {
    public static final String SESSION_COOKIE_NAME = "mySessionId";
    private Map<String, Object> sessionStore = new ConcurrentHashMap<>();

	// 세션 생성
    public void createSession(Object value, HttpServletResponse response) {
        //세션 id를 생성하고, 값을 세션에 저장
        String sessionId = UUID.randomUUID().toString();
        sessionStore.put(sessionId, value);
        //쿠키 생성
        Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
        response.addCookie(mySessionCookie);
    }
  
	// 세션 조회
    public Object getSession(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie == null) {
            return null;
        }
        return sessionStore.get(sessionCookie.getValue());
    }
    
    //세션 만료
    public void expire(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie != null) {
            sessionStore.remove(sessionCookie.getValue());
        }
    }
    
    private Cookie findCookie(HttpServletRequest request, String cookieName) {
        if (request.getCookies() == null) {
            return null;
        }
        
        return Arrays.stream(request.getCookies())
                .filter(cookie -> cookie.getName().equals(cookieName))
                .findAny()
                .orElse(null);
    }
}

ConcurrentHashMap : 동시 요청에 안전

사용

  • 로그인 성공시 세션관리자를 통해 세션 생성 및 회원 데이터 보관

  • 로그아웃시 세션관리자를 통해 쿠키를 expire로 만료


🌱 서블릿 HTTP 세션


HttpSession은 잘 구현된 기능이다.
SessionManager와 같은 방식으로 동작

쿠키 생성시
쿠키 이름 : JSESSIONID
값 : 추정 불가능한 랜덤 값

public class SessionConst {
	public static final String LOGIN_MEMBER = "loginMember";
}
HttpSesion에 데이터 보관 및 조회시, 
같은 이름이 중복되어 사용되므로, 상수를 하나 정의
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
    
    //...
    
    //로그인 성공 처리
    //세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
    HttpSession session = request.getSession();
    
    //세션에 로그인 회원 정보 보관
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
    return "redirect:/";
}

세션 생성과 조회
public HttpSession getSession(boolean create);

  • true (default)
    • 세션 o : 기존 세션 반환
    • 세션 x : 새로운 생성 및 반환
  • false
    • 세션 o : 기존 세션 반환
    • 세션 x : 생성 x , null 반환

session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

  • request.setAttribute() 와 비슷
  • 하나의 세션에 여러 값 저장 가능

로그아웃

@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
    //세션을 삭제한다.
    HttpSession session = request.getSession(false);
    
    if (session != null) {
        session.invalidate();
    }
    
    return "redirect:/";
}

session.invalidate() : 세션을 제거


✔️ @SessionAttribute


// 세션 생성 x
@GetMapping("/")
public String homeLoginV3Spring(
        @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)
        Member loginMember, Model model) {
    
    //세션에 회원 데이터가 없으면 home
    if (loginMember == null) {
        return "home";
    }
    //세션이 유지되면 로그인으로 이동
    model.addAttribute("member", loginMember);
    return "loginHome";
}
  • 세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 한번에 편리하게 처리

TrackingModes

로그인 처음 시도시 URL에 jsessionid를 포함함

  • 웹 브라우저가 쿠키 미지원시 쿠키 대신 URL을 통해서 세션 유지하는 방법

URL 전달 방식 끄기

application.properties
server servlet.session.tracking-modes=cookie

✔️ 세션 정보 확인


HttpSession session = request.getSession(false);

session.getAttributeNames().asIterator()
	.forEachRemaining(name -> log.info("session name={}, value={}", name, session.getAttribute(name)));
    
session.getId()
session.getMaxInactiveInterval()
new Date(session.getCreationTime())   
new Date(session.getLastAccessedTime())
session.isNew()

 return "세션 출력";
  • sessionId : 세션Id, JSESSIONID 의 값

  • maxInactiveInterval : 세션의 유효 시간, 예) 1800초, (30분)

  • creationTime : 세션 생성일시

  • lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신

  • isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부


세션 타임아웃 설정

로그아웃시 -> session.invalidate() 호출되어 세션 삭제
브라우저 강제 종료시 -> ?

HTTP가 비 연결성이기에 서버 입장에서 해당 상요자가 웹 브라우저를 종료한 것인지 여부 확인 불가
-> 남아 있는 세션을 무한정 보관시 다음과 같은 문제 발생

  • 세션과 관련된 쿠키(JsessionId)를 탈취 당했을 경우 오랜 시간이 지나도 해당 쿠키를 악의적으로 요청할 수 있다.

  • 세션은 기본적으로 메모리에 생성, 메모리의 크기는 무한하지 않기에 꼭 필요한 경우만 생성해서 사용해야함.

대안

스프링 부트로 글로벌 설정

application.properties
server.servlet.session.timeout=60 (defualt = 1800[30])

//특정 세션 단위로 시간 설정
session.setMaxInactiveInterval(1800);


//세션 타임아웃  사용
// 세션 타임 아웃 시간은 해당 세션관 관련된 JSESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화 됨 
// 이렇게 초기화 되면 세션 타임아웃으로 설정한 시간동안 세션을 추가로 사용 가능 

session.getLastAccessedTime() : 최근 세션 접근 시간
LastAccessedTime이후로 timeout 시간이 지나면,
WAS가 내부에서 해당 세션 제거




❗ 주요사항


  • 세션에는 최소한의 데이터만 보관
  • 회원 데이터같이 객체를 담을 시 메모리가 터질 수 있음

로그인 유지

  • 세션(메모리, 데이터베이스), 토큰(예: JWT)을 사용


🔖 학습내용 출처

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

profile
열심히 살자

0개의 댓글