본문 바로가기
개발/스프링

[혼자 구현하는 웹서비스] 5. 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (1)

by 카펀 2021. 12. 31.

0. 개요

1. 스프링 시큐리티와 스프링 시큐리티 Oauth2 클라이언트

2. 구글 서비스 등록

3. 구글 로그인 연동하기

4. 어노테이션 기반으로 개선하기

5. 세션 저장소로 데이터베이스 사용하기

6. 네이버 로그인

7. 기존 테스트에 시큐리티 적용하기

 

*이 글은 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' (프리렉, 이동욱 저)를 공부하며 내용을 정리한 글입니다.

 

0. 개요

스프링 시큐리티는 막강한 인증과 인가 (Authentication and Authorization)를 가진 프레임워크이다. 스프링 기반에서의 보안 표준으로, 스프링에서는 이를 활용하기를 적극 권장하고 있다. 확장성을 고려한 프레임워크이므로 다양한 기능을 손쉽게 추가 및 변경할 수 있고, 이는 스프링 부트 2.0으로 넘어오며 더욱 강력해졌다.

 

1. 스프링 시큐리티와 스프링 시큐리티 Oauth2 클라이언트

OAuth란 무엇일까? 먼저 개념을 짚고 넘어 가는 것이 좋겠다.

OAuth (Open Authorization)란, 웹 서비스에서 ID/Password를 받는 기존의 로그인 방식과 달리, 이미 인증된 웹사이트를 거쳐서 로그인을 하는 것이다. 우리가 흔히 사용하는 소셜 로그인이 이에 해당되는데, Facebook, Google, Naver, Kakao 등 이미 보안 수준이 어느 정도 검증된 사이트를 통해 로그인을 하는 방식이다. 자세한 내용은 이 글을 참고하자.

OAuth를 이용하면, 아래의 내용들을 직접 구현하지 않아도 된다.

 

  • 로그인 시 보안
  • 회원가입 시 이메일 혹은 전화 인증
  • 비밀번호 찾기/변경
  • 회원정보 변경

즉 상대적으로 규모가 작은 웹 서비스는 이러한 내용을 직접 구현하지 않아도 되어 비용과 시간을 절약하고 서비스 개발에만 집중할 수 있고, 이러한 API를 제공하는 웹 서비스는 사용자를 자신의 서비스 안에 묶어둘 수 있으니 서로 윈-윈이다.

 

스프링 부트 1.5에서의 OAuth 연동 방법이 2.0에서는 크게 바뀌었다. 하지만 인터넷에서 찾수 있는 자료들에서는 크게 차이가 없는 방법을 소개하는데, 이는 spring-security-oauth2-autoconfigure 라이브러리 덕분이다. 이를 사용할 경우 스프링 부트 2에서도 기존 1.5에서 사용하던 설정을 그대로 사용할 수 있다. 기존에 안전하게 작동하던 코드를 사용하는 것이 아무래도 더 확실하므로, 많은 개발자가 이 방식을 사용하였다.

여기서는 스프링 부트 2.X에서의 방식인 Spring Security Oauth2 Client 라이브러리를 사용하려고 한다. 이유는 다음과 같다.

 

  • Spring 팀에서 기존 1.5에서 사용되던 spring-security-oauth 프로젝트에는 더 이상 신규 기능을 추가하지 않고, 버그 수정 정도의 기능만 추가할 예정이며, 신규 기능은 새 oauth2 라이브러리에서만 진행하겠다고 선언
  • 스프링 부트용 라이브러리 (starter) 출시
  • 기존 방식은 확장 포인트가 적절하게 오픈되어 있지 않아 직접 상속/오버라이딩 해야 함. 신규 라이브러리의 경우 확장성을 고려하여 설계됨

CommonOAuth2Provider라는 enum이 새롭게 추가되고, 구글, 깃허브, 페이스북, 옥타의 기본 설정값은 여기서 제공한다. 이 외에 네이버, 카카오 등 다른 소셜 로그인을 추가하고 싶다면 직접 추가해야 한다.

 

2. 구글 서비스 등록

구글 서비스에 신규 서비스를 생성한다. 구글 클라우드 플랫폼에 접속한 후, 아래와 같이 진행한다.

Google Could Platform에서의 진행 과정

좌측 상단에서 프로젝트 선택 > 새 프로젝트 > 프로젝트명을 원하는 대로 지으면 된다.

 

API 및 서비스 - 사용자 인증 정보 만들기

생성이 완료되었다면 API 및 서비스 카테고리로 이동, 사용자 인증 정보 > 사용자 인증 정보 만들기 를 선택한다.

 

OAuth 동의 화면

좌측 탭에서 API 및 서비스 > 대시보드를 선택하고, 사용자 인증 정보 > 사용자 인증 정보 만들기를 누른다.

OAuth 동의 화면으로 넘어가서, 애플리케이션 이름, 지원 이메일, Google API의 범위를 작성한다.

 

동의  화면 구성이 끝났으면 다음으로 진행한다.

 

OAuth 클라이언트 ID 만들기

클라이언트 ID 만들기 화면으로 넘어가서, 위처럼 진행하면 된다. 승인된 리디렉션 URI에는 위처럼 입력했다.

승인된 리디렉션 URI는 서비스에서 파라미터로 인증 정보를 주었을 때, 인증이 성공하면 구글에서 리다이렉팅 할 URL이다. 스프링 부트 2의 시큐리티에서는 기본적으로 (도메인)/login/oahtu2/code/(소셜서비스코드) 로 리다이렉트 URL을 지원하고 있으며, 사용자가 별도로 리다이렉트 URL을 지원하는 Controller를 만들 필요가 없다.

이 내용은 추후 AWS 서버에 배포하게 되면 추가적으로 주소를 추가해야 한다.

 

위 과정까지 마쳤다면 생성된 클라이언트 정보를 볼 수 있고, 이에 따른 인증 정보 역시 확인이 가능하다.

 

클라이언트 ID와 클라이언트 보안 비밀 (clientID and clientSecret) 코드를 프로젝트에 설정해 보자.

application.properties가 있는 src/main/resources/ 디렉토리에 application-oauth.properties 파일을 생성한다.

 

application-oauth.properties 파일을 생성하고, 위처럼 입력해 준다.

clientID, clientSecret은 각자 다를 것이다. 위에서 생성한 내용을 입력하면 된다.

많은 예제에서 위의 3번째 줄의 scope를 별도로 등록하지 않는다. 기본값은 openid, profile, email인데, openid라는 scope가 존재하면 Open ID provider로 인식하기 때문이다. 이렇게 되면 OpenID Provider인 서비스(Google)과 그렇지 않은 서비스 (Naver, Kakao)로 나눠서 각각 OAuth2Service를 만들어야 한다.

 

스프링 부트에서는 properties의 이름을 application-!@#$.properties로 만들면, !@#$라는 이름의 profile이 생성되어 이를 통해 관리할 수 있다. 스프링 부트의 기본 설정 파일인 application.properties에서 application-oauth.properties를 포함하도록 해 보자.

application.properties를 열고 다음과 같은 내용을 추가한다.

 

4번째 줄의 내용을 추가한다.

이제 이 설정값을 사용할 수 있다.

 

마지막으로, application-oauth.properties가 GitHub에 올라가지 않도록 해야 한다. clientID와 clientSecret은 보안이 중요한 정보인데, 이 값이 외부에 노출되면 언제든 개인정보를 가져갈 수 있는 취약점이 될 수 있다.

.gitignore 파일을 열고 다음과 같은 내용을 추가하자.

 

6번째 줄에 해당하는 내용을 추가하면 된다.

위 내용을 추가한 이후 commit 했을 때, application-oauth.properties가 commit되지 않으면 성공이다.

 

3. 구글 로그인 연동하기

발급 받은 구글 로그인 인증 정보를 이용해 로그인을 연동해 보자.

먼저 사용자 정보를 담당할 도메인인 User 클라스를 생성하자. 패키지는 domain 아래에 user 패키지를 생성하면 된다.

 

User.java

27번째 줄의 Enumerated(EnumType.STRING)의 경우, JPA로 데이터베이스로 저장할 때 Enum 값을 어떤 형태로 저장할 것인지 결정하는 것이다. 기본적으로는 int로 된 숫자를 저장하지만, 그렇게 하면 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는지 알 수 없으므로, 문자열로 저장될 수 있도록 선언하였다.

 

이어서, 각 사용자의 권한을 관리할 Enum 클라스인 Role을 생성하자.

Role.java

스프링 시큐리티에서는 권한 코드에 항상 "ROLE_"이 앞에 있어야만 한다. 위의 경우처럼 "ROLE_GUEST", "ROLE_USER" 등이 그 예이다.

 

마지막으로 User의 CRUD를 책임질 UserRepository도 생성한다.

UserRepository.java

findByEmail의 경우, 소셜 로그인으로 반환되는 값 중 email을 통해 기존에 이미 가입한 적이 있는 사용자인지 판단하기 위한 메소드이다.

 

이상으로 User 엔티티 관련 코드를 모두 작성하였다. 다음으로는, 본격적으로 시큐리티 설정을 진행하겠다.

 

먼저 build.gradle에 스프링 시큐리티 관련 의존성을 하나 추가해야 한다.

compile('org.springframework.boot:spring-boot-starter-oauth2-client')

소셜 로그인 등, 클라이언트 입장에서 소셜 기능 구현 시 필요한 의존성이다.

 

다음으로, OAuth 라이브러리를 이용한 소셜 로그인 설정 코드를 작성한다.

config.auth 패키지를 생성한다. 여기에는 앞으로 시큐리티 관련 클라스를 모두 담을 것이다.

이어서 SecurityConfig 클라스를 생성하고 아래와 같이 코드를 작성한다.

 

SecurityConfig.java

위 코드는 다음과 같은 역할을 수행한다.

  • 10: Spring Security 설정들을 활성화시켜 준다.
  • 18: h2-console 화면을 사용하기 위해 해당 옵션들을 disable 한다.
  • 21: URL별 권한 관리를 설정하는 옵션의 시작점으로, authorizeRequests가 선언되어야지만 antMatchers 옵션을 사용할 수 있다.
  • 22: antMatchers. 권한 관리 대상을 지정하는 옵션으로, URL, HTTP 메소드별로 관리가 가능하다.
    • 위 코드에서는 "/" 등 지정된 URL들은 permitAll() 옵션을 통해 전체 열람 권한을 주었다.
    • "api/v1/** 주소를 가진 API는 USER 권한을 가진 사람만 사용하도록 하였다.
  • 25: anyRequest. 설정된 값들 이외의 나머지 URL들을 나타낸다.
    • 여기서는 authenticated()를 추가하여, 나머지 URL들은 모두 인증된 사용자 (로그인한)에게만 허용하도록 한다.
  • 28: 로그아웃 기능에 대한 여러 설정의 진입점으로, 성공 시 / 주소로 이동한다.
  • 30: OAuth2 로그인 기능에 대한 여러 설정의 진입점이다.
  • 31: OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정을 담당한다.
  • 32: userService. 로그인 성공 후 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록한다. 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있다.

위 코드는 아직 실행이 되지 않는데, CustomOAuth2UserService 클라스를 아직 작성하지 않았기 때문이다.

이를 작성하도록 하자. 이 클라스에서는 구글 로그인 이후 가져온 사용자의 정보를 기반으로 가입 및 정보 수정, 세션 저장 등의 기능을 지원한다.

 

CustomOAuth2UserService.java

마찬가지로 코드의 역할은 다음과 같다.

  • 29: 현재 로그인 진행 중인 서비스를 구분하는 코드이다. 구글/네이버/카카오 등의 서비스를 구분하기 위해 쓰인다.
  • 31: OAuth2 로그인 진행 시 키가 되는 필드값을 이야기한다. Primary key와 같은 의미이다.
  • 34: OAuth2UsersService를 통해 가져온 OAuth2User의 attribute를 담을 클라스이다.
  • 37: SessionUser. 세션에 사용자 정보를 저장하기 위한 Dto 클라스이다.

이어서 OAuthAttributes 클라스를 생성한다.

 

OAuthAttributes.java

마지막으로, 앞에서 언급했던 세션에 사용자 정보를 저장하기 위한 Dto 클라스인 SessionUser 클라스를 config.auth.dto 패키지에 추가한다.

 

SessionUser.java

SessionUser는 인증된 사용자 정보만 필요하므로, name, email, picture를 필드로 선언한다.

기존의 User 클라스를 사용하려고 하면, 직렬화 (serialization)가 구현되어 있지 않다는 에러가 뜬다. User 클라스는 엔티티이므로, 언제 다른 엔티티와 관계가 형성될지 알 수 없다. 그래서 지금처럼 직렬화 기능을 가진 세션 Dto를 따로 하나 만드는 것이 운영 및 유지보수 면에서 매우 유리하다.

 

지금까지 작성한 코드, 즉 스프링 시큐리티가 잘 작동하는지 확인하기 위해 기존의 화면에 로그인 버튼을 추가해 보자.

index.mustache에 로그인 버튼과 로그인 성공 시 사요자 이름을 보여주는 코드를 추가한다.

 

index.mustache

<!-- 위 사진의 11번째 줄 끝에 보면 role = "button> 이라고 되어 있다. role = "button"> 이라고 해야 한다. -->

 

로그인 기능 영역의 코드를 추가하였다.

이어서, index.mustache에서 userName을 사용할 수 있도록 IndexController에서 userName을 model에 저장하는 코드를 추가한다.

 

IndexController.java

잘 적용되었는지 프로젝트를 실행해서 확인해 보자.

Application.java의 메인 메소드를 실행하고, http://localhost:8080으로 접속한다.

실행 화면

정상적으로 로그인이 되는 것을 확인할 수 있다.

실제로 내부적으로는 어떨까? localhost:8080/h2-console에 접속하여 DB를 확인해 보자.

 

http://localhost:8080/h2-console

DB에서 user 테이블을 확인해 보았다.

SELECT email, name, picture, role from USER

접속한 계정이 잘 뜨는 것을 확인할 수 있다. 한 가지 유의할 점은, 권한, 즉 ROLE이라는 속성이 GUEST로 되어 있다는 점이다.

그렇다면 글 작성을 할 수 있는지 한번 테스트 해 보자.

 

게시글 등록 테스트

등록을 클릭하면 위 우측 이미지처럼 오류를 내뱉는 창이 뜬다. 403 에러는 권한 오류를 의미한다.

권한을 GUEST가 아닌 USER로 바꾸어 보자.

 

권한을 수정하고, 글을 다시 등록해 보자.

DB 상에서 아래 쿼리를 입력하면 권한을 USER로 수정할 수 있다.

update USER set ROLE = 'USER'

세션에는 이미 처음 접속할 때의 권한인 GUEST로 되어 있으므로, 재접속한 후 게시글 등록을 다시 시도해 보자.

위 사진처럼 글이 잘 등록되는 것을 확인할 수 있다.

POSTS 조회

물론 작성한 글 역시 DB 내에 잘 저장된 것을 확인할 수 있다!

 

*이 글은 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' (프리렉, 이동욱 저)를 공부하며 내용을 정리한 글입니다.

댓글