로그인 처리

이성준·2022년 3월 16일
0

스프링 MVC

목록 보기
10/10

로그인 - 요구사항

  • 로그인 요구사항
    홈 화면 - 로그인 전
    회원 가입
    로그인
  • 홈 화면 - 로그인 후
    본인 이름(누구님 환영합니다.)
    상품 관리
    로그 아웃
  • 보안 요구사항
    로그인 사용자만 상품에 접근하고, 관리할 수 있음
    로그인 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동
    회원 가입, 상품 관리

패키지 구조 설계

패키지구조 hello.login

  • domain
    item
    member
    login
  • web
    item
    member
    login
    domain 과 web 구조는 따로 설계한다

domain은 web을 알면 안되고 web은 domain을 알아도 된다.
web을 삭제해도 domain에 아무 영향이 없게끔

회원 가입

member

@Data
public class Member {
private Long id;

@NotEmpty
private String loginId;
@NotEmpty
private String name;
@NotEmpty
private String password;

}

회원가입용 멤버 객체

MemberRepository

@Slf4j
@Repository
public class MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

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


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

    public void clearStore() {

    }

}

주석부분을 람다스트림으로 바꾸면 한줄로 바꿀수 있다.

MemberController

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

검증하고 오류 발생시 addMemberForm으로 아니면 save()실행 후 리다이렉트

로그인 기능

LoginService

@Service
@RequiredArgsConstructor
public class LoginService {

private final MemberRepository memberRepository;

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;
//    }
    Optional<Member> byLoginId = memberRepository.findByLoginId(loginId);
 return byLoginId.filter(m -> m.getPassword().equals(password)).orElse(null);
}
}

로그인의 핵심 비즈니스 로직은 회원을 조회한 다음에 파라미터로 넘어온 password와 비교해서 같으면
회원을 반환하고, 만약 password가 다르면 null 을 반환한다
LoginForm

package hello.login.web.login;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@Data
public class LoginForm {
 @NotEmpty
 private String loginId;
 @NotEmpty
 private String password;
}

로그인만을 위한 로그인dto를 만들어준다.
LoginController

@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 )를 생성한다. 그리고 정보를 다시 입력하도록 로그인 폼을 뷰 템플릿으로 사용한다.

  • 문제점
    로그인하면 홈 화면에 고객 이름이 보여야되는데 보이지 않는다. 여기서 로그인의 상태를 유지하는 방법은?

쿠키



LoginController - login()

@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());
 log.info("login = {}", loginMember);
 if (loginMember == null) {
 bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
 return "login/loginForm";
 }
 //로그인 성공 처리
 //쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
 Cookie idCookie = new Cookie("memberId",
String.valueOf(loginMember.getId()));
 response.addCookie(idCookie);
 return "redirect:/";
}

로그인에 성공하면 쿠키를 생성하고 HttpServletResponse 에 담는다. 쿠키 이름은 memberId 이고, 값은 회원의 id 를 담아둔다. 웹 브라우저는 종료 전까지 회원의 id 를 서버에 계속 보내줄 것이다.

HomeController

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {

 private final MemberRepository memberRepository;
// @GetMapping("/")
 public String home() {
 return "home";
 }
 @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";
 }
}

로그인 쿠키가 없으면 기존 home으로
로그인 쿠키가 있으면 loginHome으로 추가로 member데이터도 모델에 담아서 전달

LoginController

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

setMaxAge가 0이면 해당 쿠키는 즉시 종료 된다

  • 문제점
    해당 쿠키 값은 임의로 변경 가능하다
    쿠키에 보관된 정보는 훔쳐갈수 있다.
    해커가 쿠키를 가져가면 평생 사용 가능하다.

  • 대안
    쿠키에는 임의의 토큰만 노출, 그 토큰을 사용자 id와 매핑, 그리고 서버에서 토큰 관리
    토큰은 예상 불가능하게 만든다
    해커가 털어가도 시간이 지나면 사용 못하게 만료시간을 둔다.

    세션




    세션 직접 만들기

  • 세션 생성

  1. sessionid생성
  2. 세션 저장소에 sessionid와 보관할 값 저장
  3. sessionid로 응답 쿠키 생성해서 클라이언트에 전달
  • 세션 조회
  1. 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회
  • 세션 만료
  1. 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거

    SessionManager

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

세션적용

LoginController - loginV2()

@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult
bindingResult, 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:/";
}

LoginController - logoutV2()

@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
 sessionManager.expire(request);
 return "redirect:/";
}

로그아웃시 해당 세션 제거

HomeController

@GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model) {
 //세션 관리자에 저장된 회원 정보 조회
 Member member = (Member)sessionManager.getSession(request);
 if (member == null) {
 return "home";
 }
 //로그인
 model.addAttribute("member", member);
 return "loginHome";
}

로그인 처리하기 - 서블릿 HTTP 세션1

SessionConst

public class SessionConst {
 public static final String LOGIN_MEMBER = "loginMember";
}

HttpSession에 데이터를 보관 조회 할때, 같은 이름을 쓰니까, 상수를 하나 만들어 준다.

loginV3()

HttpSession session = request.getSession();
 //세션에 로그인 회원 정보 보관
 session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

logoutV3()

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

@SessionAttribute

이미 로그인 된 사용자를 찾을때

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

HomeController - homeLoginV3Spring()

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

세션정보 확인

SessionInfoController

@Slf4j
@RestController
public class SessionInfoController {
 @GetMapping("/session-info")
 public String sessionInfo(HttpServletRequest request) {
 HttpSession session = request.getSession(false);
 if (session == null) {
 return "세션이 없습니다.";
 }
 //세션 데이터 출력
 session.getAttributeNames().asIterator()
 .forEachRemaining(name -> log.info("session name={}, value={}",
name, session.getAttribute(name)));
 log.info("sessionId={}", session.getId());
 log.info("maxInactiveInterval={}", session.getMaxInactiveInterval());
 log.info("creationTime={}", new Date(session.getCreationTime()));
 log.info("lastAccessedTime={}", new
Date(session.getLastAccessedTime()));
 log.info("isNew={}", session.isNew());
 return "세션 출력";
 }
}
  • sessionId : 세션Id, JSESSIONID 의 값이다. 예) 34B14F008AA3527C9F8ED620EFD7A4E1
  • maxInactiveInterval : 세션의 유효 시간, 예) 1800초, (30분)
  • creationTime : 세션 생성일시
  • lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.
  • isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부

0개의 댓글