[스프링부트와 AWS로 혼자 구현하는 웹 서비스] 구글 로그인 연동하기-구현

세이라·2023년 7월 28일
0

스터디를 통해 스프링부트와 AWS로 혼자 구현하는 웹 서비스(저자 이동욱) 서적을 공부하는 중입니다.

공부/실습한 내용을 정리한 포스팅입니다.
책에 모르는 부분이 있으면 구글링하거나 챗gpt에 물어봐서 보충하였습니다.
(아직 초보라 모르는 부분이 많아 이것저것 다 적었습니다.)

참고한 사이트 출처는 포스팅 맨 하단에 적었습니다.

구글 로그인 구현

1. User 클래스 - 사용자 정보 담당

domain 패키지에 user 패키지 생성 후 User 클래스 생성

package com.webservice.springboot.springboot_board.domain.user;

import com.webservice.springboot.springboot_board.domain.posts.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@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();
    }
}
  • @Enumerated(EnumType.STRING)
    : JPA로 DB 저장 시 Enum 값을 어떤 형태로 저장할지 결정. 기본은 int형.
    숫자 저장 시 DB 확인 시 무슨 코드를 의미하는지 모르므로 EnumType.STRING으로 저장.

2. Role Enum - 각 사용자 권한 관리

domain.user 패키지에 Role Enum 생성

package com.webservice.springboot.springboot_board.domain.user;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

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

    private final String key;
    private final String title;
}
  • Spring Security에서는 권한 코드에 ROLE_이 앞에 있어야 함.

3. UserRepository 인터페이스 - User CRUD

domain.user 패키지에 UserRepository 인터페이스 생성

package com.webservice.springboot.springboot_board.domain.user;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User,Long> {
    Optional<User> findByEmail(String email);
}
  • findByEmail : 소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위한 메서드.

4. Spring Security 설정 - build.gradle

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
}
  • spring-boot-starter-oauth2-client : 소셜 로그인 등 Client 입장에서 소셜 기능 구현 시 필요한 의존성이며 spring-security-oauth2-clientspring-security-oauth2-jose 기본으로 관리

spring-security-oauth2-client : OAuth2.0 인증을 간편하게 구현할 수 있도록 도와주는 라이브러리.
spring-security-oauth2-jose : JWT(Json Web Tokens)와 관련 권한을 안전하게 전송하기 위한 JOSE(JavaScript Object Signing And Encryption) 지원 라이브러리. JWT 및 JOSE 기반의 보안 기능을 구현할 수 있음.


5. SecurityConfig 클래스 - 보안설정

cofig.auth 패키지 생성. 앞으로의 Security 관련 클래스를 담음.
패키지 아래에 SecurityConfig 클래스 생성

package com.webservice.springboot.springboot_board.config.auth;

import com.webservice.springboot.springboot_board.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final CustomOAuth2UserService customOAuth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
           .csrf().disable()
           .headers().frameOptions().disable()
           .and()
            .authorizeRequests()
            .antMatchers("/", "/css/**", "/images/**", 						"/js/**", "/h2-console/**").permitAll()
           	.antMatchers("/api/v1/**").hasRole(Role.USER.name())
            .anyRequest().authenticated()
           .and()
             .logout()
             .logoutSuccessUrl("/")
           .and()
             .oauth2Login()
             .userInfoEndpoint()
             .userService(customOAuth2UserService);
    }
}
  • @EnableWebSecurity : Spring Security를 웹 애플리케이션에 적용하기 위한 annotation. Spring Security 웹 보안 기능 활성화하여 웹 애플리케이션에 보안 설정을 쉽게 할 수 있음. 보안 설정 담당하는 클래스에 적용.

  • WebSecurityConfigurerAdapter
    : Spring Security 구성하기 위해 사용되는 클래스.
    이 클래스를 상속 후 메서드를 오버라이딩함으로써 사용자 정의 보안 설정 구성 가능.
    Spring Security 사용하여 웹 애플리케이션 보안을 설정하고자 할 때 WebSecurityConfigurerAdapter를 확장하여 필요한 설정 제공할 수 있음.

  • configure(HttpSecurity http)
    : WebSecurityConfigurerAdapter 메서드.
    HTTP 보안 설정을 구성하는데 사용. 인증, 권한부여, 접근제어, 로그인, 로그아웃 등의 보안 설정을 이곳에서 정의.

  • .csrf().disable().headers().frameOptions().disable()
    : h2-console 화면을 사용하기 위해 해당 옵션 disable 처리.

  • HttpSecurity 클래스
    : Spring Security의 클래스. 보안 규칙 정의, 웹 요청에 대한 접근 제어, 인증, 인가 등을 구성.

  • and()
    : HttpSecurity 클래스에서 여러 개의 설정을 연결하기 위해 사용되는 메서드. HttpSecurity 클래스는 여러 개의 설정을 메서드 체인을 통해 구성. 이때 and() 메서드는 메서드 체인에서 이전 설정과 다음 설정을 연결하는 역할을 함.

  • authorizeRequests()
    : URI별 권한 관리 설정. authorizeRequests가 선언되어야만 antMatchers(),anyRequest() 사용 가능.

  • antMatchers(String... antPatterns)
    : 권한 관리 대상 지정. URL, HTTP 메서드별로 관리 가능.

  • permitAll()
    : 특정 URL 패턴에 대해 인증 없이 접근 허용.

  • hasRole(String role)
    : 특정 역할을 나타내는 문자열. "ROLE_" 접두사 포함 문자열.

  • anyRequest
    : 설정된 값 이외의 나머지 URL에 대해 접근 규칙 지정.

  • authenticated()
    : 인증된 사용자만 접근할 수 있다는 것을 의미. 로그인한 사용자만 허용.

  • logout().logoutSuccessUrl("/")
    : 로그아웃 기능에 대한 여러 설정의 진입점. 로그아웃 성공 시 / 주소로 이동.

  • oauth2Login()
    : OAuth2 로그인 기능에 대한 여러 설정의 진입점.

  • userInfoEndpoint
    : OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당.

  • userService(OAuth2UserService userService)
    : 소셜 로그인 성공 시 후속 조치를 진행할 인터페이스 구현체를 등록.
    리소스 서버에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능 명시.

추가 설명

.csrf().disable()

  • CSRF(Cross-site request forgery)
    : 사이트 간 요청 위조. 인증된 사용자 브라우저를 이용하여 사용자가 의도치 않은 요청을 악의적으로 실행하는 공격.
    이러한 CSRF 공격을 막기 위해 Spring은 동기화 토큰 패턴을 사용.
    세션 쿠키 외에 각 HTTP 요청에 CSRF토큰이라는 보안 무작위 생성값이 있어야 함을 요구. CSRF토큰이 없거나 실제 CSRF 토큰과 일치하지 않다면 요청 거부.
    세션인증 방식(로그인 후 세션id 발급 쿠키에 저장 후 세션을 통해 인증상태 유지)으로는 CSRF 방어 필요. 그 이유는 CSRF 공격자는 사용자의 브라우저에 저장된 세션ID를 이용하여 요청 위조 가능.

  • CSRF 토큰
    : 웹 애플리케이션에서 CSRF 공격으로부터 사용자를 보호하기 위해 사용되는 보안 매커니즘. 사용자 로그인 시 세션에 CSRF 토큰 생성. 랜덤한 문자열이며 사용자의 세션과 연결됨. 그리고 요청에 CSRF 토큰을 포함시킴.(form이나 ajax 요청과 함께 보냄. hidden 필드와 같은 곳에 숨겨놓음) 요청 시 토큰 확인.

.headers().frameOptions().disable()

  • Spring Security는 기본적으로 Click jacking 공격 막기 설정이 되어있음.
  • X-Frame-Options 헤더 설정하여 자신의 웹페이지가 다른 웹페이지에 iFrame 삽입되는 것을 방지.
  • iFrame을 사용하면 에러 발생. h2-console에서 iFrame 사용하므로 disable로 설정.
  • http.headers().frameOptions().sameOrigin() 사용 시 동일 도메인에서 iFrame 접근 가능하도록 설정 가능.
  • iFrame : HTML 문서 안에 다른 HTML 문서를 내장시키는 데 사용하는 HTML 태그. 웹페이지 내에 다른 웹 페이지 삽입.
  • Click jacking 공격 : 공격자가 악성 웹페이지를 iFrame으로 삽입하여 사용자가 의도하지 않은 동작 수행하도록 유도. 보이지 않는 iFrame 위에 있는 버튼이나 링크 클릭할 수도 있음.

6. CustomOAuth2UserService 클래스 - 로그인 성공 시 후속 조치

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

package com.webservice.springboot.springboot_board.config.auth;

import com.webservice.springboot.springboot_board.config.auth.dto.OAuthAttributes;
import com.webservice.springboot.springboot_board.config.auth.dto.SessionUser;
import com.webservice.springboot.springboot_board.domain.user.User;
import com.webservice.springboot.springboot_board.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;
import java.util.Collections;

@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest,OAuth2User> delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
                .getUserInfoEndpoint().getUserNameAttributeName();
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        User user = saveOrUpdate(attributes);
        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),attributes.getAttributes(),
                attributes.getNameAttributeKey());
    }

    private User saveOrUpdate(OAuthAttributes attributes){
        User user = userRepository.findByEmail(attributes.getEmail())
                                    .map(entity->entity.update(attributes.getName(),attributes.getPicture()))
                                    .orElse(attributes.toEntity());
        return userRepository.save(user);
    }
}
  • OAuth2UserService<T extends OAuth2UserRequest, U extends OAuth2User>
    : OAuth 2.0 인증을 통해 인증된 사용자의 정보를 가져오는 데 사용.
    이 인터페이스를 구현하여 사용자 정보를 처리하는 방법을 정의할 수 있음.

  • loadUser(OAuth2UserRequest userRequest)
    : OAuth 2.0 인증 완료된 후에 호출. OAuth2.0 토큰을 이용하여 인증된 사용자의 정보를 받아오고 로드하는 데 사용. userRequest는 OAuth2.0 인증에 대한 요청 정보를 담고 있음. OAuth2User 객체 반환.

  • OAuth2User 인터페이스
    : 사용자 정보를 나타냄. 사용자의 기본 정보, 사용자가 가지고 있는 인가된 권한 정보를 담고 있음. 사용자의 정보는 OAuth2UserAttribute 객체 통해 확인 가능.

  • OAuth2UserAttributes 클래스
    : OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스. Map 형태로 사용자의 속성 정보 제공.

  • DefaultOAuth2UserService
    : Spring Security OAuth 2.0에서 제공하는 구현체 중 하나로, OAuth2UserService 인터페이스 구현 클래스.

  • registrationId
    : OAuth 2.0 클라이언트의 등록 ID를 나타내는 값. 현재 로그인 진행 중인 서비스 구분.
    구글만 사용하면 불필요하지만, 다른 소셜 로그인도 있으면 구분하기 위해 사용해야 함.

  • userNameAttributeName
    : OAuth2 로그인 진행 시 키가 되는 필드값.
    이후 네이버 로그인과 구글 로그인을 동시 지원할 때 사용.

  • HttpSession
    - Spring Security의 경우 세션 기반 인증 지원.
    (세션은 웹 애플리케이션에서 클라이언트와 서버 간의 상태 정보 유지하기 위한 매커니즘)
    인증 성공 시, Spring Security는 HttpSession을 사용하여 세션 생성하고 유지. 세션은 서버에 저장,클라이언트는 서버에서 할당된 세션ID를 쿠키를 통해 전달 받음. 그 후, 클라이언트 요청에 세션ID가 포함되어 서버가 해당 세션을 식별하고 유지.
    - Java Servlet API 제공 인터페이스로 웹 애플리케이션에서 클라이언트와 서버 간의 세션을 관리하기 위해 사용. Spring Security 사용하더라도 HttpSession 사용O
    - setAttribute(String name, Object value)을 사용하여 사용자 정보를 세션에 저장하면 해당 정보가 세션에 유지. 객체를 세션에 저장하면, 해당 객체는 직렬화되어 저장. 그러므로 Serializable 인터페이스 구현하고 있어야 함. Java는 객체 직렬화 매커니즘을 이용하여 바이트 스트림으로 변환. 세션에 저장된 데이터는 서버 측에서 유지되기 때문에 객체의 직렬화와 역직렬화 필요.
    - But, 보안적인 측면에서 적절치 않음. 추가적인 보안 처리가 반드시 필요. 세션에 저장하는 데이터는 서버측에서만 유지. 이진 형태로 저장되기 때문에 데이터를 읽기 어렵고 해석하기 어렵지만, 서버에 접근할 수 있는 공격자가 해당 데이터 탈취하거나 악의적으로 사용할 수도 있음.
    - Spring Security에선 기본적으로 SecurityContext 사용하여 보안과 관련된 정보인 사용자 정보 저장 관리.

※ 여기서 말하는 클라이언트, 서버는 OAuth2 주체가 아니고 일반적으로 말하는 클라이언트와 서버를 뜻함.
※ 직렬화된 데이터를 분석하고 해석할 수 있는 기술과도구 사용하여 데이터 역직렬화가 가능하기 때문에 보안에 취약. 또는 역직렬화하는 과정에서 보안 상 문제가 있으면 공격해서 탈취.

  • SessionUser
    : 세션에 사용자 정보를 저장하기 위한 Dto 클래스.

  • DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey)
    : OAuth2User 인터페이스 구현 클래스.
    authorities는 인증된 권한의 정보. attributes는 OAuth2.0 제공자로부터 받아온 사용자의 기본 정보. nameAttributeKey는 attributes Map에서 사용자의 이름을 나타내는 속성의 키를 나타내는 문자열.
    -SimpleGrantedAuthority(String role)
    : GrantedAuthority 인터페이스를 구현 클래스.
    GrantedAuthority는 Spring Security에서 인증된 사용자의 권한을 나타내는 인터페이스로, 애플리케이션의 보안 기능을 위해 사용됨.
    SimpleGrantedAuthority는 간단한 형태의 권한 정보를 나타내기 위해 사용됨. 예를 들어, "ROLE_USER", "ROLE_ADMIN"과 같은 문자열 형태의 권한 이름을 SimpleGrantedAuthority 객체로 표현할 수 있음.

  • updateOrUpdate
    : 사용자 이름이나 프로필 사진 변경 시 User Entity에 반영.

추가 설명

OAuth2UserRequest

  • OAuth 2.0 서버로부터 사용자 정보를 요청하기 위한 필요한 정보들을 한 번에 다 전달 받음(OAuth 2.0 클라이언트 등록정보, 인증 요청 정보(인증 흐름에 따라 사용되는 정보들 담겨져 있음), 액세스 토큰 등 포함)
OAuth2UserRequestClientRegistration
  • OAuth 2.0 클라이언트의 등록 정보.
  • Client ID/Secret, 인가된 리다이렉트 URI 등
OAuth2UserRequestProviderDetails
  • OAuth2.0 제공자에 대한 세부정보.
  • 인증 처리 시, providerDetail를 통해 제공자에 대한 설정 정보 참조.
ProviderDetailsUserInfoEndpoint
  • OAuth2.0 인증 완료 후 사용자 정보 요청 시 사용되는 endpoint에 대한 정보 제공.
  • userNameAttributeName, uri가 주요 속성이며,
    uri는 사용자 정보 요청하는 URI. Client는 이 URI를 통해 사용자 정보 요청.
    userNameAttributeName는 OAuth2.0 제공자에서 제공하는 사용자 정보 중에서 사용자 이름을 나타내는 속성의 이름. Google의 경우 "sub" 속성.

7. OAuthAttributes - OAuth User의 Attribute 담음

config.auth.dto 패키지 생성 후 OAuthAttributes 클래스 생성

package com.webservice.springboot.springboot_board.config.auth.dto;

import com.webservice.springboot.springboot_board.domain.user.Role;
import com.webservice.springboot.springboot_board.domain.user.User;
import lombok.Builder;
import lombok.Getter;

import java.util.Map;

@Getter
public class OAuthAttributes {
    private Map<String,Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;

    @Builder
    public OAuthAttributes(Map<String,Object> attributes, String nameAttributeKey, String name, String email, String picture){
        this.attributes=attributes;
        this.nameAttributeKey=nameAttributeKey;
        this.name=name;
        this.email=email;
        this.picture=picture;
    }

    public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String,Object> attributes){
        return ofGoogle(userNameAttributeName,attributes);
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String,Object> attributes){
        return OAuthAttributes.builder()
                .name((String) attributes.get("name"))
                .email((String)attributes.get("email"))
                .picture((String)attributes.get("picture"))
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    public User toEntity(){
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }
}
  • of()
    : OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 반환해야 함.
  • toEntity()
    : User Entity 생성. OAuthAttributs에서 Entity 생성 시점은 처음 가입할 때. 가입할 때의 기본권한을 GUEST로 주기 위해 role 빌더값은 Role.GUEST.

8. SessionUser 클래스 - HttpSession에 담을 정보 틀

config.auth.dto 패키지에 생성.

package com.webservice.springboot.springboot_board.config.auth.dto;

import com.webservice.springboot.springboot_board.domain.user.User;
import lombok.Getter;

import java.io.Serializable;

@Getter
public class SessionUser implements Serializable {
    private String name;
    private String email;
    private String picture;
    public SessionUser(User user) {
        this.name = user.getName();
        this.email = user.getEmail();
        this.picture = user.getPicture();
    }
}
  • User 클래스를 사용하면 안되는 이유
    : 직렬화 구현하지 않아서. HttpSession에 넣으려면 직렬화가 구현되어 있어야 함. 그리고 User 클래스의 경우 Entity라 직렬화를 구현하면 안됨.
    그 이유는 Entity의 경우 다른 Entity와 관계를 맺어 @OneToMany, @ManyToMany 등으로 인하여 자식 Entity를 갖고 있게 된다면,
    직렬화 대상에 자식들까지 포함되어 성능 이슈, 부수 효과가 발생 확률이 높음.
    그래서 따로 SessionUser 클래스 생성.

참고 설명

endpoint

  • 네트워크 서비스에 접근하는 지점. 웹 애플리케이션 또는 서비스에서 클라이언트나 다른 서버와 통신하기 위한 URL을 특정 지점으로 지정하여 해당 서비스의 기능이나 리소스 접근.

Collections.singleton(객체 생성자)

  • 단 한개의 객체만 저장 가능한 컬렉션 만들고 싶을 때 사용
  • 매개변수로 넣고 싶은 객체 생성자 넣으면 됨.

출처

스프링부트 - #5 oAuth2
csrf 맘대로 꺼도될까
[Spring] Spring Security에서 'X-Frame-Options' 응답 헤더 설정
컬렉션 - Collections 클래스와 메서드
챗GPT 활용

0개의 댓글