스프링 시큐리티(Spring Security)는 막강한 인증과 인과(혹인 권한 부여) 기능을 가진 프레임워크입니다.
사실상 스프링 기반의 애플리케이션에서는 보안을 위한 표준이라고 보면 됩니다. 필터 기반의 보안 기능을 구현하는 것보다 스프링 시큐리티를 통해 구현하는 것을 적극적으로 권하고 있습니다.
하지만 오픈 아이디는 표준스펙이 존재하지 않았기 때문에 제공자마다 서로 다른 프로토콜을 제공하여 여러 오픈 아이디를 제공하기 위해선 각 규격별로 연동을 만들어야 했고, 일부 프로토콜에선 보안 이슈가 많이 나오기도 했습니다.
때문에 표준적인 방법을 고려하던 사람들은 OAuth 라는 규격을 만들고 2007년 10월 03일 OAuth 1.0 을 발표하게 되었습니다.
https://oauth.net/
2016년 OAuth 의 최신버전은 2.0이며 대부분의 소셜과 국내 대형 포털들은 해당 스펙에 맞게 로그인을 제공해 주고있습니다.
OAuth2 를 제공하는 서비스
페이스북, 깃허브, 트위터, 네이버, 카카오등...
(그밖에도 많이 있지만 필자도 찾아봐야지 압니다.... 일반적으로 로그인 API를 지원한다라고 말하면 OAuth2 지원이라고 봐도 무방합니다.)
스프링 부트 1.5.에서의 OAuth2 연동 방법이 2.0에서는 크게 변경되었습니다. 하지만 설정 방법에 크게 차이가 없는 경우를 자주 봅니다. 이는 spring-security-oauth2-autoconfigure 라이브러리 덕입니다.
spring-security-oauth2-autoconfigure
spring-security-oauth2-autoconfigure 라이브러리를 사용할 경우 스프링 부트 2에서도 1.5에서 쓰던 설정을 그대로 사용할 수 있습니다.
하지만 이 책에서는 스프링 부트 2 방식인 Spring Security Oatuh2 Clinet 라이브러리를 사용해서 진행합니다. 이유는 다음과 같습니다.
먼저 구글 서비스에 신규 서비스를 생성합니다. 여기서 발급된 인증 정보(clientId와 clientSecret)를 통해서 로그인 기능과 소셜 서비스 기능을 사용 할 수 있으니 무조건 발급 받고 시작해야 합니다.
구글 클라우드 플랫폼 주소(Google Cloud Platform)로 이동합니다.
프로젝트 생성
API 및 서비스 대시보드 이동
사용자 인증 정보 > 사용자 인증 정보 만들기
OAuth 클라이언트 ID 만들기 > 동의 화면 구성 버튼 클릭
OAuth 동의 화면 입력
OAuth 클라이언트 ID 만들기
승인된 리디렉션 URL
application-oauth.properties
spring.security.oauth2.client.registration.google.client-id=클라이언트 ID spring.security.oauth2.client.registration.google.client-secret=클라이언트 보안 비밀번호 spring.security.oauth2.client.registration.google.scope=profile,email
scope=profile,email
application.properties 추가
spring.profiles.include=oauth
.gitignore 등록
application-oauth.properties
보안을 위해 깃허브에 application-oauth.properties 파일이 올라가는 것을 방지하겠습니다.
추가한 뒤 커밋했을 때 커밋 파일 목록에 application-oauth.properties가 나오지 않으면 성공입니다.
구글의 로그인 인증정보를 발급 받았으니 프로젝트 구현을 진행하겠습니다. 먼저 사용자 정보를 담당할 도메인인 User 클래스를 생성합니다.
User 클래스 생성
package com.swchoi.webservice.springboot.domain.user; import com.swchoi.webservice.springboot.domain.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(); } }
각 사용자의 권한을 관리할 Enum 클래스 Role을 생성합니다.
Role
package com.swchoi.webservice.springboot.domain.user; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor public enum Role { GUSET("ROLE_GUEST", "손님"), USER("ROLE_USER", "일반 사용자"); private final String key; private final String title; }
스프링 시큐리티에서는 권한 코드에 항상 ROLE_이 앞에 있어야만 합니다. 그래서 코드별 키 값을 ROLE_GUEST, ROLE_USER 등으로 지정합니다.
마지막으로 User의 CRUD를 책임질 UserRepository도 생성합니다.
UserRepository
package com.swchoi.webservice.springboot.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); }
User 엔티티 관련 코드를 모두 작성했으니 본격적으로 시큐리티 설정을 진행하겠습니다.
먼저 build.gradle에 스프링 시큐리티 관련 의존성 하나를 추가합니다.
build.gradle
compile('org.springframework.boot:spring-boot-starter-oauth2-client')
build.gradle 설정이 끝났으면 OAuth 라이브러리를 이용한 소셜 로그인 설정 코드를 작성합니다.
패키지 생성
SecurityConfig
package com.swchoi.webservice.springboot.config.auth; import com.swchoi.webservice.springboot.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); } }
설정 코드 작성이 끝났다면 CustomOAuth2UserService클래스를 생성 합니다. 이 클래스에서는 구글 로그인 이후 가져온 사용자의 정보(email,name,picture등) 들을 기반으로 가입 및 정보수정, 세션 저장 등의 기능을 지원합니다.
CustomOAuth2UserService
package com.swchoi.webservice.springboot.config.auth; import com.swchoi.webservice.springboot.config.auth.dto.OAuthAttributes; import com.swchoi.webservice.springboot.config.auth.dto.SessionUser; import com.swchoi.webservice.springboot.domain.user.User; import com.swchoi.webservice.springboot.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.Collection; 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); } }
구글 사용자 정보가 업데이트 되었을 때를 대비하여 update 기능도 같이 구현되었습니다. 사용자의 이름이나 프로필 사진이 변경되면 User엔티티에도 반영됩니다.
CustomOAuth2UserService 클래스까지 생성되었다면 OAuthAttributes 클래스를 생성합니다.
OAuthAttributes
package com.swchoi.webservice.springboot.config.auth.dto; import com.swchoi.webservice.springboot.domain.user.Role; import com.swchoi.webservice.springboot.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.GUSET) .build(); } }
config.auth.dto 패키지에 SessionUser 클래스를 추가합니다.
SessionUser
package com.swchoi.webservice.springboot.config.auth.dto; import com.swchoi.webservice.springboot.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(); } }
SessionUser에는 인증된 사용자 정보만 필요합니다.
@Entity User 클래스를 SessionUser로 사용안하는 이유
세션에 저장하기 위해 User클래스를 세션에 저장하려고 하니 User 클래스에 직렬화를 구현하지 않았다는
에러가 난다.
스프링 시큐리티가 잘 적용되었는지 확인하기 위해 화면에 로그인 버튼을 추가해보자
index.mustache 수정
{{>layout/header}} <h1>스프링 부트로 시작하는 웹 서비스</h1> <div class="col-md-12"> <!--로그인 기능 영역--> <div class="row"> <div class="col-md-6"> <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a> {{#userName}} Logged in as : <span id="user">{{username}}</span> <a href="/logout" class="btn btn-info active" role="button">Logout</a> {{/userName}} {{^userName}} <a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a> {{/userName}} </div> </div> </div> <br> <!-- 목록 출력 영역 --> ... {{>layout/footer}}
코드설명 1. {{#userName}} - 머스테치는 다른 언어와 같은 if문(if userName != null)을 제공하지 않습니다. - true/false 여부만 판단할 뿐입니다. 2. a href="/logout" - 스프링 시큐리티에서 기본적으로 제공하는 로그아웃 URL입니다. - 즉, 개발자가 별도로 저 URL에 해당하는 컨트롤러를 만들 필요가 없습니다. - SecurityConfig 클래스에서 URL을 변경할 수 있다. 3. {{^userName}} - 머스테치에서 해당 값이 존재하지 않는 경우에는 ^를 사용합니다. 4. a href="/oauth2/authorization/google" - 스프링 시큐리티에서 기본적으로 제공하는 로그인 URL입니다. - 로그아웃 URL과 마찬가지로 개발자가 별도의 컨트롤러를 생성할 필요가 없습니다.
index.mustache에서 userName을 사용할 수 있게 IndexController에서 userName을 추가합니다.
IndexController
import javax.servlet.http.HttpSession; @RequiredArgsConstructor @Controller public class IndexController { private final PostService postService; private final HttpSession httpSession; @GetMapping("/") public String index(Model model) { model.addAttribute("posts", postService.findAllDesc()); SessionUser user = (SessionUser) httpSession.getAttribute("user"); if(user != null){ model.addAttribute("userName", user.getName()); } return "index"; } ... }
그럼 한번 로그인 테스트를 진행하겠습니다.
로그인 버튼
구글 로그인 버튼 클릭
구글 로그인 성공
회원가입도 잘 되어 있는지 확인 -> http://loaclhost:8080/h2-console 접속
데이터베이스에 정상적으로 회원정보가 들어간 것까지 확인했습니다. 또한 권한관리도 잘되는지 확인해 보겠습니다. 현재 로그인 사용자의 권한(ROLE)은 GUEST입니다. 이 상태에서는 posts 기능을 전혀 쓸 수 없습니다. 실제 글 등록 기능으 사용해보겠습니다.
게시글 등록 실패
403(권한 거부) 에러가 발생한 것을 볼 수 있습니다.
사용자 권한 변경
update user set role = 'USER';
권한 변경 후 등록 성공
기본적인 구글 로그인,로그아웃,회원가입,권한관리 기능이 모두 구현 되었습니다.
반복되는 코드를 개선하기
SessionUser user = (SessionUser) httpSession.getAttribute("user");
다른 컨트롤러와 메소드에서 세션값이 필요하면 그때마다 직접 세션에서 값으 가져와야 합니다.
이 부분을 메소드 인자로 세션값을 받을 수 있도록 변경해 보겠습니다.
@LoginUser
package com.swchoi.webservice.springboot.config.auth; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface LoginUser { }
HandlerMethodArgumentResolver 인터페이스를 구현한 LoginUserArgumentResolver 클래스를 생성합니다.
HandlerMethodArgumentResolver는 한가지 기능을 지원합니다.
바로 조건에 맞는 경우 메소드가 있다면 HandlerMethodArgumentResolver의 구현체가 지정한 값으로 해당 메소드의 파라미터로 넘길수 있습니다.
package com.swchoi.webservice.springboot.config.auth; import com.swchoi.webservice.springboot.config.auth.dto.SessionUser; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import javax.servlet.http.HttpSession; @RequiredArgsConstructor @Component public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver { private final HttpSession httpSession; @Override public boolean supportsParameter(MethodParameter parameter) { boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null; boolean isUserClass = SessionUser.class.equals(parameter.getParameterType()); return isLoginUserAnnotation && isUserClass; } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { return httpSession.getAttribute("user"); } }
코드설명
- supportsParameter
- 컨트롤러 메서드의 특정 파라미터를 지원하는지 판단합니다.
- 여기서 파라미터에 @LoginUser 어노테이션이 붙어 있고, 파라미터 클래스 타입이 SessionUser.class인 경우 true를 반환
- resolveArgument
- 파라미터에 전달할 객체를 생성
LoginUserArgumentResolver를 스프링에서 인식될 수 있도록 WebMvcConfigurer 추가해야한다.
config 패키지에 WebConfig 클래스 생성하여 다음과 같이 설정한다.
package com.swchoi.webservice.springboot.config; import com.swchoi.webservice.springboot.config.auth.LoginUserArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @RequiredArgsConstructor @Configuration public class WebConfig implements WebMvcConfigurer { private final LoginUserArgumentResolver loginUserArgumentResolver; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(loginUserArgumentResolver); } }
HandlerMethodArgumentResolver는 항상 WebMvcConfigurer의 addArgumentResolvers를 사용해 추가해야한다.
모든 설정이 끝났으니 IndexController의 코드를 수정하겠습니다.
IndexController
@RequiredArgsConstructor @Controller public class IndexController { private final PostService postService; @GetMapping("/") public String index(Model model, @LoginUser SessionUser user) { model.addAttribute("posts", postService.findAllDesc()); if(user != null){ model.addAttribute("userName", user.getName()); } return "index"; } ... }
코드설명
- @LoginUser SessionUser user
- 기존에 (User)httpSerssion.getAttribut("user")로 가져오던 세션 정보 값이 개선되었습니다.
- 이제는 어느 컨트롤러든지 @LoginUser만 사용하면 세션 정보를 가져올 수 있게 되었습니다.
세션 저장소에 대해 다음의 3가지 중 한 가지를 선택합니다.
먼저 build.gradle에 다음과 같이 의존성을 등록합니다.
build.gradle
compile('org.springframework.session:spring-session-jdbc')
application.properties
spring.session.store-type=jdbc
h2-console를 보면 세션을 위한 테이블 2개(SPRING_SESSION,SPRING_SESSION_ATTRIBUTES)가 생성된 것을 볼 수 있습니다. JPA로 인해 세션 테이블이 자동 생성되었습니다.
세션 테이블
먼저 네이버 오픈 API로 이동
https://developers.naver.com/apps/#/register?api=nvlogin
네이버 서비스 등록
로그인 오픈 API 서비스 환경 설정
등록을 완료하면 ClientID와 ClientSecret가 생성된다.
해당 키값들을 application-oauth.properties에 등록합니다. 네이버에서는 스프링 시큐리티를 공식 지원하지 않기 때문에 그동안 Common-OAuth2Provider에서 해주던 값들도 전부 수동으로 입력해야합니다.
application-oauth.properties
# registration spring.security.oauth2.client.registration.naver.client-id=클라이언트ID spring.security.oauth2.client.registration.naver.client-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
네이버 오픈 API의 로그인 회원 결과는 다음과 같습니다.
{
"resultcode" : "00",
"message: : "success",
"response" : {
"email" : "openapi@naver.com",
...
}
}
스프링 시큐리티에선 하위 필드를 명시 할 수 없습니다. 최상위 필드들만 user_name으로 지정가능합니다.
구글 로그인을 등록하면서 대부분 코드가 확장성 있게 작성되었다 보니 네이버는 쉽게 등록 가능합니다.
OAuthAttributes에서 다음과 같이 네이버인지 판단하는 코드와 네이버 생성자만 추가해 주면 됩니다.
OAuthAttributes
@Getter 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에 네이버 로그인 버튼을 추가합니다.
index.mustache
... <div class="col-md-12"> <!--로그인 기능 영역--> <div class="row"> <div class="col-md-6"> <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a> {{#userName}} Logged in as : <span id="user">{{username}}</span> <a href="/logout" class="btn btn-info active" role="button">Logout</a> {{/userName}} {{^userName}} <a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a> <a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a> {{/userName}} </div> </div> </div> ...
로컬 네이버 로그인 화면
네이버 로그인
로그인 성공 화면
인텔리제이 오른쪽 위에 [Gradle] 탭 클릭합니다. [Tasks -> verification -> test]를
차례로 선택해서 전체 테스트를 수행합니다.
Gradle 탭의 test Task
test를 실행하면 실패하는 것을 확인할 수있습니다.
전체 테스트
실패 이유
소셜 로그인 관련 설정값들이 없기 때문에 발생합니다.
test에 application.properties를 설정합니다.
application.properties(test)
spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect 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_등록된다 실패 로그
build.gradle
testCompile('org.springframework.security:spring-security-test')
PostsApiControllerTest
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class PostsApiControllerTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Autowired private PostsRepository postsRepository; @Autowired private WebApplicationContext context; private MockMvc mvc; @Before public void setup() { mvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()) .build(); } @After public void tearDown() throws Exception { postsRepository.deleteAll(); } @Test @WithMockUser(roles = "USER") public void Posts_등록된다() throws Exception { //given String title = "title"; String content = "content"; PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder() .title(title) .content(content) .author("author") .build(); String url = "http://localhost:" + port + "/api/v1/posts"; //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 Posts_수정된다() throws Exception { //given Posts savePosts = postsRepository.save(Posts.builder() .title("title") .content("content") .author("author") .build()); Long updateId = savePosts.getId(); String expectedTitle = "title2"; String expectedContent = "content2"; PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder() .title(expectedTitle) .content(expectedContent) .build(); String url = "http://localhost:" + port + "/api/v1/posts/" + updateId; HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto); //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); } }
@WebMvcTest는 WebSecurityConfigurerAdapter, WebMvcConfigurer를 비롯한 @ControllerAdvice, @Controller를 읽습니다. 즉 @Repository, @Service, @Component는 스캔 대상이 아니다.
HelloControllerTest
@RunWith(SpringRunner.class) @WebMvcTest(controllers = HelloController.class, excludeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class) }) public class HelloControllerTest { @Autowired private MockMvc mvc; @WithMockUser(roles = "USER") @Test public void hello가_리턴된다() throws Exception { String hello = "hello"; mvc.perform(get("/hello")) .andExpect(status().isOk()) .andExpect(content().string(hello)); } @WithMockUser(roles = "USER") @Test public void helloDto가_리턴된다() throws Exception { String name = "hello"; int amount = 1000; mvc.perform( get("/hello/dto") .param("name", name) .param("amount", String.valueOf(amount))) .andExpect(status().isOk()) .andExpect(jsonPath("$.name", is(name))) .andExpect(jsonPath("$.amount", is(amount))); } }
@WebMvcTest이다 보니 당연히 에러가 난다. @EnableJpaAuditing @Entity클래스 최소 하나 필요하다.
Application 수정
// @EnableJpaAuditing 제거 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
JpaConfig 생성
package com.swchoi.webservice.springboot.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Configuration @EnableJpaAuditing // JPA Auditing 활성화 public class JpaConfig { }
전체 테스트 통과 확인
완료!
여기서 user.getRole().getKey() 아닌가요~? 댓글해주시면 감사하겟습니다 감사히잘배우고잇습니다.