춤추는 개발자

[Spring boot] 소셜 로그인 (Spring Security와 OAuth2.0) 본문

Developer's_til/스프링 프레임워크

[Spring boot] 소셜 로그인 (Spring Security와 OAuth2.0)

Heon_9u 2021. 7. 27. 13:10
728x90
반응형

✅ Spring Security란?

 스프링 기반 어플리케이션의 보안을 위한 표준으로 막강한 인증인가 기능을 가진 프레임워크를 말합니다.

주로 서블릿 필터와 필터체인으로 구성된 위임모델을 사용합니다. 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있습니다. 또한, 확장성을 고려한 프레임워크로 다양한 요구사항을 쉽게 추가 및 변경할 수 있습니다.

 

보안용어

Spring Security 레퍼런스나 기술 블로그를 보면, 전문 용어가 많습니다. 빠른 이해를 위해 아래 용어들을 한번 쯤은 익히고 가는 것을 추천드립니다.

  • 접근 주체(principal): 보호된 리소스에 접근하는 대상.
  • 인증(Authentication): 보호된 리소스에 접근한 대상에 대해 이 유저가 누구인지, 어플리케이션의 작업을 수행해도 되는 주체인지 확인하는 과정.
  • 인가(Authorize): 해당 리소스에 대해 접근 가능한 권한을 가지고 있는지 확인하는 과정.
  • 권한: 특정 리소스에 대한 접근 제한, 모든 리소스는 접근 제어 권한이 걸려있다. 즉, 인가 과정에서 해당 리소스에 대해 제한된 최소한의 권한을 가졌는지 확인.

 

✅ Spring Security의 특징과 구조

Spring Security는 다양한 필터들이 있지만, OAuth2.0에 필요한 기능들만 적용하여 소셜 로그인을 구현했습니다. 추가적인 설명들은 Reference를 참고하시기 바랍니다.

 

  • Filter 기반으로 동작하여 MVC와 분리하여 관리 및 동작
  • Annotation을 통한 간단한 설정
  • 기본적으로 세션 & 쿠키방식으로 인증
  • 인증관리자와 접근 결정 관리자를 통해 사용자의 접근을 관리

 

✅ Google 서비스 등록

 가장 먼저, 구글 서비스에서 인증 정보를 발급받아 소셜 서비스 기능을 사용합니다.

https://console.cloud.google.com/

 

Google Cloud Platform

하나의 계정으로 모든 Google 서비스를 Google Cloud Platform을 사용하려면 로그인하세요.

accounts.google.com

 

1. [새 프로젝트]를 생성합니다.

2. 생성이 완료된 프로젝트를 선택하고 왼쪽 메뉴 탭의 [API 및 서비스] -> [사용자 인증 정보]로 들어갑니다.

3. [사용자 인증 정보 만들기]의 여러 메뉴 중 [OAuth 클라이언트 ID]를 클릭합니다.

4. 안내에 따라 [동의 화면 구성]을 작성하며, [Google API 범위]는 기본 값인 email/profile/openid를 사용합니다.

5. 다음은 아래 사진에 따라 구성합니다.

 

 

 승인된 리디렉션 URI란 서비스에서 파라미터로 인증 정보를 주었을 때, 인증이 성공하면 구글에서 리다이렉트할 URL을 의미합니다. Spring boot 2 버전의 Security에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL을 지원하고 있습니다.

 

✅ Google 로그인 연동하기

 앞서 생성된 OAuth 클라이언트 ID를 프로젝트에 설정합니다.

프로젝트로 돌아와 src/main/resources/ 디렉토리에 application-oauth.properties 파일을 생성하여 아래와 같이 작성합니다.

spring.security.oauth2.client.registration.google.client-id=클라이언트ID
spring.security.oauth2.client.registration.google.client-secret=클라이언트 보안 비밀번호
spring.security.oauth2.client.registration.google.scope=profile,email

 

 

이제부터 사용자 정보를 담당할 User 클래스를 생성합니다. domain 아래에 user 패키지를 생성하여 Entity 역할을 할 User와 각 사용자의 권한을 관리한 Enum 클래스인 Role을 생성합니다.

 

@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder
    public User(String name, String email, String picture, Role role) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }

    public User update(String name, String picture) {
        this.name = name;
        this.picture = picture;

        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}

 

@Getter
@RequiredArgsConstructor
public enum Role {
    GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER", "일반 사용자");

    private final String key;
    private final String title;
}

 

Spring Security에서는 권한 코드에 항상 ROLE_이 앞에 있어야만 합니다. 그래서 ROLE_GUEST, ROLE_USER 등으로 권한을 지정하여 특정 기능에 대해 차단하거나 허락할 수 있습니다.

 

다음으로 User의 CRUD를 책임질 Repository를 생성합니다.

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

 

이제 build.gradle에 Spring Security와 관련된 의존성 하나를 추가합니다.

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

 

이후, config.auth 패키지를 생성하여 Security 관련 클래스를 모두 작성합니다.

(실제 코드양이 너무 많기 때문에 github 코드를 참고하시기 바랍니다.)

  • SecurityConfig
  • CustomOAuth2UserService
  • OAuthAttributes
  • SessionUser

SecurityConfig

 Spring Security 설정들을 활성화시키며, URL별로 권한 관리를 설정합니다. authorizeRequests와 antMatchers 옵션을 사용하여 권한 관리 대상을 지정합니다. 이외에도 로그아웃 기능에 대한 여러 설정을 진행합니다.

 

CustomOAuth2UserService

 구글 로그인 이후, 가져온 사용자의 정보들을 기반으로 가입 및 정보 수정, 세션 저장 등의 기능을 지원합니다.

 

OAuthAttributes

 OAuth2User의 attribute를 담을 Dto 클래스입니다. OAuth2User에서 반환되는 사용자 정보는 Map이기 때문에 값 하나하나를 변환하여 저장합니다.

 

SessionUser

 세션에 사용자 정보를 저장하기 위한 Dto 클래스입니다. 인증된 사용자 정보만 저장합니다.

 

🛠 각 코드의 자세한 사항은 교재를 참고하세요!

 

📢여기서 잠깐!

 사용자 정보를 담는 User 클래스가 있는데, 왜 세션에 저장하기 위한 클래스인 SessionUser를 생성할까요?

이유는 직렬화 때문입니다.

 

User 클래스를 세션에 저장하려고 하면 직렬화를 구현하지 않았다는 의미의 에러가 발생합니다. 물론, User 클래스에 직렬화 코드를 넣으면 해결할 수 있습니다. 하지만, User 클래스는 Entity입니다. 이전 포스팅에서도 Posts 클래스의 Entity/request/reponse용 Dto를 생성하며 사용했습니다.

 

 User 클래스도 마찬가지로 Entity이기 때문에 Database와 가장 밀접한 Dto입니다. 또한, 다른 Entity와 1:N, N:M 관계가 형성될 수 있습니다. 만약 자식 Entity를 갖고 있는 경우, 직렬화 대상에 포함되기 때문에 성능 이슈, 추가 작업 등이 발생할 수 있습니다.

 이러한 문제를 사전에 방지하기 위해 직렬화 기능을 가진 SessionUser를 추가로 만드는 것입니다.

 

 

 이제 테스트를 위해 index.mustache에 로그인 버튼을 추가합니다. 그리고 index.mustache에서 loginName을 사용할 수 있게 IndexController까지 생성합니다.

 교재에서 4장 내용인 [Mustache로 화면 구성하기]를 스킵했습니다. 관련된 사항은 Github의 코드를 붙여넣기에서 사용하시거나 직접 교재를 구입하시는 것을 추천드립니다.

 

 

✅ Annotation으로 같은 코드 개선하기

 현재 Github 코드에서는 이미 LoginUser라는 Annotation으로 구현했습니다. 기존 교재를 따라 갔다면 IndexController에서 세션값을 가져오는 부분이 아래와 같이 작성되었을 것입니다.

SessionUser user = (SessionUser) httpSession.getAttributes("user");

 

index 메서드 외에 다른 컨트롤러와 메소드에서 세션값이 필요할 때면, 위 코드가 반복될 수밖에 없습니다. 그래서 이 부분을 메서드 인자로 세션값을 바로 받을 수 있게 변경합니다.

 

config.auth 패키지에 아래와 같은 Annotation 및 클래스를 생성합니다.

  • @LoginUser(@interface)
  • LoginUserArgumentResolver

LoginUserArgumentResolver는 HandlerMethodArgumentResolver 인터페이스를 구현한 클래스입니다. 조건에 맞는 경우 메서드가 있다면 HandlerMethodArgumentResolver의 구현체가 지정한 값으로 해당 메서드의 파라미터로 넘길 수 있습니다.

 

위에 작성한 코드들이 스프링에서 인식될 수 있도록 WebConfig 클래스를 생성합니다.

HandlerMethodArgumentResolver는 항상 WebMvcConfigurer의 addArgumentResolvers()를 통해 추가해야 합니다.

 

모든 설정이 끝났습니다. 처음 언급한대로 IndexController의 코드를 @LoginUser로 개선하기 전과 후 코드를 비교해보겠습니다.

 

<기존 코드>

@GetMapping("/")
public String index(Model model) {
    model.addAttribute("posts", postsService.findAllDesc());
    SessionUser user = (SessionUser) httpSession.getAttribute("user");

    if(user != null) {
        model.addAttribute("loginName", user.getName());
    }

    return "index";
}

 

<@LoginUser를 적용한 코드>

@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user) {
    model.addAttribute("posts", postsService.findAllDesc());

    if(user != null) {
        model.addAttribute("loginName", user.getName());
    }

    return "index";
}

 

 

✅ 세션 저장소로 Database 사용하기

 지금까지 소셜 로그인 기능을 완성하였습니다. 하지만, 어플리케이션을 재실행할 때마다 로그인이 풀립니다. 이런 현상은 세션이 내장 톰캣의 메모리에 저장되기 때문입니다.

 기본적으로 세션은 WAS의 메모리에 저장되고 호출됩니다. 이로 인해 내장 톰캣처럼 어플리케이션 실행 시, 항상 초기화가 이루어집니다. 즉, 배포할 때마다 톰캣이 재시작되는 것입니다.

 

 이외에도 2대 이상의 서버에서 서비스하는 경우, 톰캣마다 세션 동기화 설정을 해야합니다. 그래서 실제 현업에서는 세션 저장소에 대해 [톰캣 세션], [MySQL과 같은 DB], [Redis, Memcached와 같은 메모리 DB]를 사용합니다.

 

 여기서는 설정이 간단하고, 비용 절감을 위해 DB를 세션 저장소로 사용해보겠습니다.

 

먼저, build.gradle에 아래 의존성을 등록합니다.

compile('org.springframework.session:spring-session-jdbc')

 

그리고 application.properties에 세션 저장소를 jdbc로 설정하도록 코드를 추가합니다.

spring.session.store-type=jdbc

 

끝입니다! JPA로 인해 세션 테이블이 자동 생성되기 때문에 별도로 해야할 일은 없습니다. 물론, 지금은 스프링을 재시작하면 동시에 재시작되는 H2를 사용하기 때문에 로그인이 풀릴 것입니다.

 하지만, AWS로 배포하고 AWS의 RDS를 사용하게 되면, 로그인이 풀리지 않을 것입니다.

 

🙋‍♂️ 느낀점

 예전 프로젝트를 진행할 때, 로그인과 관련된 인증 절차나 보안적인 부분을 구현하는데 많은 시간이 들어갔다.(물론, 완벽하게 구현하지 못했지만..) 하지만!! Spring Security라는 효율적인 기술이 있었다니.. 역시 아는게 힘이다😂

 처음에 언급한 것 외에도 Spring Security에는 온갖 Filter들이 존재한다. 전부 활용하기엔 부담스러워서 전체적인 구조나 자료만 참고해야 겠다.

 

 이전 포스팅에서도 느꼈지만, 이번에도 역시 Annotation을 직접 만들며 코드를 간결하게 만들기 위한 과정이 들어갔다. 또한, 어플리케이션을 재배포하며 로그인이 풀리는 문제를 [세션과 Databaes]라는 개념을 통해 해결했다. 특정 문제를 해결하기 위해 다양한 방법이 존재하지만, 각 방법의 장단점과 내 프로젝트의 상황을 비교하며 적용해야함을 알 수 있었다. 당연한 얘기지만, 알고리즘 문제만 풀며 해결하다보니 톰캣이나 Database, 메모리와 관련된 특징들에 대해서 잘 모르는게 사실이다. 종류가 너무 많아서 완벽하게 알기 힘들지만, 현재 진행하는 프로젝트 or 내 업무에서 사용하는 기술 & Tool은 90%이상 알고있어야 겠다.

 

✅ Reference

https://devuna.tistory.com/55

https://coding-start.tistory.com/153

 

 

 

728x90
반응형