이 글은 책 「스프링 부트와 AWS로 혼자 구현하는 웹 서비스」를 공부하고 정리한 글입니다.
오늘은 네이버 로그인 기능을 추가해보자.
🔗 네이버 오픈 API 접속
서비스 URL
은 필수이다. Callback URL
은 구글에서 등록한 리디렉션 URL과 같은 역할을 한다. 등록을 완료하면 ClientID
와 ClientSecret
이 생성된다.
Client ID
와 Client Secret
정보를 등록해야 한다. 네이버에서는 스프링 시큐리티를 공식 지원하지 않기 때문에 그동안 CommonOAuth2Provider
에서 해주던 값들도 전부 수동으로 입력해야 한다.
# registration
spring.security.oauth2.client.registration.naver.client-id=클라이언트ID
spring.security.oauth2.client.registration.naver.client-secret=클라이언트SECRET
spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/{action}/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email,profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver
# provider
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response
user_name_attribute=response
user_name
의 이름을 네이버에서는 response
로 해야 한다.response
를 user_name
으로 지정하고 이후 자바 코드로 response의 id를 user_name으로 지정하겠다.public class OAuthAttributes {
...
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
if ("naver".equals(registrationId)) {
return ofNaver("id", attributes);
}
return ofGoogle(userNameAttributeName, attributes);
}
...
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return OAuthAttributes.builder()
.name((String) response.get("name"))
.email((String) response.get("email"))
.picture((String) response.get("profile_image"))
.attributes(response)
.nameAttributeKey(userNameAttributeName)
.build();
}
...
}
네이버인지 판단하는 코드와 네이버 생성자를 추가하면 된다.
{{^userName}}
<a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
<a href="/oauth2/authorization/naver" class="btn btn-success active" role="button">Naver Login</a>
{{/userName}}
/oauth2/authorization/naver
application-oauth.properties
에 등록한 redirect-uri
값에 맞춰 자동으로 등록된다./oauth2/authorization/
까지는 고정이고 마지막 Path만 각 소셜 로그인 코드를 사용하면 된다.로그인 성공!
기존 테스트에 시큐리티 적용으로 문제가 되는 부분을 해결해보자.
기존에는 바로 API를 호출할 수 있어서 테스트 코드 역시 바로 API를 호출하도록 구성했다. 하지만 시큐리티 옵션이 활성화되면 인증된 사용자만 API를 호출할 수 있다. 기존의 API 테스트 코드들이 모두 인증에 대한 권한을 받지 못하였으므로, 테스트 코드마다 인증한 사용자가 호출한 것처럼 작동하도록 수정해야 한다.
문제점을 하나씩 살펴보면서 해결해보자.
첫 번째 실패 테스트인 hello가_리턴된다
의 메시지를 보면 다음과 같은 에러를 발견할 수 있다.
No qualifying bean of type 'com.yeonju.book.config.auth.CustomOAuth2UserService'
CustomOAuth2UserService
를 생성하는데 필요한 소셜 로그인 관련 설정값들이 없기 때문에 발생한다.
application-oauth.properties
에 분명히 설정값들을 추가했는데 이러한 이유가 발생한 이유는 src/main
과 src/test
은 본인만의 환경 구성을 가지기 때문이다. test
가 자동으로 가져오는 옵션의 범위는 application.properties
까지로, application-oauth.properties
는 test
에 없다고 가져오는 파일이 아니라서 에러가 발생하는 것이다.
이 문제를 해결하기 위해 테스트 환경을 위한 application.properties
를 만들어 보자. 실제로 구글 연동까지 진행할 것은 아니므로 가짜 설정값을 등록한다.
src/test/resources/application.properteis
spring.jpa.show_sql=true
spring.h2.console.enabled=true
spring.session.store-type=jdbc
# Test OAuth
spring.security.oauth2.client.registration.google.client-id=test
spring.security.oauth2.client.registration.google.client-secret=test
spring.security.oauth2.client.registration.google.scope=profile,email
두 번째 실패 테스트인 Post_등록()
을 보면 응답의 결과로 200
을 기대했는데 302(리다이렉션 응답)
이 와서 실패한 것을 알 수 있다. 이는 스프링 시큐리티 설정 때문에 인증되지 않은 사용자의 요청은 이동시키기 때문이다. 그래서 이런 API 요청은 임의로 인증된 사용자를 추가하여 API만 테스트해 볼 수 있도록 하여 해결해보자.
스프링 시큐리티 테스트를 위한 여러 도구를 지원하는 spring-security-test
를 build.gradle
에 추가한다.
testImplementation 'org.springframework.security:spring-security-test:5.6.2'
PostApiControllerTest
의 2개의 테스트 메소드에 임의 사용자 인증을 추가해보자.
@Test
@WithMockUser(roles = "USER")
public void Post_등록() throws Exception {
...
@Test
@WithMockUser(roles = "USER")
public void Post_수정() throws Exception {
@WithMockUser(roles="USER")
roles
에 권한을 추가할 수 있다.ROLE_UER
권한을 가진 사용자가 API를 요청하는 것과 동일한 효과를 가지게 된다.이렇게 해도 @WithMockUser
가 MockMvc에서만 작동하기 때문에 아직 테스트가 작동하지 않는다. 현재 PostApiController
는 MockMvc를 전혀 사용하지 않는다. @SpringBootTest
에서 MockMvc를 사용하도록 코드를 다음과 같이 변경해보자.
public class PostApiControllerTest {
...
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
...
@Test
@WithMockUser(roles = "USER")
public void Post_등록() throws Exception {
...
//when
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(new ObjectMapper().writeValueAsString(requestDto)))
.andExpect(status().isOk());
//then
List<Post> all = postRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
@Test
@WithMockUser(roles = "USER")
public void Post_수정() throws Exception {
...
//when
mvc.perform(put(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(new ObjectMapper().writeValueAsString(requestDto)))
.andExpect(status().isOk());
//then
List<Post> all = postRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
@Before
mvc.perform
ObjectMapper
를 통해 문자열 JSON으로 변환한다.전체 테스트를 다시 수행해보면 다음과 같이 Post
테스트가 정상적으로 수행되는 것을 볼 수 있다.
@WebMvcTest
는 CustomOAuth2UserService
를 스캔하지 않기 때문에 발생하는 에러이다. **스캔 대상에서 SecurityConfig`를 제거하여 해결해보자.
HelloControllerTest.java
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)
})
class HelloControllerTest { ... }
@WithMockUser
를 사용해서 가짜로 인증된 사용자도 생성하자.
@WithMockUser(roles = "USER")
@Test
public void hello가_리턴된다() throws Exception { ... }
@WithMockUser(roles = "USER")
@Test
public void helloDto가_리턴된다() throws Exception { ... }
테스트를 돌려보면 추가 에러가 발생한다.
java.lang.IllegalArgumentException: At least one JPA metamodel must be present!
@EnableJpaAuditing
때문에 발생한다. @EnableJpaAuditing
을 사용하기 위해서는 최소 하나의 @Entity
클래스가 필요한데 @WebMvcTest
이다 보니 당연히 없다. 그래서 @EnableJpaAuditing
과 @SpringBootApplucation
둘을 분리해보자.
Application.java
//@EnableJpaAuditing 제거
@SpringBootApplication
public class BookApplication { ... }
config/JpaConfig.java
@Configuration
@EnableJpaAuditing //JPA Auditing 활성화
public class JpaConfig {
}
이제 전체 테스트를 수행하면 모든 테스트가 통과한다!
다음 시간에는 AWS를 이용하여 서비스를 배포하고 운영하는 과정을 진행해보자!