본문 바로가기

2023년/멋쟁이사자처럼 팀프로젝트

[팀프로젝트] Spring Boot에서 jwt + 쿠키를 활용한 로그인 구현과 비동기 방식 처리

팀 프로젝트 3주차

spring boot 에서 jwt 토큰을 발급 받은 뒤 어떤식으로 처리하면 좋을까에 대한 생각을 적은 글

 

주의 : 이론적 설명을 제외하고 어떻게 사용할까에 대한 담은 글입니다. 참고용도로만 적용해주시고 실무에서는 꼭 보안규칙을 따져가면서 꼼꼼히 작성하셔야합니다!

 

jwt 토큰은 user 정보가 들어있기 때문에 매우 신중히 고려해서 다뤄줘야됨

 

구글링으로 찾은 내용으로,, jwt 를 저장하는 방식으로는

1. 로컬스토리지 저장

2. 쿠키에 저장

+a. httpSession 활용

간략히 이렇게 나타나 있는데, 각자의 장단점 및 이론에 대해서는 추후 정리하고 일단 어떻게 사용하는지에 대해서 설명함.

 

백엔드에서 로그인 성공 후 Response 값 (swagger-ui)

보통 jwt 값이 백엔드에서는 위와 같이 리턴되게 될텐데 (jwt 를 string 방식으로 반환해준 것)

로컬 스토리지를 사용할 경우 프론트엔드에서 처리를 하게 됨 

1. 로컬 스토리지 + JWT

 

loginTest.js

async function loginTest() {
    let response = await fetch("/api/v1/users/login", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({
            userId: document.getElementById("username1").value,
            password: document.getElementById("password1").value
        })
    })
    // fetch 비동기 방식으로 해당 body값에 대해서 api 접근
    if(response.ok){
        let json = await response.json();
        document.getElementById("area_login").innerHTML = json.resultCode;
        localStorage.setItem("jwt",'Bearer ' + json.result.accessToken);
        console.log(localStorage.getItem("jwt"));
    }else{
        let json = await response.text();
        document.getElementById("area_login").innerHTML = json;
    }
    // 해당 리스폰스 값에 따른 출력
    ;
}

위 소스에서 중요하게 볼 점은 백엔드에서 나온 Response를 어떻게 처리하냐 이말이다.

localStorage.setItem("jwt",'Bearer ' + json.result.accessToken);

fetch 비동기 메소드를 통해 나온 Response 값(promise 타입)을 > Json 형식으로 전환시켜줘서 해당 데이터에 접근 할 수 있다.

결과 값으로 나온 데이터를 위 소스를 통해 setItem 으로 jwt 토큰을 localstorage에 저장 할 수 있다. 

F12 개발자도구 - Application - Storage - Local Storage 에서 확인 가능!

지금은 없지만 set 하면 key value 값으로 저장된다.

사용은 어떻게?

 

로그인의 유무와 Security 설정 후 Authentication 의 헤더에 적용할 수 있는 방법

 

간단히 게시글 작성하는 비동기식 메소드 에서

async function crewMake() {
    let response = await fetch("/api/v1/crews", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": localStorage.getItem('jwt'),
            "Authentication": localStorage.getItem('jwt')
        },
        body: JSON.stringify({
            content: document.getElementById("content").value,
            title: document.getElementById("title").value
        })
    })
    if(response.ok){
        let json = await response.json()
        console.log(json);
        document.getElementById("area_crew").innerHTML = json.result.crewId + " 모임생성 " + json.result.message;
    }else{
        let json = await response.text();
        document.getElementById("area_crew").innerHTML = json;
    }
}

fetch 헤더부분을 참고해보면, 

"Authorization": localStorage.getItem('jwt'),
"Authentication": localStorage.getItem('jwt')

getItem 으로 접근해서 데이터를 헤더에 담아 보낼 수 있다. 

 

F12 개발자도구에서 콘솔창으로 쉽게 테스트 해 볼 수 있으니까 참고!

 

2. 쿠키 + jwt

백엔드단에서 쿠키의 방식을 설정해줘서 보낼 수 있다.

간단한 로그인 예제

로그인이 성공하면 cookie를 담아 view 를 반환해준다.

@PostMapping("/view/v1/signin")
public String login(UserLoginRequest userLoginRequest, HttpServletResponse response) throws UnsupportedEncodingException {
    UserLoginResponse tokens = userService.login(userLoginRequest);
    //cookie 설정은 스페이스가 안되기 때문에 Bearer 앞에 +를 붙인다. Security Filter에서 + -> " " 로 치환할 것이다.
    
    Cookie cookie = new Cookie("jwt", "Bearer+"+tokens.getAccessToken());
    cookie.setPath("/");
    cookie.setSecure(true);
    cookie.setHttpOnly(true);
    cookie.setMaxAge(60 * 25); //초단위 25분설정
    response.addCookie(cookie);

    return "redirect:/view/v1/crews";
}

이슈1. cookie 설정에서 스페이스가 안되어 저장하는 방식을 다음과 같이 설정해줬음.

이슈2. setPath를 안해줘서 다른 api 호출간 쿠키가 넘어가지 않는 현상을 확인

 

Cookie cookie = new Cookie("jwt", "Bearer+"+tokens.getAccessToken());
//쿠키를 만듬
cookie.setPath("/");
//쿠키가 적용되는 범위 및 경로지정 (/ 모든경로)
cookie.setSecure(true);
//쿠키 ssl 통신시에만 쿠키를 넘겨주도록 설정
cookie.setHttpOnly(true);
//설정해주면 브라우저에서 쿠키에 접근하지 못하게됨 ㅠㅠ
cookie.setMaxAge(60 * 25); //초단위 25분설정
response.addCookie(cookie);
//response 에 쿠키를 담아서 보냄

 

view 화면에서 로그인을 성공하게 되면, 아래와 같이 쿠키 단에서 jwt 토큰값이 잘 저장되어 있는 것을 확인 할 수 있다.

 

내가 만든 쿠키

사용은 어떻게?

다른 url 을 호출하게 되면 쿠키에 있는 데이터가 헤더에 담겨 자동적으로 넘어간다.

백엔드에서는 request의 header 부분에 jwt 토큰값에 접근해서 filter 처리를 해주면 됨.

 

httponly true 옵션 쿠키를 비동기 방식에서 처리

let response = await fetch(url,{
    method: "GET",
    headers: {
        "Content-Type": "application/json"
    },
    credentials: "same-origin",
    credentials: 'include'
})

header 부분에 credentials 옵션을 적용하면 쿠키값 전체가 넘어간다.

 

 

+ 쿠키는 원래 js 에서 접근 가능하다. 단) httponly 속성이 아닐 경우

httponly 속성 때문에 조회가 안됨
속성 체크해제 후에는 사용 가능하다.

3. a+ 세션에 저장

간단히 로그인 로직을 거친 뒤 jwt 결과값을 session에 저장 후 view 로 반환해주는 컨트롤러

@PostMapping("/login")
public String login(@Valid UserLoginRequest userLoginRequest , HttpServletRequest httpServletRequest, Model model) {
    String jwtToken = "";
    jwtToken = userService.login(userLoginRequest).getJwt();
    
    httpServletRequest.getSession().invalidate();
    //세션의 데이터를 삭제함
    //초기화, 로그아웃할때 활용
    
    HttpSession session = httpServletRequest.getSession(true);
    //로그인 성공처리
	//새로운 신규 세션을 생성
    
    session.setAttribute("jwt", "Bearer " + jwtToken);
    //세션에 jwt 관련 정보 저장
    
    session.setMaxInactiveInterval(1800);
    //유효시간 설정

    return "redirect:/view/v1/posts/list";
}

서블릿을 통해 HttpSession을 생성하면 다음과 같은 쿠키를 생성

쿠키 이름이 JSESSIONID이고, 값은 추정 불가능한 랜덤 값이다.

 

개발자 도구에서 확인가능하다.

사용은 어떻게?

세션에 저장한 데이터에 대해서는

protected void method(HttpServletRequest request){
	header = request.getSession().getAttribute("jwt").toString();
}

getSession() 메소드를 활용해서 데이터의 유무를 찾음

 

간략하게 스프링부트에서 jwt 토큰 저장 방식과 활용 방법에 대해서 확인했는데,, 아직 더 많은 산이 남아 있는거 같다.

쿠키와 로컬스토리지 차이에 대해서는 서로 문제가 있다고 하니 이론적인 부분에 대해서는 추후 공부예정이며,  redis 를 통한 refresh 토큰 방식을 채택한다면 두 단점을 조금이나마 커버할 수 있는 팁을 남기고 갑니다. 감사합니다.

팀프로젝트는 어렵지만 재밌다!