프로젝트/Moamoa

[프로젝트] 스프링 시큐리티를 이용한 소셜로그인 및 JWT 구현하기 - (1)

개발하는 민우 2022. 7. 28. 17:07

[참고 글 및 코드]

https://velog.io/@tmdgh0221/Spring-Security-%EC%99%80-OAuth-2.0-%EC%99%80-JWT-%EC%9D%98-%EC%BD%9C%EB%9D%BC%EB%B3%B4

Spring Security 와 OAuth 2.0 와 JWT 의 콜라보

Spring Boot, Spring Security, OAuth 2.0, JWT 와의 치열한 싸움 기록

velog.io

https://deeplify.dev/back-end/spring/oauth2-social-login

[Spring Boot] OAuth2 소셜 로그인 가이드 (구글, 페이스북, 네이버, 카카오)

스프링부트를 이용하여 구글, 페이스북, 네이버, 카카오 OAuth2 로그인 구현하는 방법에 대해서 소개합니다.

deeplify.dev

https://velog.io/@max9106/OAuth

[OAuth + Spring Boot + JWT] 1. OAuth란? 프론트엔드와 백엔드의 역할

OAuth(Open Authorization)란? OAuth는 인증을 위한 프로토콜이다. 다른 인터넷 서비스의 기능을 다른 어플리케이션에서도 사용할 수 있게 해준다. OAuth는 인증(Authentication)과 인가(Authorization)를 모두 포함

velog.io

✅️본 프로젝트는 위의 코드를 참고 및 변형하여 만들어 졌음을 알리며, 개념 또한 위 글을 참고하여 작성했습니다.

☑️[모아모아 프로젝트]
https://github.com/Asset-management-service/backend

GitHub - Asset-management-service/backend: 📚 자산 관리 서비스 - BE

📚 자산 관리 서비스 - BE. Contribute to Asset-management-service/backend development by creating an account on GitHub.

github.com


일단 들어가기 앞서 Spring security와 OAuth2.0, JWT에 대해 알아보자.

✅️ Spring Security

스프링 시큐리티는 서블릿 필터와 이들로 구성된 필터
체인을 사용한다.


1. 사용자가 로그인 정보와 함께 인증 및 요청(http Request)
2. AuthenticationFilter가 요청을 가로채어 미검증 Authentication 객체인 UsernamePasswordAuthenticationToken 생성
3. AuthenticationManager에게 토큰 객체 전달
4. AuthenticationProvider에게 토큰 객체 전달
5. 실제 DB로부터 사용자 인증 정보를 가져오는 UserDetailsService에 정보 전달
6. 넘겨받은 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체 생성
7. AuthenticationProvider는 넘겨받은 객체를 통해 사용자 검증
8. 인증 완료시 사용자 정보를 담은 Authentication 객체 전달
9.최초의 AuthenticationFilter에 Authentication 객체 반환
10. Authentication 객체를 SecurityContext 에 저장


실질적인 인증 과정은 사용자가 입력한 ID, Password 와 UserDetailService의 loadUserByUsername() 메서드가 반환하는 UserDetails 객체를 비교함으로써 동작하기 때문에 UserDetails를 어떻게 구현하느냐에 따라서 동작 방식이 달라진다.

📚 프로젝트에서는 User 엔티티에 UserDetails를 구현하는 방식과 달리 따로 CustomUserDetails를 생성하였음.



OAuth 2.0 로그인을 사용한다면, UsernamePasswordAuthenticationFilter 대신, Oauth2LoginAuthenticationFilter가 호출된다!

✅️[OAuth2.0에 적용되는 핸들러 개념 설명]

AuthenticationEntryPoint: 스프링 시큐리티 내에서 인증과정에서 실패하거나, 인증 헤더를 보내지 않는 경우 401 에러를 보내는 로직 처리 부분

AuthenticationSuccessHandler, AuthenticationFailureHandler: 인증 성공, 실패시 스프링 시큐리티 내에서 필요한 작업을 구현하는 인터페이스

UserDetailsService : DB에서 유저 정보를 가져오는 역할을 하는 인터페이스

UserDetails: 스프링 시큐리티에서 사용자의 정보를 담는 인터페이스




✅️ OAuth2.0

Oauth란 타사의 사이트에 대한 접근 권한을 얻고, 그 권한을 이용하여 개발할 수 있도록 도와주는 프레임워크이다. 즉 구글, 네이버, 카카오 소셜로그인 연동시 필요한 프레임워크라고 이해하면 된다.

Resource Owner: 사용자, 사람이 될 수도 있고, Application 자체가 될 수도 있다.(ex. 나)
Client Application: 사용자가 사용하는 서비스 애플리케이션.(ex. moamoadev.shop)

Resource Server: OAuth를 통해 인증, 인가를 제공해주는 서버. 자원 서버. 자원(이름, 이메일, 프로필 사진 등)을 제공해준다.(ex. github, naver, kakao, google)

Authorization Server: OAuth를 통해 인증, 인가를 제공해주는 서버. 인증 서버. 토큰을 발급해준다.(ex. github, naver, kakao, google)


내가 적용한 프로젝트에는 Authorization Code Grant 방식이 적용되었다.

✅️ Authorization Code Grant
OAuth 서버에서 client Application에게 바로 access token을 넘겨주는 것이 아니라, Authorization code를 넘겨주고, client Application은 Authorization code를 통해 access token을 발급 받아, access token으로 허가된 리소스 요청을 하는 방식이다.

❗️이렇게 Authorization code를 도입하게 되면 access token 자체는 백엔드에서만 존재하게 되므로, 중간에 access token을 탈취당하지 않게 된다.

📚 모아모아 프로젝트에서는 google, kakao, naver을 통해 로그인(인증), (인가) 이 2가지를 OAuth를 이용해서 구현하고, 회원은 애플리케이션의 특정 API를 사용할 수 있다를 처리할 때는 JWT를 사용하였다.



✅️ JWT

AccessToken은 리소스 (사용자 정보) 에 직접 접근할 수 있도록 해주는 정보만을 가지고 있다.
RefreshToken에 비해 짧은 만료 기간을 가지며, 주로 세션에 담아 관리한다.

Refresh Token은 새로운 Access Token을 발급받기 위한 정보를 담고 있다. 클라이언트가 Access Token이 없거나 만료된 상태라면, Refresh Token을 통해 Auth Server에 요청하여 새로운 Access Token을 발급 받을 수 있다. Refresh Token은 외부에 노출되지 않도록 하기 위해 보통은 DB에 저장하곤 한다.

📚 프로젝트에서는 RefreshToken을 자체 내장 DB(redis)에 저장하였고, 보안을 위해 Refresh Token을 쿠키로 전달하였다.

OAuth2.0은 JWT 방식과 달리 구글, 카카오, 네이버의 Authorization Server를 통해 회원 정보를 인증하고, AccessToken을 발급받는다. 발급받은 AccessToken을 통해 직접 개발한 서버의 API 서비스를 이용한다.

❗️[문제점] 매번 요청마다, accessToken이 유효한지를 확인과, 검증 및 업데이트를 위한 DB 검증이 필요함. 그래서 프로젝트에서는 JWT를 이용하였음.



[JWT의 장점]
✅️
JWT는 Claim 기반 방식을 사용하는데, 여기서 Claim이란 사용자에 대한 속성 값들을 가리킨다.
즉, JWT은 의미있는 토큰 (사용자의 상태를 포함) 으로 구성되어 있기 때문에, Auth Server에 검증 요청을 보내야만 했던 과정을 생략하고 각 서버에서 수행할 수 있게 되어 비용 절감 및 Stateless 아키텍처를 구성할 수 있다.

JWT에서는 AccessToken과 RefreshToken을 사용하는데, AccessToken으로 API 호출을 요청하고, AccessToken이 만료되면, RefreshToken이 유효한지와 만료되었는지 여부를 탐색하여 재 발급한다.
refreshToken은 클라이언트의 쿠키로 저장하며, 유효기간을 길게 설정한다.
accessToken은 앱 내 변수로 관리한다.
클라이언트에서 서버 데이터 요청 시 accessToken으로 인증된 사용자의 여부를 판단한다.


📚 프로젝트 내에서 RefreshToken 발급에는 세션에 담는 방법, 쿠키에 담는 방법이 있는데 우리는 쿠키에 담는 방법을 채택하였음.



❗️[리프레시 토큰을 세션에 담는 방법의 단점]
1. CSRF 공격에 취약하다.
2. SSR 서버의 세션 부하가 너무 많이 발생한다.
-> 로그인 요청이 들어오는 모든 사용자마다 세션을 관리하고 생성해줘야 한다.

📚 그래서 프로젝트 내에서 보안을 위해 서버에서 직접 특정 주소로 리다이렉트하여 accessToken을 발급하고, RefreshToken을 쿠키에 담아 클라이언트에 전달하였다.

‼️[쿠키 구현을 위해 필요한점]

1. Samesite=None 설정 및 Secure 설정 (이는 백엔드 배포에서 https 적용이 필요함)
2. 다른 도메인간의 쿠키값을 저장하는 것이 현실적으로 불가능하여 클라이언트를 서브 도메인 및 같은 도메인으로 설정해야 함. -> (대략 3일 동안 작업하면서 이를 해결하기 위해 정말 진땀을 흘렸다 😭)
3. 쿠키 전달을 위한 CORS(교차 출처 리소스 공유)