1. 계기

로그인 api만 개발했을 때는 로그인 성공 후 응답에 bearer token을 발급하는 것에서 그쳤지만, 화면까지 같이 개발하면서 로그인 유저와 로그아웃 유저를 식별해야하기에 발급된 토큰을 화면에서 다루는 법을 학습하기 위해 진행했다. 

 

Spring Security로 로그인을 구현했던터라 세션 방식의 로그인을 구현할 수 있었지만, 실무에서는 세션 로그인보다는 api/토큰으로 프론트와 통신하는 작업이 더 많을 것이라 생각해 실무를 위해서 조금 어렵더라도 Spring Security, JWT 토큰, Cookie로 로그인을 구현했다.

 

2. Spring Security로 JWT 토큰 발급 및 검증 구현

이 과정은 설명이 꽤 길어서 별도의 포스트에 기록했다. 아래 링크를 참고하면 된다.

로그인 : https://heyazoo1007.tistory.com/811

로그아웃 : https://heyazoo1007.tistory.com/812

 

3. 로그인 구현

AuthController, signIn()

@Controller
@RequestMapping("/v1/auth")
@RequiredArgsConstructor
public class AuthController {
    private static final int LOGIN_DURATION = 60 *  60 * 10;
    private final AuthService authService;

    ...
    
    @PostMapping("/sign-in")
    @ResponseBody
    public ApiResponse<LinkResponseDto> signIn(
            @Valid @RequestBody LoginRequestDto request, HttpServletResponse response
    ) {
        TokenResponseDto tokenResponseDto = authService.signIn(request.getEmail(), request.getPassword());

        Cookie cookie = new Cookie("access_token", tokenResponseDto.getAccessToken());
        cookie.setMaxAge(LOGIN_DURATION);
        cookie.setPath("/");

        response.addCookie(cookie);

        return ApiResponse.SUCCESS;
    }
}

 

AuthService, signIn()

@Service
@RequiredArgsConstructor
public class AuthService {    
    
    public TokenResponseDto signIn(String email, String password) {
        // 해당 사용자가 존재하지 않는 경우
        Member member = notExistedMember(email);

        // 이메일에 비밀번호가 매치되지 않는 경우
        // 비밀번호가 틀렸는데 아이디 혹은 비밀번호로 출력하는 이유는 혹시 모를 개인 정보 유출 때문
        emailOrPasswordMismatch(password, member.getPassword());

        List<String> roles = new ArrayList<>();
        roles.add(member.getRole().toString());

        return jwtTokenProvider.createToken(email, roles);
    }
}

 

TokenResponseDto

@Builder
@Data
@AllArgsConstructor
public class TokenResponseDto {

    private String accessToken;
}

 

4. 토큰으로 로그인한 유저 확인

위 2, 3의 단계를 설정 완료한 뒤 브라우저에서 로그인을 성공하면 다음과 같이 HTTP 메시지 헤더 Cookie에 JWT access token 값이 들어있는 것을 확인할 수 있다.

 

 

클라이언트에서 보낸 HTTP 메시지 헤더의 Cookie값을 서버에서 유효성 검증을 하기 위해서는 JwtTokenProvider 클래스resolveToken()가 필요한데, 코드는 다음과 같다.

    public String resolveToken(HttpServletRequest request) {
        String cookie = request.getHeader("Cookie");
        if (cookie != null) {
            String bearerToken = cookie.split("access_token=")[1];
            if (StringUtils.hasText(bearerToken)) {
                return bearerToken;
            }
        }
        return null;
    }

 

위의 resolveToken()를 JwtAuthenticationFilter 클래스에서 사용하면 클라이언트에서 넘어온 Cookie에 있는 JWT토큰의 유효성을 검증할 수 있다. 

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain
    ) throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);


        // 2. validateToken 으로 토큰 유효성 검사
        ... (코드 생략)
    }
}

 

5. Cookie로 로그인 유저 식별하기

이 방법은 브라우저에서 Cookie의 값을 가져온 뒤, 서버에서 발급한 access token이 있다면 로그인을 한 것으로 보고 access token이 없다면 로그아웃된 상태로 보는 방법이다. 코드는 다음과 같다.

 

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="layout/default_layout">
<th:block layout:fragment="content">
    <script>
        $(document).ready(function() {
            initIndex();
        });
    </script>

    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container-fluid">
            <h1 class="navbar-brand">AccountBook</h1>
            <div>
                <!--로그인 전-->
                
                ...(코드 생략)
                
                <!--로그인 후-->
                
                ...(코드 생략)
                
            </div>
        </div>
    </nav>

</th:block>
</html>

 

initIndex()

function initIndex() {
    // httpOnly 로 설정된 Cookie 는 document.cookie 로 읽을 수 없음
    const accessToken = document.cookie.split('access_token=')[1];
	
    ... (코드 생략)
    
    if(accessToken != null) { // 쿠키에 access_token 이 있을 때(로그인 했을 때)
        ... (코드 생략)   
    } else { // 쿠키에 access_token 이 없을 때
        ...(코드 생략)
    }
}

 

사실 위 코드는 cookie에 대한 보안이 없어서 토큰을 브라우저에서 바로 확인할 수 있고, 그렇기에 외부에서 토큰을 탈취하기가 쉬워 보안적으로 좋은 방법은 아니다. 쿠키의 보안을 위해 httpOnly Cookie를 사용할 수도 있지만, 프론트 프레임워크를 사용하지 않는 지금으로썬 브라우저에서 토큰을 확인할 방법이 없기 때문에(httpOnly cookie를 사용하면 document.cookie로 쿠키 값 확인이 안된다) 보안적으로 취약하지만 이 방법이 현재의 최선책이라고 생각해 진행했다. 추후에 관련된 지식이 추가되면 방법을 바꿀 예정이다.

 

 

참고

Security+타임리프+jwt를 이용한 회원가입 및 로그인

 

+ Recent posts