티스토리 뷰
이전 포스팅에서 안드로이드 클라이언트와 로그인 유지 관련한 이슈가 있었다.
[프로젝트] TeamOne - 안드로이드 로그인 유지의 필요성에 대한 고찰
현재 로그인 관련해서 이슈가 있다. 먼저, 백단에서는 다음과 같이 구현되어있다. 사용자가 로그인을 하면 HttpSession을 이용해서 세션값을 부여하고, 세션에 해당 멤버 정보를 저장한다. 사용자
mopil.tistory.com
이를 해결하기 위해 다음 고민을 했었다.
# 1 서버 로그인 검증 포기
- 어차피 안드로이드 화면을 기준으로 로그인을 해야 다음 비즈니스 화면으로 넘어가므로, 서버에선 따로 인증을 하지 않는 방식이다. (즉, 인증을 프론트에 전담한다.)
장점 : 인증 관련한 고민을 백단에서 할 필요가 없어진다.
단점 : 당연히 보안상 취약하고, 로그인의 의미가 사라진다.
# 2 커스텀 인증 방식 도입
- 문제가 되는 부분은 스프링 서버에서 안드로이드 request에서 session을 잡지 못 한다는 점이였다. 그래서 이를 포기하고 커스텀 SessionManager를 만들어서 조금 귀찮더라도 인증을 직접 구현하는 방식이다.
장점 : 로그인 인증의 의미를 살릴 수 있다.
단점 : 중복된 코드가 조금 증가한다.
# Spring Security (JWT) 도입
당장 급한불 부터 끄느라고 제일 마지막에 생각한건데, 결국 언젠가는 마이그레이션 해야 할 거 같다.
결국, 우리는 2번 방식으로 변경하기로 결정했다.
기본적인 매커니즘
- 클라이언트에서 최초 로그인을 시도한다.
- 서버는 HttpSession을 통해서 임의의 랜덤값(세션값)을 만들고 커스텀 SessionManager에 저장한다. (HttpSession은 랜덤값을 생성하는 용도로만 사용)
- 해당 세션값을 멤버 객체와 같이 클라이언트 응답으로 내려준다.
- 안드로이드에선 해당 세션값을 쿠키매니져에 저장하고, 인터셉터를 통해 매 Request마다 헤더에 쿠키를 추가해서 요청한다.
- 스프링 컨트롤러에서 Request에 쿠키가 있는지 판단하여 인증을 검사한다.
커스텀 SessionManager
@Component
@Slf4j
public class SessionManager {
public static final String SESSION_ID = "cookie";
public static final String PREFIX = "JSESSIONID=";
// concurrentHashMap 으로 해야 동시 접속 처리 가능(멀티쓰레드)
private final Map<String, Member> sessionStore = new ConcurrentHashMap<>();
// 세션 값 저장
public void save(String sessionId, Member loginMember) {
sessionStore.put(sessionId, loginMember);
}
// 세션 조회
public Member getLoginMember(String sessionId) throws LoginException {
log.info("세션저장소 = {}", sessionStore);
if (!sessionStore.containsKey(sessionId)) throw new LoginException("로그인 되어 있지 않음.");
return sessionStore.get(sessionId);
}
// 세션 삭제
public void expire(String sessionId) {
sessionStore.remove(sessionId);
}
}
세션 자동 만료와 갱신의 문제가 아직 존재한다.
// 비밀번호 수정
@PutMapping("/password")
public ResponseEntity<?> changePassword(HttpServletRequest request,
@Valid @RequestBody ChangePasswordForm form,
BindingResult bindingResult) throws LoginException {
if (bindingResult.hasErrors()) {
log.info("Errors = {}", bindingResult.getFieldErrors());
return badRequest(convertJson(bindingResult.getFieldErrors()));
}
Member loginMember = loginService.getLoginMember(request);
log.info("현재 로그인된 사용자 = {}", loginMember);
log.info("변경하고자 하는 비밀번호 = {}", form.getNewPassword());
Member updatedMember = memberService.changePassword(loginMember, form);
log.info("변경된 사용자 정보 = {}", updatedMember);
return success(updatedMember.toResponse());
}
새로운 인증 방식을 적용한 컨트롤러 예시다.
이렇게 하니 안드로이드와 인증 관련한 통신이 모두 성공적으로 마치는 것을 확인했다.
그러나...
예상치 못한 사이드이펙트
HttpSession을 통해서 임시값을 생성하다 보니, 포스트맨으로 테스트하면 항상 JSESSIONID= 가 세션값 앞에 붙어서 나가는걸 식별했다.
랜덤값 생성을 바꿔도 되지만, 일단은 모든 세션값을 저장할때 앞에 prefix로 JSESSIONID=가 붙도록 설정하니, 포스트맨도 잘 되는 것을 확인했다.
# TIL
[인증 직접 구현의 어려움]
인증을 직접 구현하니 허점도 많고 어렵고, 무엇보다도 예상치도 못한 부분(포스트맨은 되는데, 안드로이드는 안 되는)에서 막히니 괜히 사람들이 써드파티 OAuth나, Spring Security를 사용하는게 아닌걸 다시금 느꼈다.. (물론 공부는 매우매우 많이 되었다...)
[안드로이드 애뮬레이터 문제인가?]
세션을 안드로이드 레트로핏에서 요청을 보낼때 HTTP메시지 어디에 저장하는지는 아직도 의문이다. 보통 헤더에 넣지 않는가? 그런데 왜 스프링은 session을 null로 잡는지 모르겠다... 애뮬레이터 문제인게 뭔가 제일 의심가긴하나, 실제 디바이스로 확인하는데는 제한이 있기 때문에 훗날 풀릴 미스테리로 남겨놓았다....