프로젝트/스프링 부트와 AWS로 혼자 구현하는 웹 서비스

구글 소셜로그인 구현 - 2

SeoburiFaust 2022. 10. 3. 15:27

프로젝트 구현

 

user 패키지 생성(import 생략)

@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로 데이터베이스로 저장할 때 Enum값을 어떤 형태로 저장할 지를 결정한다.

 - 기본적으로는 int로 된 숫자가 저장된다.

 - 숫자로 저장되면 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는 지가 알 수 없다.

 - 그래서 문자열(EnumType.STRING)로 저장될 수 있도록 선언

 

 

사용자의 권한을 관리할 Enum 클래스의 Role생성

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

    private final String key;
    private final String title;
}

선언한 순서대로 Key가 첫번 째 element, title이 두번 째 element를 의미한다.

 

enum을 사용하는 이유

https://bcp0109.tistory.com/334

 

UserRepository생성

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);
}

 

build.gradle에 security 의존성을 추가

 

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

 

SecurityConfig를 생성

 

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig{

    private final CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain securityFilterChain(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); //소셜로그인 성공시 후속조치를 시행할 UserService 인터페이스를 등록
        return http.build();
    }
}

 

@EnableWebsecurity

 - Spring Security 설정들을 활성화시켜 줍니다.

csrf()

 - Cross Site Request Forgery(사이트간  위조 요청)

 - 서버에 인증정보를 저장하지 않기 때문에 굳이 불필요한 csrf 코드들을 작성할 필요가 없다.

 - 자세한 설명은 다음 링크 (https://velog.io/@woohobi/Spring-security-csrf%EB%9E%80)

authorizeRequests

 - URL별 권한 관리를 설정하는 옵션의 시작점이다.

 - authorizeRequest가 선언되어야만 antMatchers옵션을 사용할 수 있다.

antMatchers

 - 권한 관리 대상을 지정하는 옵션이다.

 - URL, HTTP 메소드별로 관리가 가능하다.

 - "/"등 지정된 URL등은 permitAll()옵션을 통해 전체 열람 권한을 주었다.

 - POST메소드이면서 "/api/v1/**"주소를 가진 API는 USER권한을 가진 사람만 가능하도록 했다.

anyRequest

 - 설정된 값들 이외 나머지 URL들을 나타낸다.

 - 여기서는 authenticated()를 추가하여 나머지 URL들은 모두 인증된 사용자들에게만 허용하게 한다.

 - 인증된 사용자 즉, 로그인한 사용자들을 이야기 한다.

logout().logoutsuccessUrl("/")

 - 로그아웃 기능에 대한 여러 설정의 진입점이다.

 - 로그아웃 성공시 / 주소로 이동한다.

oauth2Login

 - OAuth2 로그인 기능에 대한 여러설정의 진입점이다.

userInfoEndpoint

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

userService

 - 소셜로그인 성공 시 후속 조치를 진행할 Userservice 인터페이스의 구현체를 등록한다.

 - 리소스 서버(즉, 소셜 서비스들)에서 사용자 정보를 가져온상태에서 추가로 진행하고자 하는 기능을 명시할 수 있다.

 - 여기서는 CustomOAuth2UserService를 등록한다.(CustomOAth2UserService는 OAuth2UserService의 구현체다.)

 

CustomOAuth2UserService

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

@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 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);
    }
}

1. DefaultOAuth2UserService를 delegate라는 이름으로 인스턴스화 한다. 

2. delegate.loadUser(userRequest)의 값을 Oauth2User의 인스턴스인 oauth2User에 담아준다.

 

registrationId

 - 현재 로그인 진행중인 서비스를 구분하는 코드(네이버인지 구글인지)

userNameAttributeName

 - OAuth2로그인 진행 시 키가 되는 필드값을 이야기한다. Primary key와 같은 의미

 - 구글의 경우 이 값은 "sub"인데 네이버와 카카오는 지원하지 않는다. 그래서 직접 userNameAttributeName을 찾아서 그 값을 알아내야한다.

OAuthAttributes

 - OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스

 - 모든 소셜로그인의 attribute가 여기 담긴다.

SessionUser

 - 세션에 사용자 정보를 저장하기 위한 Dto클래스

 

※user를 직렬화하면 안된다. Entity 클래스는 언제 다른 Entity와 관계가 형성될 지 모르기 때문에 직렬화를 하면 안된다. 그래서 직렬화 기능을 가진 세션 Dto를 하나 추가로 만드는 것이 이후 운영 및 유지보수 때 많은 도움이 된다.

OAuthAttributes

@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);
    }

    public 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.USER)
                .build();
    }
}

 

of()

 - OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야만 한다.

toEntity()

 - User 엔티티를 생성한다.

 - OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입할 때다.

 - 가입할 때 기본 권한을 USER로 줬다.

SessionUser

@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();
    }
}

 

인증된 사용자 정보만 필요 name, email, picture만 필드로 선언.

 

index.mustache에 로그인 버튼의 링크에는 "/oauth2/authorization/google"을 심어준다.

스프링 시큐리티에서 기본으로 제공하는 로그인 URL이다. 별도의 컨트롤러를 생성할 필요가 없다.

 

IndexController

@Controller
@RequiredArgsConstructor
public class IndexController {

    private final HttpSession httpSession;
    @GetMapping("/")
    public String index(Model model) {
        SessionUser user = (SessionUser) httpSession.getAttribute("user");
        if (user != null) {
            model.addAttribute("userName", user.getName());
        }
        return "index";
    }
}

Controller에서는 SessionUser의 인스턴스에 httpSession에 있는 user정보를 꺼내 넣어준다. model에 user로 부터 유저정보를 addAttribute로 넣어주면 이제 프론트엔드에서 이것을 사용할 수 있다.