본문 바로가기
SpringBoot

[Spring Boot] 30. Google OAuth with JWT (2)

by 청양호박이 2021. 4. 19.

지난 시간에는 jjwt 0.11.2를 이용해서 jws(short for signed JWT)를 생성해서 생성결과를 확인하는 부분까지 진행했었습니다. Json Web Token은 Header, Payload, Signature... 총 3가지 부분으로 구성되며 각 항목순으로 '.'을 중간에 붙여서 최종적인 Token이 생성됨을 확인했었습니다. 

 

그 이후에는 생성된 결과를 jwt.io의 decode page를 통해서 header와 payload의 내용을 검증도 해 보았습니다. 이번에는 아래의 그림의 절차 중에서... Token 보관 부분에 대해서 구현해 보겠습니다. 

생성된 Token을 사용자에게 전달하여, Token을 개별 Storage에 저장하는 부분에 대해서 생각해보면... 생성한 Token을 사용자에게 전달하여 보관하는 방법에 대해서는 많은 방법이 있지만, 우선 Session Storage에 토큰을 저장해서 사용 하겠습니다. 

추후에 Local Storage, Session Storage, Cookies에 대해서 비교해 보고 최종적으로는 Cookie를 구워보도록 하겠습니다. 그럼 우선은 생성된 Token을 사용자에게 전달하는 단계부터 시작하겠습니다. 

 

 

1. Front-End Token전달


의외로 Front-End에 Token을 전달하는 부분은 어렵지 않습니다. 추후에 Token은 모두 httpHeader에 넣어서 다니기 때문에, Token은 httpResponse Header에 보내고 나머지는 본문에 넣어서 보내주기로 하겠습니다. Controller에서 Header에 Token을 넣는 방법은 여러가지가 있습니다. 

 

[HttpServletResponse 사용]

	public ResponseEntity<UserDTO> tokenVerify(String idToken, HttpServletResponse res){

		UserDTO userInfo = googleOAuthService.tokenVerify(idToken);
		
		if(userInfo.getEmail() != null) {
			String atJwtToken = jwtService.create();
			res.addHeader("at-jwt-token", atJwtToken);
		}
		
		return ResponseEntity.ok(userInfo);
	}

Controller 내 개별 API에 HttpServletResponse를 정의하고, 코드 본문에서 단지 res.addHeader를 추가함으로써 자동으로 response를 통해서 Token값이 넘어갑니다.

 

[ResponseEntity 사용]

	public ResponseEntity<UserDTO> tokenVerify(String idToken){

		UserDTO userInfo = googleOAuthService.tokenVerify(idToken);
        
        	HttpHeaders res = new HttpHeaders();
		
		if(userInfo.getEmail() != null) {
			String atJwtToken = jwtService.create();
			res.add("at-jwt-token", atJwtToken);
		}
		
		return ResponseEntity.ok().headers(res).body(userInfo);
	}

명시적으로 HttpHeaders를 통해서 신규 HttpHeader를 생성하고 그 안에 res.add로 custom header를 추가해 줍니다. 최종적으로 ResponseEntity를 build하고 header와 body를 명기하여 추가해 줍니다. 이를 통해서 사용자에게 header를 통한 Token값 전송이 가능합니다. 

 

추가적으로 Google API를 통해서 Verification을 수행한 결과로 payload에 있던 값들은 UserDTO를 하나 추가하여 전달하도록 하였습니다. 그렇다면, 이렇게 전달된 결과를 우선 Front-End에서 console.log로 확인해 보겠습니다. 

 

[Error]

response data 
	{email: "xxx@gmail.com", 
     name: "이름", 
     pictureUrl: "https://lh4.googleusercontent.com/photo.jpg", 
     locale: "ko",  …}

response header
	{content-type: "application/json"}

뭔가 이상합니다. res.data를 통해서는 보낸 userInfo가 정상적으로 response되었지만 header정보가 전혀 보이지 않습니다. 이상한 마음에 postman을 통해서 강제로 해당 API에 x-www-form-urlencoded로 key/value를 추가하여 요청을 보내보면...

 



 

2. CORS configuration


header부분에 정상적으로 custom header인 atJwtToken값이 보입니다. 이런 당황스러운 상황에 대해서 왠지 axios에서 값을 제대로 못 가져온다고 생각할 수 있지만, 결국 axios는 Back-End로 부터 받은 결과를 그대로 보여줄 뿐 입니다. 

이를 해결하기 위해서는 Back-End에 약간의 configuration작업이 필요합니다. 결국 이 문제는 CORS에 해당하는 문제로 addCorsMappings부분에서 exposedHeaders method에 해당 header의 key 이름을 지정해서 넣어주면 정상적으로 axios를 통한 header값이 정상적으로 보이게 됩니다.

 

If axios response headers only have content-type, It is just related with Back-End CORS configuration.

 

CORS에 대한 configuration은 WebMvcConfigurer에 포함되며, 해당 부분의 최종 코드는 아래와 같습니다. 

 

[WebConfiguration.java]

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
        	.allowedMethods("GET", "POST")
        	.exposedHeaders("at-jwt-token")
        	.maxAge(3000);
	}

}

자 이제 다시한번 시도해 보도록 하겠습니다. 

 

[결과]

response data 
	{email: "xxx@gmail.com", 
     name: "이름", 
     pictureUrl: "https://lh4.googleusercontent.com/photo.jpg", 
     locale: "ko",  …}

response header
	{at-jwt-token: "eyJ0eXAiOiJKV1QiLCJhbGcixMiJ9.eyJpcasdfqet.asdfqtweywrye",
     content-type: "application/json"}

console.log를 통해서 res.headers를 확인한 결과 정상적으로 Token이 response header를 통해서 사용자에게 전달됨을 확인했습니다. 

 

 

3. Session Storage에 Token저장


이제 사용자에게 Token이 전달되었으니, 간단하게 Session Storage에 Token을 저장해 보도록 하겠습니다. Local Storage에 저장도 방법은 크게 차이가 나지 않으니, 2가지에 대한 차이를 간단하게 알아보고 구현해 보겠습니다. 

이 2가지는 공통적으로 HTML5에 포함된 스펙으로 web application의 데이터를 사용자의 환경에 저장할 수 있는 수단입니다. 저장되는 방식은 key/value 쌍으로 저장되며, key로 데이터를 불러와 사용할 수 있습니다. 이 저장되는 데이터는 가장 상위에 Domain으로 구분이 되기 때문에 타 Domain에서 해당 데이터를 사용할 수 없습니다. 

하지만 차이점은 아래와 같습니다. 

 

  • Local Storage: 저장한 데이터를 명시적으로 setItem("이름", "")처럼 지워야 하며, 지우지 않을 경우 영구적으로 보관. Domain만 같으면 타 browser를 통해서 접근해도 사용가능.
  • Session Storage: 현재 사용중인 browser를 종료하면 해당 Domain의 Storage에 저장된 데이터가 자동으로 지워짐. Domain보다 browser가 상위개념으로 있기 때문에 같은 Domain이라도 데이터 접근 사용 불가.

 

[window.sessionStorage]

// storage 설정
const storage = window.sessionStorage;

// setItem
storage.setItem('at-jwt-token', res.headers['at-jwt-token']);

원하는 vue.js의 methods위치에 해당 설정을 추가하면 다음과 같이 sessionStorage에 Token을 저장하게 됩니다. 

 

[번외 - window.localStorage]

// storage 설정
const storage = window.localStorage;

// setItem
storage.setItem('at-jwt-token', res.headers['at-jwt-token']);

storage를 지정하는 부분만 변경해 주면 됩니다.

 

이렇게 정상적으로 Back-End에서 생성한 jws를 정상적으로 response header에 포함하여, 사용자의 개별 browser에 Token을 저장하는 로직을 구현하였습니다. 

 

- Ayotera Lab -

댓글