이 글은 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 책을 참고하여 작성되었습니다.

1. 로그인 코드 및 환경 개선

1) 로그인 코드 개선

세션 부분은 index 메서드 외에도 다른 컨트롤러와 메서드에서 사용 가능성이 있는데 현재는 그저 코드로만 구현되어 활용하려면 복사 붙여넣기를 해야한다. 코드 유지보수와 에러 발견에 어려움을 주기때문에 이를 개선해보자.

  • 해당 코드
    SessionUser user = (SessionUser) httpSession.getAttribute("user");

@LoginUser 어노테이션을 생성해개선해보자.


LoginUser.java

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
  • @interface: 해당 자바 파일이 어노테이션임을 나타낸다.
  • @Target(ElementType.PARAMETER): 이 어노테이션이 생성될 수 있는 위치를 지정한다. PARAMETER로 지정했으므로 메서드의 파라미터로 선언된 객체에서만 사용가능하다. 이 외에도 여러 타입이 존재한다.

HandlerMethodArgumentResolver는 조건에 맞는 경우 메서드가 있다면 구현체가 지정한 값으로 해당 메서드의 파라미터로 넘길 수 있게 해준다. 구현체를 작성해보자.

LoginUserArgumentResolver.java

@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 타입이 모두 존재하는지 판단한다. 어노테이션은 바로 비교식으로 비교하고, 클래스는 equals 메서드로 비교한다.
  • resolveArgument(): 세션에서 객체를 가져와 파라미터에 전달할 객체를 생성한다.

위에 작성된 코드를 통해 IndexController를 개선해보자.

IndexController.java

public class IndexController {
    private final PostsService postsService;

    @GetMapping("/")
    public String index(Model model, @LoginUser SessionUser user) {
        model.addAttribute("posts", postsService.findAllDesc());
        if (user != null) {
            model.addAttribute("loginUser", user.getName());
        }
        return "index";
    }
    
...

}
  • @LoginUser SessionUser user: 기존 코드를 개선한 부분이다. @LoginUser만 사용하면 세션정보를 가져올 수 있다.

2) 로그인 환경 개선

현재 환경에서는 세션이 내장 톰캣의 메모리에 저장되어 실행 시 항상 초기화 때문에 애플리케이션 재시작시 로그인이 풀린다. 따라서 DB를 세션 저장소로 활용하여 사용한다. 서비스가 커지면 메모리 DB 방식을 사용한다고 한다.

build.gradle에 의존성을 추가해주자.
implementation('org.springframework.session:spring-session-jdbc')

그런 다음 세션 저장소를 jdbc로 사용하도록 application.properties에 코드를 추가하자.
spring.session.store-type=jdbc

현재는 재시작하면 초기화되는 h2가 저장소라 마찬가지로 로그인이 풀리지만 추후 aws에서 rds를 사용하면 풀리지 않게된다.

2. 네이버 로그인

책에 명시된대로 네이버 서비스 등록 링크로 들어가 서비스를 생성한다.
그후 application-oauth.properties에 id와 시크릿을 입력한다.

# 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
  • user_name_attribute=response: 네이버는 JSON 형태로 회원 정보를 반환하고 유저 이름은 response 필드의 하위필드이다. 스프링 시큐리티는 하위 필드를 명시할 수 없기 때문에 response를 지정해주고 자바 코드에서 처리하도록한다.

OAuthAttributes에서 네이버인지 판단하는 코드와 생성자를 추가하고, index.mustache에 네이버 로그인 버튼을 추가하면 완성된다.


OAuthAttributes.java

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("picture"))
                .attributes(response)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }
...

}
  • response.get("name");: response 필드에서 name 하위필드를 얻어 사용한다.

index.mustache

...

	{{^loginUser}}
		<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>
	{{/loginUser}}

...

/oauth2/authorization/...: 마지막 Path에 각 소셜 로그인 코드를 사용한다.

3. 시큐리티 테스트

시큐리티 옵션 활성화로 api 호출에 role에 따라 제약이 생기기 때문에 여러 테스트 코드가 실패하게된다. 이를 고쳐보자.

1) test에 application-oauth.properties 적용

main에는 이미 생성했지만 test에는 적용되지 않는다. test는 application.properties까지만 자동으로 가져오기 때문이다. 따라서 테스트를 위해 src/test/resources/application.properties에 테스트용 설정 값을 추가하자.

spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.h2.console.enabled=true
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb;MODE=MYSQL
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

2) 임의 사용자 인증 추가하기

ROLES_USER가 아닌 사용자는 글 등록 및 수정이 불가능하기 때문에 임의 사용자 인증을 추가하여 테스트를 진행하여야한다. 의존성을 추가하여 진행하자.

testImplementation("org.springframework.security:spring-security-test")

그리고 PostApiControllerTest의 각 Test 메서드에 @WithMockUser(roles = "USER") 어노테이션을 작성하여 임의 사용자 인증을 추가해주자. 그후 Mockmvc 환경에서 작동하게 @BeforeEach로 매번 테스트마다 Mockmvc 인스턴스를 생성하여 mvc.perform 으로 테스트하자.

3) @WebMvcTest 스캔대상 예외 만들기

HelloControllerTest는 @WebMvcTest를 사용하므로 기본적으로 SecurityConfig를 스캔한다. 그래서 @Repository, @Service, @Component는 읽지 않아 테스트 실패 문제가 발생한다. 따라서 SecurityConfig를 스캔 대상에서 제거하고 임의 사용자 인증을 추가하자.

@WebMvcTest(controllers = HelloController.class,
        excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)
        }
)

그리고 WebMvcTest에서 @Entity 클래스가 없어서 @EnableJpaAuditing 오류가 나기때문에 application에서 해당 어노테이션을 삭제한다. 그리고 config 패키지에 JpaConfig 클래스를 만들어 @Configuration, @EnableJpaAuditing 어노테이션만 붙인 클래스를 만들어 문제를 해결한다.

+) IndexControllerTest 오류

이 부분은 index.mustache의 h1 부분을 스프링부트로 시작하는 웹 서비스 Ver.2 로 수정하고 Test의 테스트 문자열을 수정하지 않아서 발생한 단순한 오류이다. 간단하게 문자열만 고쳐주면된다.

//수정 전
        assertThat(body).contains("스프링부트로 시작하는 웹 서비스");
//수정 후
        assertThat(body).contains("스프링부트로 시작하는 웹 서비스 Ver.2");
profile
여러가지를 시도하는 학생입니다

0개의 댓글