회원 가입할 정보폼을 받는 객체
@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);
}
}
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
으로 보냄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
을 확인할 수 있음, 해당 쿠키는 즉시 종료보안문제
대안
앞서 쿠키에 중요한 정보를 보관하는 방법은 해커에게 털릴 위험이 있다. 이 문제를 해결하려면 정보를 모두 서버에 저장해야한다. 그리고 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결해야한다. 이렇게 서버에서 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라한다.
세션 관리 주요 기능 3가지
세션생성
세션 조회
세션 만료
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:/";
}
@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:/";
}
세션의 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(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