-> login.html / register.html 변경
- 오류가 많이 떠서 이 방법으로 수정.
-> header.html
- 이름이 뜨지 않는다..
- UserEntity가 getName을 했을 때 null을 돌려줄까?
UserEntity가 어디서 부터 객체화가 되었는지에 대해 알아야한다.- DB에서 받아온 UserEntity 객체가 가진 값을 LoginVo에 다 넣어줘야 정상작동할 것이다.
-> UserService
- 필드들을 초기화를 해준다.
-> UserController getLogout
메서드 생성
- 로그아웃을 누르게 되면 root directory로 redirect 시켜주는 맵핑을 한다.
SessionAttributes
라는 어노테이션으로 세션 저장소에 들어갓던 객체들을 너네는 여기까지다 라는 선언을 해줘야한다.
SessionAttributes
에 의해 세션 저장소에 들어갔던 객체들이 다 죽게된다. 그렇게 되면 로그아웃이 되는 것이다.
sessionStatus
스프링 프레임 워크 패키지에 있는 객체를 받아와서setComplete();
메서드를 호출하면SessionAttributes
에 의해 세션 저장소에 저장된 객체들이 싹 날아가게 된다.
그렇다면 다른 브라우저에서 로그인하면 현재 브라우저에서 로그아웃되게 하자.
유저1이라는 '이메일, 비밀번호'로 로그인을 했다면 우리는
key1
을 만들어서 DB에 넣고 유저1에게key1
을 돌려준다.
유저1은 자신의 컴퓨터로 들어올 때key1
을 가지고 소통하게 된다.key1
를 가진사람이 사이트에 들어왔을 때 DB에 저장된Key1
랑 비교해서 올바르면 로그인 된 사용자다 라고 해준다.
그런데 유저2가 유저1의 '이메일과 비밀번호'를 가지고 로그인한다고 하면key1
에 대한 내용은 다 날리고key2
를 새로 만들어서 유저2에게 돌려준다.
key1
이 삭제되었기때문에 유저1이라는 사람의 컴퓨터에서 로그아웃된 것처럼 보이게 된다.
-> UserService sessionKey
생성
- sessionKey라는 문자열 자체는 절대 추측할 수 없는 내용이 되고 hasing을 해주며 마무리 한다.
-> 사용자의 세션정보를 담기위한 테이블 sessions 생성
created_at : 언제로그인 했는가 updated_at : 요청을 보낸 마지막 시간 (이 세션이 언제 마지막으로 활동을 했는가, 페이지를 옮겼다와 같은 것들) expires_at : 이 세션이 언제 만료되는가 (로그인 후 1시간뒤 로그아웃 시킴) expired_flag : 만료되기 전에 로그아웃을 눌렀다거나 다른사람이 로그인을 했을때 더 이상 유효하지 않게 하기 위해서 사용. 만료가 됬는지에 대한 여부.1이면 True, 0이면 False user_email key ua
-> SessionEntity sessions
expiresAt
의 역할 : 공공기관이나 은행에서 일정시간 뒤에 로그아웃되는 것.- 세션이 만들어진 그 시간에서 1시간 더한 시간으로
expiresAt
을 지정해주고 update가 일어날 때 즉, 어떤 종류든 상관없이request
가 들어오면updatedAt
을NOW()
로 다시 지정하고expiresAt
은NOW() + 1시
간으로 연장시켜준다.
-> UserService
-> IUserMapper insertSession
추가
sessionEntity
를 session테이블에 insert하자.
-> UserMapper.xml
1. 누가 로그인 요청함
2. 동일한 이메일을 가진 모든 세션 만료시키기
3. 세션키 만들어서 Insert
-> UserService
2. 동일한 이메일을 가진 모든 세션 만료시키기
세션 만료처리를 하기 전에 세션이 잘 들어가는지 확인을 먼저해보자.
-> UserController HttpServletRequest
- login메소드 호출하면
LoginVo
만 주는 것이 아닌HttpServletRequest
도 준다.
-> UserService에 User-Agent
로 수정
-> session이 잘 들어가는지 확인을 해보자. 로그인 시도
- 정상적으로 로그인을 했다면 Sessions테이블에 레코드가 추가가 되는 것을 확인 할 수 있다.
현재 로그인한 사람의 key를 만들어서 DB에 집어넣고 그 사람에게 돌려줘야하는데 돌려주지는 않았다. 그냥 주는 것이 아닌 cookie(쿠키)와 함께 보내줄 것이다.
예로 들어 모든 request와 response에 우리가 명시하지 않아도 header가 항상 같이 따라다닌다.
그것과 비슷한 개념으로 cookie(쿠키) 라는 것이 있다. tomcat이 클라이언트를 구분짓기위해 사용하는게 JSessionId 인 것처럼 이 key를 cookie에 실어보낼 것이다. 한번 실어보내면 요청할때마다 그 쿠기가 들어갔다가 나갔다가 항상 따라다닐 것이다.
-> LoginVo sessionEntity
+ Getter,Setter
-> UserService
결과가 SUCCESS일 때
LoginVo의 SessionEntity가 가지고 있는 key를 cookie에 집어넣자.
-> UserController
- 앞으로 요청을 하고 응답을 할 때 쿠키를 추가해 줄테니 요청을 보낼 때 항상 쿠키를 함께 보내세요. 라고 클라이언트에게 알려줘야해서
reponse
에서 처리를 한다.- SUCCESS일 때
reponse
객체에addCookie
해서 추가해주는데addCookie
메서드가 받는 건 문자열이 아니라 쿠키 객체를 받아온다. 그래서 쿠키객체를 생성해준다. (sk라는 이름으로)
배민이나 요기요나 이런 서비스는 레거시 세션을 사용하지 않는다.
즉, JSessionId 과 SessionAttributes를 사용하지 않는다는 것이다.
레거시 세션 : 스프링이나 톰캣이 기본적으로 제공하는 세션.
여기서 만든 것 처럼 redis랑 통합해서 세션시스템을 구축해서 사용한다.(userEntity를 때려넣는 것이 아니다.)
sessionKeyCookie.setPath("/")'
를 하지않으면 기본 path 즉 현재 주소(/user/**);
로 들어가게 된다. /(슬래시)로 하게 되면 우리 도메인의 모든 주소에서 사용할 수 있게 된다.
-> 로그인을 해서 쿠키값을 알아보자.
- sk라는 이름이 우리가 cookie를 만들 때 지정했던 그 이름이 된다.
밑의 값은 테이블에 있는 그 session key값이랑 동일한 값이 들어가 있다.- 이 쿠키가 만들어지고 그 자리에 가만히 있는 것이 아니라 우리 서버로 그 어떤 요청이 들어오더라도 그 쿠키가 같이 들어온다.
여기까지 왔다는 것은 로그인 결과가 SUCCESS라는 것이다.
얘가 가지고 있는 이메일 기준으로 sessions 테이블에 그 이메일과 동일한 모든 세션키의 expried_flag를 TRUE로 만들자.
-> IUserMapper updateSessionExpiredByEmail
- 이메일로 사용자 세션을 만료되게끔 업데이트하는 메서드 생성.
-> UserMapper.xml updateSessionExpiredByEmail
- 싹 다 만료되어야 되기 때문에 LIMIT는 두면 안된다.
- 만료일시가 지금보다 미래며, 남아있고 만료가 되었는지에 대한 여부가 False
즉, 0인것에 한해서 업데이트를 한다.
-> UserService
- 이렇게 되면 로그인 시 다른 브라우저에 로그인했던 것들이 만료가 된다.
최근 로그인 한사람만 1이고 나머지는 0이된다.
(expired_flag : 1 = True)
아직까지는 로그아웃에 대한 처리는 해주지 않았기 때문에 로그아웃은 되지않는다. DB에서만 확인이 가능하다.
이런기록은 DELETE 하는 것이 아니라 남아있어야한다.
가끔 다른나라에서 이메일 해킹 시도를 했는가에 대해서 로그인 기록을 확인 할 때가 있다.
이런 것들을 위해서라도 기록을 남겨두어야 한다.
프로젝트의 세션의존성을 없애도록 하자.
Controller / SessionAttributes
삭제
SessionStatus sessionStatus
sessionStatus.setComplete();
UserController getLogout메서드에서 삭제
현재 레거시 세션이 없기 때문에 로그인이 안된다.
=> 클라이언트가 우리에게 던져준 sk를 이용해서 userEntity를 받아와야 한다.
Interceptor 생성!
-> WebMvcConfig
addInterceptors
,configurePathMatch
메서드 오버라이드!
/user/login
/ /user/login/
이 두개의 경로 모두 로그인 페이지로 들어가지긴 하지만 상대경로로 보면 완전히 다른 이야기가 된다.< 상대경로 >
/user/login : login이란 디렉터리가 있는 것.
/user/login/ : login이란 파일이 있는 것.
이것을 작동하게 안하게끔 하려면 configurePathMatch
에 들어와서 조취를 취해줘야한다.
configurer.setUseTrailingSlashMatch(false);
이렇게 주게 되면 /user/login
는 작동을 하고 /user/login/
는 작동을 하지 않게 된다.상대경로 설정하기가 상당히 힘들기 때문에 새로운 프로젝트를 시작한다면 이것을 먼저 설정해놓고 하는게 좋다.
-> interceptors 패키지 > SessionInterceptor 생성
1. 쿠기에서 sk 값 가져오기
2. <1>의 sk를 이용하여 Db에서 SessionEntity 긁어오기. 단, 완료되지 않은 것만
3. <2>에서 돌아온 SessionEntitu 가 Null이거나 userEmail이 없거나 하면 로그인 안한거임
4. <2>에서 돌아온 SessionEntity의 userEmail이 있고 이 email로 UserEntity 를 긁어왔더니 정상이다. = 로그인한 상태
5. <4>에서 긁어온 UserEntity를 request 객체에 Attribute로 추가하면 로그인 처리가 될 것이다.
- Interceptor에서 사용하는 request객체랑 UserController에서 사용하는 request는 같은 객체이다. 주소값이 같다!
Interceptor의 request을 통해서 getAttribute를 하면 Controller에서도 getAttribute을 하게 되면 정상적으로 가져와지게 된다.
SessionInterceptor
라는 클래스가 interceptor가 되기 위해서는
HandlerInterceptor
라는 인터페이스를 구현해야한다.Controller
로 들어가기 전에 조취상황을 취해주기 위한 메서드는preHandle
이다. 여기서false
라는 값을 반환하면 Controller로 들어가지 않는다. 그런데 여기서return false
할 일은 없고 세션이 있는 것처럼 작동을 하게끔 조취를 취해주는 일을 할 것이다.
1. 쿠기에서 sk 값 가져오기
-> interceptors 패키지 > SessionInterceptor
- 우리가 '쿠키를 추가할테니 앞으로 요청을 보낼 때 쿠기를 함께 보내주렴' 이라고 알려줄때는 response을 사용했다. 요청이 들어왔을 때 sk 쿠키가 있는지 확인을 하려면
request
에서 해야한다.request.getCookies()
을 하는데 쿠키객체의 배열을 돌려주기 때문에 for문을 사용한다.- 만약 쿠키가 없는 상태에 요청이 들어오게 되면
getCookies()
는 길이가 0인 배열이 돌아오는 것이 아닌 null이 돌아오게 되서nullPointException
이 발생하게 된다. 그래서request.getCookies() != null
검사를 먼저해준다.- 원래여기서 null검사를 하고 sk값에 대한 정규화를 해야한다. getValue한게 null이 아니고 정규식에 맞는지 확인을 해야하는데 넘어가겠다.
sessionKeyCookie = Arrays.stream(request.getCookies()) .filter(x => x.getName().equals("sk")) .findFirst() .orElse(null); Optional<T>
- for문적은 부분을 람다식을 이용해 이렇게도 작성이 가능하다.
.findFirst()
는 쿠키타입의 객체를 돌려주는 것이 아닌Optional<Cookie>
를 돌려준다.
Optional : 제네릭이며whiles
? 라는 메서드를 가지고 있다.
쿠키타입의 객체를 가지고 있으면 쿠키를 돌려주고 없으면 null을 돌려준다.
지금까지 쿠키배열이 null이 아닌 상황에서 cookie를 싹다 돌려서
cookie이름이 sk인 것을 도출해서 sessionKeyCookie로 가져오는 로직을 작성했다.
2. <1>의 sk를 이용하여 Db에서 SessionEntity 긁어오기. 단, 완료되지 않은 것만
-> UserService getSession
- SessionEntity타입을 돌려주는 getSession 생성
-> IUserMapper selectSessionByKey
-> UserMapper.xml selectSessionByKey
- 유효한 세션만 가지고 오겠다는 조건을 건다.
`expires_at` > NOW() `expired_flag` = FALSE
-> SessionInterceptor
- UserService에서
getSession
메서드를 호출해서 인터셉터의sessionEntity
를 가져와야한다. 인터셉터는 생성자를 통한 멤버변수의 초기화가 되지 않아서 아래처럼 명시해준다.@Autowired private UserService userService;
-> UserService
-> WebMvcConfig
sessionInterceptor
추가 및@Bean
어노테이션 등록
addInterceptor
메서드에 interceptor추가
.excludePathPatterns("/resources/**");
예외처리
메서드 체인
:
addInterceptor
메서드가 반환하는 타입이InterceptorResistration
이다. 그 외에 똑같기 때문에 ' . ' 찍으면서 메서드 호출할 수 있다. 실질적으로 계속 retrun this하는 것이다.
메서드 체인에 입각해서 객체를 짜게되면 개발하기가 많이 수월해진다.
-> SessionInterceptor
- 만약 sk라는 이름을 가진 cookie의 값을 가져와서 DB에서 긁어와봤더니 sessionEntity가 null이 아니고 sessionEntity가 가진 UserEmail이 null이 아니라면 UserEmail을 가지고 sessionEntity을 긁어올 수 있다.
-> UserService getUser
- 서비스에서는 getUserByEmail이 아닌 getUser이 된 이유는 오버로딩을 하자는 취지에서 getUser 사용.
-> IUserMapper selectUserByEmail
-> UserMapper.xml selectUserByEmail
-> UserService
-> SessionInterceptor
if(userEntity != null && userEntity.getEmail() != null)
이면 로그인이 되어있는 상태다. 이 메서드가 가진request.setAttribute
에 값을 넣으면 된다.
if (request.getAttribute("userEntity") == null && sessionKeyCookie != null) { sessionKeyCookie.setMaxAge(0); response.addCookie(sessionKeyCookie); }
- 클라이언트가 sk를 가지고 있어도 되지만 sk라는 쿠키를 가지고 있으면
DB를 확인해봐야한다. 서버의 성능상의 이점을 가지고 가지 위해서request.getAttribute("userEntity") == null
이라면 sk라는 쿠키를 지워주도록 하자.- cookie는 life span(수명) 이라는게 있다. 수명을 0으로 하면 죽게된다.
sessionKeyCookie != null
조건을 추가한다.- cookie는 remove나 delete같은게 없어서 setMaxAge를 0으로 함으로써 삭제할수있다.
여기서 로그아웃 로그인을 번갈아 가면서 해서 하나만 살아있으면 성공이다.
- 위 크롬 / 아래 사파리
- 로그아웃 당하면 expired_flag는 1이 된다.
- interceptor만 작성했는데 왜 잘 작동이 되는가? request에 들어가있는 객체도 이름만 쓰면 잘가져와진다.
예전에 html파일로 request에 setAttribute해서 넘겼다.
interceptor에서 addAttribute를 하면 controller가 알게되고 얘가 알게되면 당연히 thymelef에 있는 html도 Attribute에 접근할 수 있는 것이다.
interceptor -> controller -> html
request라는 객체가 이 세곳에서 다 같은 객체임으로 동일하게 작동한다.
attribute를 한번 지정해놓으면 다 따라다닌다.
즉, 연결되어있는 메서드라고 생각하면 쉽다.
서버를 껐다가 켜도 DB가 날아가는 건 아니기 때문에 로그인 상태를 유지하게 된다. 쿠키 또한 유지되어있다.
- 이제는 이거 두 개에 대한 내용을 UPDATE 해줘야한다.
해주지 않으면 로그인을 한 순간부터 1시간 시한부이다. 1시간 후에 로그아웃이 되버린다.- interceptor에서 userEntity가 null이 아님을 확인 할 때마다 UPDATE해줘야한다.
updated_at : 현재시간 / expires_at : 60분 뒤로
-> UserService expendSession
- 여기서 머리를 더 쓰면 회원별로 어디서 무엇을 하고 있는지 DB에 다 들어갈 수 잇있다. 어느 페이지에서 더 오래머물고 어느 페이지에서 금방 나가는지 통계를 내서 수익화를 한다. 하지는 않겠음.
-> SessionInterceptor
this.userService.extendSession(sessionEntity);
호출만 해준다.
-> IUserMapper updateSession
-> UserMapper.xml
updated_at : 언제 마지막으로 활동했는가.
-> UserService
this.userMapper.updateSession(sessionEntity);
추가- 로그인한 시간에서 새로고침한 시간으로 바뀌었고 = updated_at
업데이트한 것에 대한 한시간뒤로 바뀐다 = expires_at
이렇게 하면 데이터베이스에 부담을 주기 때문에 이러한 유저관련한 것들을 보통 radis로 처리한다.
포폴에서 중복로그인을 막는 기능을 구현을 한다면 DB로 해도된다.
레거시 세션에 의존하지 않고 만든 것만으로도 대단한 것이다.
유저컨트롤러에서 로그아웃 부분
1. 어디선가 UserEntity 타입의 객체를 받아와서 이가 가진 email 이 null이 아니라면 sessions 테이블에서 이 이메일을 가진 유효한 세션 만료시키기
2. sk라는 이름을 가지는 쿠키 삭제