과정을 즐기자

로그인 방식 개선 과정 본문

Spring

로그인 방식 개선 과정

320Hwany 2023. 1. 7. 17:33

로그인 방식 개선 과정 

프로젝트를 진행하면서 저번보다 로그인 방식을 조금 수정하였다.

먼저 어떻게 개선하였는지 먼저 간략하게 소개하자면

 

1.  기존에는 HttpSession을 이용하여 톰캣에 세션을 저장하였다. 이 방식은 톰캣을 종료하면 세션 정보가
다 사라지기 때문에 사용자가 처음부터 다시 로그인을 진행해야 하는 문제점이 있었다.
해결책으로 세션을 DB에 저장할 수도 있고 Redis와 같은 인메모리 캐시에 저장할 수 있다.
이번 프로젝트에서는 세션을 DB에 저장하는 방식으로 바꾸었다.

(이때 Spring Boot의 설정으로 DB에 저장하는 방식도 있지만 이번에는 세션 엔티티를 직접 구현해서 해봤습니다)
또한 HttpSession을 이용하지 않고 ResponseCookie로 세션에 맞는 쿠키를 설정하였다. 

 

2. 기존에는 스프링 인터셉터를 사용하여 url을 설정하여 접근을 제어하였다면

이번에는 ArgumentResolver를 활용하여 필요한 메소드에 직접 넣어줘서 관리할 수 있도록 하였다.

 

기존 로그인 방식

먼저 DB에 저장되어 있는 회원 정보의 일치여부를 확인한다.

만약에 일치한다면 HttpServletRequest, HttpSession을 이용하여 세션을 생성해주었다.

(없다면 새로 생성하고 있으면 기존 값을 반환한다)

HttpSession session = request.getSession();

 

이 과정에서 서버는 세션 쿠키를 생성해준다. JSESSIONID 쿠키의 값을 key로 보관하게 된다. 

세션 저장소에는 각각의 JSESSIONID 값에 대한 고유한 공간을 가지게 되고 이 공간에 key, value값으로 

원하는 객체들을 보관할 수 있다.  다음과 같이 "loginMember" key에 memberLoginDto라는 value가 저장된다. 

session.setAttribute("loginMember", memberLoginDto);

 

이때 여러명의 클라이언트가 같이 "loginMember"로 요청을 하는데 구분을 할 수 있는 이유는 

앞에서 말한 JSESSIONID 값에 따른 고유한 공간이 각각 있고 그 공간 안에 loginMember key값으로 

저장되기 때문이다.  

 

다음 요청시 클라이언트는 쿠키가 아직 존재한다면 요청 헤더에 쿠키를 넣어서 보낸다.  

이때 쿠키에는 보안에 취약하므로 JSESSIONID만 있고 중요한 정보는 JSESSIONID를 이용해  

세션에 저장되어 있다.  JESSIONID에 맞는 세션이 있다면 반환하고 없어도 생성하지 않는다.

HttpSession session = request.getSession(false);
 
다음으로 JSESSIONID에 맞는 세션이 존재한다면 그 세션에서 "loginMember" key 값으로 value를 찾는다.
MemberLoginDto loginMember = (MemberLoginDto) session.getAttribute("loginMember");

 

이렇게 한번 로그인한 회원의 로그인을 유지할 수 있도록 하였다. 

이때 로그인이 필요한 url은 스프링 인터셉터를 이용하여 처리하였다.  

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/api/**", "/signup", "/login", "/logout", "/css/**");
    }
}

 

LoginCheckInterceptor

public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, 
    		HttpServletResponse response, Object handler)
            throws Exception {
        String requestURI = request.getRequestURI();
        HttpSession session = request.getSession(false);
        if (session == null) {
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }
        return true;
    }
}

 

개선한 로그인 방식 

먼저 세션을 DB에 저장하기 위해 엔티티로 만들었다. 연관관계는 Many(세션) to One(회원)으로 설정하였다.

Session

@NoArgsConstructor(access = PROTECTED)
@Getter
@Entity
public class Session {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "session_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    private String accessToken;

    @Builder
    public Session(Member member, CreateAccessToken createAccessToken) {
        this.member = member;
        this.accessToken = createAccessToken.getAccessToken();
    }

    public ResponseCookie setCookie() {
        ResponseCookie cookie = ResponseCookie.from("SESSION", accessToken)
                .domain("localhost")
                .path("/")
                .httpOnly(true)
                .secure(false)
                .maxAge(Duration.ofDays(30))
                .sameSite("Strict")
                .build();
        return cookie;
    }
}

Controller

@PostMapping("/login")
public ResponseEntity<Object> login(@RequestBody @Valid MemberLogin memberLogin) {
    Member member = memberService.getByMemberLogin(memberLogin);
    Session session = sessionService.makeSession(member, createAccessToken);
    ResponseCookie cookie = session.setCookie();
    return ResponseEntity.ok()
            .header(SET_COOKIE, cookie.toString())
            .build();
}

 

클라이언트가 로그인 요청을 하면 DB에 저장되어 있는 회원 정보로 일치여부를 확인한다.  

회원정보가 일치하면 member의 새로운 session을 만든다. 

 

만든 세션을 이용해서 setCookie 메소드로 ResponseCookie를 생성후 반환한다. 이렇게 쿠키를 만들고  

ResponseEntity로 반환을 하면 클라이언트가 다음 요청을 할 때 요청 헤더의 쿠키 부분에 만든 쿠키정보가 있다.

 

쿠키가 존재한다면 모든 요청의 헤더에 쿠키가 있지만 서버쪽에서 언제 쿠키정보를 확인할 지를 

따로 설정해주어야 한다.  이때 스프링 인터셉터가 아닌 ArgumentResolver를 이용하였다.  

WebMvcConfig

@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final SessionRepository sessionRepository;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AuthResolver(sessionRepository));
    }
}

AuthResolver

@RequiredArgsConstructor
public class AuthResolver implements HandlerMethodArgumentResolver {

    private final SessionRepository sessionRepository;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(MemberSession.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
    		ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory)
            throws Exception {
        HttpServletRequest httpServletRequest = getHttpServletRequest(webRequest);
        Cookie[] cookies = getCookies(httpServletRequest);
        Session session = getSession(cookies);

        return MemberSession
                .builder()
                .id(session.getMember().getId())
                .build();
    }

    private static HttpServletRequest getHttpServletRequest(
    				NativeWebRequest webRequest) {
        HttpServletRequest httpServletRequest = 
        webRequest.getNativeRequest(HttpServletRequest.class);
        if (httpServletRequest == null) {
            throw new UnauthorizedException();
        }
        return httpServletRequest;
    }
    
    private static Cookie[] getCookies(HttpServletRequest httpServletRequest) {
        Cookie[] cookies = httpServletRequest.getCookies();
        if (cookies == null) {
            throw new UnauthorizedException();
        }
        return cookies;
    }
    
    private Session getSession(Cookie[] cookies) {
        String accessToken = cookies[0].getValue();
        Session session = sessionRepository.findByAccessToken(accessToken)
                .orElseThrow(UnauthorizedException::new);
        return session;
    }
}

 

결과 확인

마지막으로 postman과 DB를 직접 확인하여 세션이 저장되는지를 확인하자.   

먼저 2명의 회원을 각각 회원가입 시켰다. 이때는 세션이 존재하지 않는다. 

 

회원 1이 로그인을 할 때 요청 헤더의 쿠키를 postman으로 확인해보자 

회원 2가 로그인을 할 때 요청 헤더의 쿠키를 postman으로 확인해보자

이제 DB에서 세션을 확인해보자

쿠키에 있는 Value가 DB의 세션에 저장되어 있는 ACCESS_TOKEN과 일치한다.