쿠키/세션으로 로그인 처리

이동건 (불꽃냥펀치)·2025년 1월 25일
0

회원가입

회원 가입할 정보폼을 받는 객체

 @Data
 public class Member {
     private Long id;
    @NotEmpty
    private String loginId; //로그인 ID 
    @NotEmpty
    private String name; //사용자 이름 
    @NotEmpty
    private String password;
}

회원정보를 저장할 별도의 저장소

@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());
		}
     public void clearStore() {
         store.clear();
		} 
}

오류 발생시 다시 회원 정보 입력폼으로 이동

@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(@Valid @ModelAttribute Member member, BindingResult result) {
         if (result.hasErrors()) {
             return "members/addMemberForm";
		}
         memberRepository.save(member);
         return "redirect:/";
     }
}

로그인 시도 후 정보가 일치하지 않으면 null 반환하는 서비스

@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);
	}
 }
  • 로그인의 핵심 비즈니스 로직은 회원을 조회한 다음 파라미터로 넘어온 password와 비교해서 같으면 회원을 반환하고 만약 password가 다르면 null을 반환한다

로그인 입력 객체

 @Data
 public class LoginForm {
     @NotEmpty
     private String loginId;
     @NotEmpty
     private String password;
}

로그인 처리 컨트롤러

@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:/";
	} 
}
  • 로그인 컨트롤러는 로그인 서비스를 호출해서 로그인에 성공하면 홈 화면으로 이동하고, 로그인에 실패하면 bindingResult,reject()를 사용해서 글로벌 오류(ObjectError)를 생성한다. 그리고 정보를 다시 입력하도록 로그인 폼을 뷰 템플릿으로 사용한다.

로그인 처리 - 쿠키 사용

쿠키의 종류

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

기존 "/login"-post 을 처리하던 컨트롤러에 쿠키 생성 코드 추가

@PostMapping("/login")
 public String login(@Valid @ModelAttribute LoginForm form, BindingResult
 bindingResult, HttpServletResponse response) {
 .
 .
 .
 Cookie idCookie = new Cookie("memberId",String.valueOf(loginMember.getId()));
     response.addCookie(idCookie);
     return "redirect:/";
 }
  • 로그인에 성공하면 쿠키를 생성하고 HttpServletResponse에 담는다. 쿠키이름은 memberId이고 값은 회원의 id를 담아둔다. 웹 브라우저는 종료전까지 회원의 id를 서버에 계속 보내줄 것이다.
@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";
    }
}
  • @CokkieValue(required=false)를 사용하면 편리하게 쿠키를 조회할 수 있다
  • 로그인하지 않은 사용자도 홈에 접근할 수 있기 때문에 required=falase를 사용한다
  • 로그인한 사용자가 null이 아니라면 모델에 담아 loginHome으로 보냄



로그아웃

  • 세션 쿠키이므로 웹 브라우저 종료시
  • 서버에서 해당 쿠키의 종료 날짜를 0으로 지정

logout 기능 추가

 @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);
}
  • 로그아웃도 응답 쿠키를 생성하는데 Max-Age=0을 확인할 수 있음, 해당 쿠키는 즉시 종료

쿠키& 보안문제

보안문제

  • 쿠키값은 임의로 변경할 수 있음
    • 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 됨
  • 쿠키에 보관된 정보는 훔칠 수 있음
  • 해커가 쿠키를 한번 훔쳐가면 평생 사용가능

대안

  • 쿠키에 중요한 값을 노출하지 않고 사용자 별로 예측 불가능한 임의의 토큰을 노출하고 서버에서 토큰과 사용자 id를 매핑해서 인식한다. 그리고 서버에서 토큰을 관리
  • 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야한다
  • 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다

로그인 처리 - 세션

앞서 쿠키에 중요한 정보를 보관하는 방법은 해커에게 털릴 위험이 있다. 이 문제를 해결하려면 정보를 모두 서버에 저장해야한다. 그리고 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결해야한다. 이렇게 서버에서 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라한다.

세션 관리 주요 기능 3가지

  • 세션생성

    • sessionId 생성
    • 세션저장소에 sessionId와 보관할 값 저장
    • sessionId로 응답 쿠키를 생성해서 클라이언트로 전달
  • 세션 조회

    • 클라이언트가 요총한 sessionId 쿠키의 값으로 세션 저장소에 보관한 값 조회
  • 세션 만료

    • 클라이언트가 요청한 sessionid 쿠키의 값으로 세션 저장소에 보관한 sessionId와 값 제거

session manager

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

직접 만든 세션 적용

@PostMapping("/login")
 public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult nbindingResult,
 HttpServletResponse response) {
     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";
	}
//로그인 성공 처리
//세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관 
	 sessionManager.createSession(loginMember, response);
     return "redirect:/";
 }
  @PostMapping("/logout")
 public String logoutV2(HttpServletRequest request) {
     sessionManager.expire(request);
     return "redirect:/";
 }
  • 로그인 하는 과정에서 오류가 발생하지 않으면 세션 생성
  • 로그아웃 요청시 세션 삭제

HTTP Session

 @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:/";
 }	
  • 이전에 만들었던 session 대신 HTTP 객체가 제공하는 session을 만듦

세션의 create 옵션에 대해 알아보자

  • request.getSession(true)
    • 세션이 있으면 기존 세션을 반환한다
    • 세션이 없으면 새로운 세션을 생성해서 반환한다
  • request.getSession(false)
    • 세션이 있으면 기존 세션을 반환
    • 세션이 없으면 null
  • request.getSession() : 신규 세션을 생성하는 request.getSession(true) 와 동일

세션에 로그인 회원 정보 보관
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

@SessionAttribute(name = "loginMember", required = false) Member loginMember

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








출처:https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

profile
자바를 사랑합니다

0개의 댓글

관련 채용 정보