spring

로그인-cookie, session

키위먹고싶다 2022. 2. 4. 15:21

쿠키로 로그인 기능을 구현해보자

먼저 회원가입 기능 부터 만들어야 한다.

 

회원가입 기능

package hello.itemservice.domain.member;

import lombok.Data;

import javax.validation.constraints.NotEmpty;

@Data
public class Member {

    private Long id;

    @NotEmpty
    private String loginId; //로그인 ID
    @NotEmpty
    private String name;
    @NotEmpty
    private String password;
}
package hello.itemservice.domain.member;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.*;

@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){
//        List<Member> all = findAll();
//        for (Member m : all) {
//            if(m.getLoginId().equals(loginId)){
//                return Optional.of(m);
//            }
//        }
//
//        return Optional.empty();
        return findAll().stream().filter(m -> m.getLoginId().equals(loginId)).findFirst();
    }

    public List<Member> findAll(){
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}
package hello.itemservice.web.member;

import hello.itemservice.domain.member.Member;
import hello.itemservice.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@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, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return "members/addMemberForm";
        }

        memberRepository.save(member);
        return "redirect:/";    //회원가입 성공하면 홈 화면으로 리다이렉트
    }
}

 

 

로그인 기능

package hello.itemservice.domain.login;

import hello.itemservice.domain.member.Member;
import hello.itemservice.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class LoginService {

    private final MemberRepository memberRepository;

    /**
     *
     * @return null 로그읜 실패
     */
    public Member login(String loginId, String password) {
//        Optional<Member> findMemberOptional = memberRepository.findByLoginId(loginId);
//        Member member = findMemberOptional.get();
//        if(member.getPassword().equals(password)){
//            return member;
//        }else{
//            return null;
//        }

        return memberRepository.findByLoginId(loginId)
                .filter(m -> m.getPassword().equals(password)).orElse(null);

    }
}
package hello.itemservice.web.login;

import lombok.Data;

import javax.validation.constraints.NotEmpty;

@Data
public class LoginForm {

    @NotEmpty
    private String loginId;

    @NotEmpty
    private String password;
}
package hello.itemservice.web.login;

import hello.itemservice.domain.login.LoginService;
import hello.itemservice.domain.member.Member;
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.HttpServletResponse;
import javax.validation.Valid;

@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, 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:/";
    }

    @PostMapping("/logout")
    public String logout(HttpServletResponse response){
        expireCookie(response, "memberId");
        return "redirect:/";
    }

    private void expireCookie(HttpServletResponse response, String cookieName) {
        Cookie cookie = new Cookie("memberId", cookieName);
        cookie.setMaxAge(0);
        response.addCookie(cookie);
    }
}

로그인 하고 나면 response헤더에 서버가 쿠키 정보를 브라우저에게 제공한다. 그러면 브라우저의 쿠키 저장소에 쿠키가 담기게 되고 다른 요청을 할때마다 쿠키 정보를 request헤더에 넣어서 요청한다. 

 

로그아웃 하기 전까지 저 쿠키 정보가 유지된다. 

 

그런데 여기에는 심각한 보안 문제가 있다.

클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다. 그리고 쿠키에 보관된 정보를 훔쳐갈 수 있다. 만약 회원아이디가 아니라 계좌번호나 다른 개인정보가 있다면 쿠키 정보가 털릴경우 매우 큰일난다. 

 

저기 쿠키 정보에서 value를 바꾸면 다른 사용자가 될 수 있다.

 

 

ssession

서블릿은 세션을 위해 HttpSession이라는 기능을 제공한다. 

@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request){
    if (bindingResult.hasErrors()){
        return "login/loginForm";
    }

    Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

    if(loginMember == null){
        bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
        return "login/loginForm";
    }

    //로그인 성공 처리
    //세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
    HttpSession session = request.getSession();
    //세션에 로그인 회원 정보 보관
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

    return "redirect:/";
}

세션을 생성하는데 이때 생성된 세션값은 UUID값으로 중복되지 않는다. 

그리고 그 세션에 로그인한 member객체를 저장한다.(회원 정보)

 

@PostMapping("/logout")
public String logoutV3(HttpServletRequest request){
    HttpSession session = request.getSession(false);
    if (session != null){
        session.invalidate();
    }

    return "redirect:/";
}

로그아웃할때는 request에서 getSession하는데 이때 기본값인 true로 납두면 세션이 없을 때 세션을 생성하기 때문에 false로 설정한다. false는 세션값이 있으면 세션을 가져오고 없으면 세션을 생성하지 않고 null을 반환하지만 true는 세션값이 있으면 세션을 가져오고 없으면 세션을 새로 생성하기 때문이다. 그리고 세션값이 존재하면 invalidate해서 세션을 삭제 한다.

 

@GetMapping("/")
    public String homeLoginV3(HttpServletRequest request, Model model){

        HttpSession session = request.getSession(false);    //세션을 생성할 의도가 아님. 그래서 false로 함. true로 두면 세션이 없으면 세션을 생성함.

        //로그인
        if (session == null){
            return "home";
        }

        Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);

        //세션에 회원 데이터가 없으면 home
        if (loginMember == null){
            return "home";
        }

        //세션이 유지되면 로그인 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }

로그인이 성공했을때 홈 화면!

 

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

스프링이 제공하는 @SessionAttribute를 사용하면 간단하게 코드를 줄일 수 있다.

 

 

 

세션 정보와 타임아웃 설정

여러 세션 정보들인데 특히 getMaxInactiveInterval=1800은 세션의 유효시간이다. (1800초, 30분)

 

세션은 기본적으로 메모리를 사용하기 때문에 여러 사용자가 로그인 했다면 사용자 수만큼 세션을 만들고 메모리가 사용된다. 사용자가 로그아웃 할 경우 세션을 삭제 해서 메모리를 반환해야 하는데 이때 세션의 종료 시점을 결정해야 한다. 만약 세션의 유효시간을 30분으로 잡고 세션의 생성시점부터 30분후 세션을 종료 시킨다면 사용자가 사이트를 이용하다가 30분이 지나면 다시 로그인을 해서 세션을 생성해야 한다. 그래서 사용자가 최근에 요청한 시간을 기준으로 30분을 유지하는것을 종료시점 기준으로 잡는 것이다. HttpSession은 이 방식을 사용한다. 

 

server.servlet.session.timeout=60

60초로 설정하면 lastAccessedTime시간 기준으로 60초 뒤에 세션이 종료된다. 저건 글로벌 단위이고

개별적으로 session.setMaxInactiveInterval(60)로 설정할 수 있다.