스프링 부트와 AWS로 혼자 구현하는 웹 서비스 를 공부하고 정리한 내용입니다.
서비스 URL
은 필수이다.Callback URL
은 구글에서 등록한 리디렉션 URL과 같은 역할을 한다.여기서는 http://localhost:8080/login/oauth2/code/naver
등록을 완료하면 ClientID
와 ClientSecret
이 생성된다.
✔ application-oauth.yml
Client ID
와 Client Secret
정보를 등록해야 한다. 네이버에서는 스프링 시큐리티를 공식 지원하지 않기 때문에 그동안 CommonOAuth2Provider
에서 해주던 값들도 전부 수동으로 입력해야 한다.
spring:
security:
oauth2:
client:
registration:
~
naver:
clientId: xxxx
clientSecret: xxxx
redirect-uri: {baseUrl}/{action}/oauth2/code/{registrationId} # http://localhost:8080/login/oauth2/code/naver
authorization-grant-type: authorization_code
scope: name, email, profile_image
client-name: Naver
provider:
naver:
authorization_uri: https://nid.naver.com/oauth2.0/authorize
token_uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user_name_attribute: response
user_name_attribute: response
user_name
의 이름을 네이버에서는 response
로 해야 한다.resultCode
, message
, response
이다.response
를 user_name
으로 지정하고 이후 자바 코드로 response의 id를 user_name으로 지정할 것이다. redirect-uri: baseUrl
Callback URL
을 입력한다.
이미 구글 로그인을 등록하면서 대부분 코드가 확장성 있게 작성되었다보니, 네이버는 쉽게 등록 가능하다.
OAuthAttributes
네이버인지 판단하는 코드와 네이버 생성자만 추가해주면 된다.
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();
}
...
}
index.mustache
네이버 로그인 버튼을 추가한다.
~
{{^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만 각 소셜 로그인 코드를 사용하면 된다.
실행 결과
APP 이름
나오며 회원 이름, 이메일, 프로필 사진 동의 관련 화면이 나온다. (사진 캡쳐를 하지 못했다.)
기존 테스트에서 시큐리 적용으로 문제가 되는 부분을 해결해보자
기존에는 바로 API를 호출할 수 있어서 테스트 코드 역시 바로 API를 호출하도록 구성했다. 하지만 시큐리티 옵션이 활성화되면 인증된 사용자만 API를 호출할 수 있다. 기존의 API 테스트 코드들이 모두 인증에 대한 권한을 받지 못하였으므로, 테스트 코드마다 인증한 사용자가 호출한 것처럼 작동하도록 수정해야 한다.
문제점을 하나씩 살펴보면서 해결해보자.
✔ 전체 테스트 실행
첫 번째 실패 테스트인 hello가_리턴된다
의 메시지를 보면 다음과 같은 에러를 발견할 수 있다.
No qualifying bean of type 'springbootawsbook.springawsbook.config.auth.CustomOAuth2UserService'
CustomOAuth2UserService
를 생성하는데 필요한 소셜 로그인 관련 설정값들이 없기 때문에 발생한다.
application-oauth.yml
에 분명히 설정값들을 추가했는데 이러한 이유가 발생한 이유는 src/main
과 src/test
은 본인만의 환경 구성을 가지기 때문이다. test
가 자동으로 가져오는 옵션의 범위는 application.yml
까지로, application-oauth.yml
는 test
에 파일이 없기 때문에 가져오는 파일은 아니라서 에러가 발생한 것이다.
✔ 해결 방법
이 문제를 해결하기 위해서는 테스트 환경을 위한 application.yml
을 만들어보자!
실제로 구글 연동까지 진행할 것은 아니므로 가짜 설정값을 등록한다.
src/test/resources/application.yml
spring:
jpa:
show-sql: true
h2:
console:
enabled: true
session:
store-type: jdbc
# Test OAuth
security:
oauth2:
client:
registration:
google:
clientId: google클라이언트ID
clientSecret: google클라이언트비밀번호
scope: profile,email
다시 그레이들로 테스트를 수행해 보면 다음과 같이 7개의 실패 테스트가 4개로 줄어들었다.
두 번째 실패 테스트인 Posts_등록()
을 보면 응답의 결과로 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
에서만 작동하기 때문에 아직 테스트가 작동하지 않는다.
현재 PostApiControllerTest
는 @SpringBottTest
로만 되어있으며 MockMvc
를 전혀 사용하지 않는다. 그래서 @SrpingBootTest
에서 MockMvc
를 사용하도록 코드를 다음과 같이 변경해보자.
public class PostApiControllerTest {
...
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@BeforeEach
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<Posts> all = postsRepository.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<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
@BeforeEach
mvc.perform
ObjectMapper
를 통해 문자열 JSON으로 변환한다.
전체 테스트를 다시 수행할시, Posts
테스트도 정상적으로 수행되었다.
제일 앞에서 발생한 “Hello가 리턴된다” 테스트를 확인해보자!
첫 번째로 해결한 것과 동일한 메시지인
No qualifying bean of type ‘com.
jojoldu.book.springboot.config.auth.CustomOAuth2UserService’
이다.
이 문제가 왜 발생한걸까?
HelloControllerTest는 1번과 다르게 @WebMvcTest
를 사용한다.
1번을 통해 스프링 시큐리티 설정은 잘 작동했지만, @WebMvcTest
는 CustomOAuth2UserService
를 스캔하지 않기 때문이다.
@WebMvcTest
는 WebSecurityConfigurerAdapter, WebMvcConfigurer
를 비롯한 @ControllerAdvice, @Controller를 읽지만 @Repository, @Service, @Component
는 스캔 대상이 아니다.
CustomOAuth2UserService는 읽을 수가 없어서 이와 같은 에러가 발생한 것이다.
이 문제를 해결하기 위해 스캔 대상에서 SecurityConfig
를 제거하자!
HelloControllerTest
@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
가 @SpringBootApplication
와 함께 있다보니 @WebMvcTest
에서도 스캔하게 되었다.
그래서 @EnableJpaAuditing
과 @SpringBootApplucation
둘을 분리해보자.
Application.java
//@EnableJpaAuditing 제거
@SpringBootApplication
public class SpringawsbookApplication {...}
config/JpaConfig.java
@Configuration
@EnableJpaAuditing //JPA Auditing 활성화
public class JpaConfig {
}
@WebMvcTest
는 일반적인 @Configuration
을 스캔하지 않는다.
이제 전체 테스트를 수행하면 모든 테스트가 통과한다.