Blog

[Spring-LDW] 스프링 시큐리티 구글로그인 기능 구현

Category
Author
citeFred
citeFred
PinOnMain
1 more property
스프링 부트와 AWS로 혼자 구현하는 웹 서비스  : 인텔리제이, JPA, JUnit 테스트, 그레이들, 소셜 로그인, AWS 인프라로 무중단 배포까지 이동욱 저
Table of Content

버전 변경

start.spring.io 에서 자동 생성한 스프링 부트 3.2.1(최신) 버전에 맞추어 의존성이 추가되었던 프로젝트 구조는 여러 레퍼런스들과의 호환성이 맞지 않아서 구현에 어려움을 겪었다. 이를 위하여 명시적으로 Spring Boot 2.6.1 / Spring Security 5.6.1 로 다운그레이드하여 전체적인 코드를 리팩토링했다. 결과적으로 이전에 시도했던 복잡한 코드 레퍼런스들이 정상적으로 작동하지 않고 필요 이상의 시간을 소요하던 로그인 구현의 기본 틀을 쉽게 따라 구현 할 수 있었다.
다음은 최초 기본 구조이다. 다음 글에서 곧바로 각종 어노테이션을 활용한 방법으로 리팩토링하여 보다 깔끔한 코드로 정리할 것이지만 기본적인 구조를 우선 기록해두려고 한다.

build.gradle

의존성 버전을 변경한 현재 상태의 build.gradle은 다음과 같다. Spring Boot 2.6.1 에 맞도록 나머지 의존성들은 버전을 명시하지 않았기 때문에 부트 버전에 호환되는 버전들로 자동 추가된다.
plugins { id 'java' id 'org.springframework.boot' version '2.6.1' id 'io.spring.dependency-management' version '1.0.11.RELEASE' } group = 'com.citefred' version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '17' } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' // JPA implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // in-memory H2 DB for testing implementation 'com.h2database:h2' // Mustache implementation 'org.springframework.boot:spring-boot-starter-mustache' // OAuth 2.0 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { useJUnitPlatform() }
Java
복사

application-oauth.properties

Google에서 앱을 생성하고 받게 되는 Client ID, Client Secret 키를 보관해두어야 한다. 이 파일은 .gitignore를 통해 원격저장소에 노출 되지 않도록 하는것이 좋다.
spring.security.oauth2.client.registration.google.client-id=772 ... googleusercontent.com spring.security.oauth2.client.registration.google.client-secret=G...X-k-r...x-hGL....OL1 spring.security.oauth2.client.registration.google.scope=profile,email
Java
복사
위 파일은 컴파일러가 properties 파일(설정파일) 임을 인식 할 수 있도록 등록해주어야 한다. Boot에서 기본적으로 등록되는 파일인 application.properties 에 다음과 같이 위 파일도 설정 파일임을 등록해준다.
spring.profiles.include=oauth
Java
복사
위 파일들은 OAuth2ClientProperties.java를 통해 Registration 클래스의 필드로 입력되어 자동으로 사용 할 수 있게 된다.

SecurityConfig.java

스프링 시큐리티의 설정 기본 파일이다. 시큐리티 6.x 이상 버전이 FilterChain으로 구성된것과 달리 HttpSecurity에서 체인 메소드 방식으로 모두 연결하여 구현하는 방식으로 되어있는 점이 다른것으로 보여진다. 또한 WebSecurityConfigurerAdapter 클래스를 사용 할 수 있다. antMatchers 메소드도 사용이 되는 것이 확인되었다.
package com.citefred.ldwspring.config.auth; import com.citefred.ldwspring.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/**", "/profile").permitAll() .antMatchers("/api/v1/**").hasRole(Role.USER.name()) .anyRequest().authenticated() .and() .logout() .logoutSuccessUrl("/") .and() .oauth2Login() .userInfoEndpoint() .userService(customOAuth2UserService); } }
Java
복사

CustomOAuth2UserService.java

이 클래스는 구글 로그인 이후 가져온 사용자의 정보를 기반으로 가입, 정보수정, 세션 저장 등의 기능을 구현하는 로직이다.
package com.citefred.ldwspring.config.auth; import com.citefred.ldwspring.config.auth.dto.OAuthAttributes; import com.citefred.ldwspring.config.auth.dto.SessionUser; import com.citefred.ldwspring.domain.user.User; import com.citefred.ldwspring.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 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); } }
Java
복사

registrationId

현재 로그인 서비스를 구분하는 코드(추후 네이버, 카카오 등 추가되면 로그인 방식을 결정하는데 사용)

userNameAttributeName

OAuth2 fㅗ그인 징행 시 키가 되는 필드값, PK와 같은 의미
구글의 경우 기본적으로 코드를 지원하지만, 네이버 카카오 등은 지원하지 않는다. 구글의 기본 코드는 “sub”이다.

SessionUser

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

OAuthAttributes.java

OAuth2UserService 를 통해 가져온 OAuth2User의 attribute를 담을 클래스
package com.citefred.ldwspring.config.auth.dto; import com.citefred.ldwspring.domain.user.Role; import com.citefred.ldwspring.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(); } }
Java
복사

OAuthAttributes of( … )

OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 반환해야 함

toEntity()

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

.role(Role.GUEST)

기본 권한은 ROLE.GUEST로 지정

SessionUser.java

인증된 사용자 정보를 반환할 필드들 설정하는 Dto
package com.citefred.ldwspring.config.auth.dto; import com.citefred.ldwspring.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(); } }
Java
복사

구글 로그인 작동 테스트

Search