Gom3rye

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 5장 (로그인 기능 구현) -2 본문

웹 개발

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 5장 (로그인 기능 구현) -2

Gom3rye 2022. 9. 4. 11:54

login 후 user db에 잘 저장되었나 확인하는 방법

http://localhost:8080/h2-console 에 들어가서 user 을 클릭해보자.

로그인된 사용자의 권한이 guest라면 posts 기능을 쓸 수 없다. (403 권한 거부 에러 발생하는 것을 볼 수 있음) -> h2-console로 가서 사용자의 role을 USER로 변경하자. (update user set role = 'USER'; 명령어 입력)

 

나쁜 코드란? -> 같은 코드가 반복되는 코드 cuz 유지보수성이 떨어짐

IndexController에서 세션값을 가져오는 부분인 SessionUser user = (SessionUser) httpSession.getAttribute("user"); 이 부분을 메소드 인자로 세션값을 바로 받을 수 있도록 변경해보자.

(이유 : index 메소드 외에 다른 컨트롤러와 메소드에서 세션값이 필요하면 그때마다 직접 세션에서 값을 가져와야 하고 같은 코드가 계속 반복되는 것은 불필요하니까)

 

@Target(ElementType.PARAMETER) // Parameter로 지정해서 메소드의 파라미터로 선언된 객체에서만 사용할 수 있다.
// 이 어노테이션이 생성될 수 있는 위치를 지정한다.
public @interface LoginUser {  // 이 파일을 어노테이션 클래스로 지정한다. LoginUser이라는 이름을 가진 어노테이션이 생성되었다고 보면 된다.

supportsParameter()

public boolean supportsParameter(MethodParameter parameter) {  //컨트롤러 메서드의 특정 파라미터를 지원하는지 판단한다. 여기서는 파라미터에 @LoginUser 어노테이션이 붙어 있고, 파라미터 클래스 타입이 SessionUser.class인 경우 true를 반환한다.

resolveArgument()

@Override // resolveArgument() : 파라미터에 전달할 객체를 생성한다. 여기서는 세션에서 객체를 가져온다.
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return httpSession.getAttribute("user");
    }

--------- 이렇게 @LoginUser를 사용하기 위한 환경은 구성되었다. 이제 LoginUserArgumentResolver가 스프링에서 인식될 수 있도록 WebMvcConfigurer에 추가하자. config 패키지에 WebConfig 클래스를 생성해서 설정 추가하자.

 

HandlerMethodArgumentResolver는 항상 WebMvcConfigurer의 addArgumentResolvers()를 통해 추가해야 한다. 다른 HandlerMethodArgumentResolver가 필요하다면 같은 방식으로 추가해 주면 된다.

@LoginUser SessionUser user

@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user){  // 기존에 (User) httpSession.getAttribute("user")로 가져오던 세션 정보 값이 개선되었다. 이젠 어느 컨트롤러든지 @LoginUser만 사용하면 세션 정보를 가져올 수 있게 됐다.
    model.addAttribute("posts", postsService.findAllDesc());
    if (user != null) { // 세션에 저장된 값이 있을 때만 model에 userName으로 등록한다.
        model.addAttribute("loginUserName", user.getName());
    }
    return "index";
}

5.5 세션 저장소로 DB 사용하기

애플리케이션 재실행하면 로그인이 풀림 -> 세션이 내장 톰캣의 메모리에 저장되기 때문이다. 기본적으로 세션은 실행되는 WAS(Web Application Server)의 메모리에서 저장되고 호출된다. 메모리에 저장되다 보니 내장 톰캣처럼 애플리케이션 실행 시 실행되는 구조에선 항상 초기화된다. 즉, 배포할 때마다 톰캣이 재시작되는 것!

+ 2대 이상의 서버에서 서비스 -> 톰캣마다 세션 동기화 설정을 해야 한다.

따라서 실제 협업에서는 세션 저장소에 대해 아래 3가지 중 한 가지를 선택한다.

  1. 톰캣 세션 사용
  2. MySQL과 같은 DB를 세션 저장소로 사용
    - 여러 WAS 간의 공용 세션을 사용할 수 있는 가장 쉬운 방법
    - 많은 설정이 필요 없지만, 결국 로그인 요청마다 DB IO가 발생해 성능상 이슈가 발생할 수 있다.
    - 보통 로그인 요청이 많이 없는 백오피스, 사내 시스템 용도에서 사용한다.
  3. Redis, Memcached와 같은 메모리 DB를 세션 저장소로 사용
    - B2C 서비스에서 가장 많이 사용하는 방식
    - 실제 서비스로 사용하기 위해서는 Embedded Redis와 같은 방식이 아닌 외부 메모리 서버가 필요하다.

build.gradle에 implementation('org.springframework.session:spring-session-jdbc') 의존성을 추가해줘야 spring-session-jdbc를 사용할 수 있다. (spring web, spring jpa를 사용했던 것과 마찬가지로)

그리고 application.properties에 세션 저장소를 jdbc로 선택하도록 spring.session.store-type=jdbc 를 추가해준다.

 

h2-console 보면 세션을 위한 테이블 SPRING_SESSION, SPRING_SESSION_ATTRIBUTES)가 생성된 것을 볼 수 있다.
-> JPA로 인해 세션 테이블이 자동 생성되었기 때문에 별도로 해야 할 일은 없다.

세션 저장소를 DB로 교체한 것을 볼 수 있다. But 동일하게 스프링을 재시작하면 세션이 풀림 -> H2 기반으로 스프링이 재실행 될 때 H2도 재시작되기 때문이다.

이후에 AWS로 배포하게 되면 AWS의 DB 서비스인 RDS를 사용하게 되어 세션이 풀리지 않게 된다. (지금까지는 그 기반이 되는 코드를 작성한 것)

user-name-attribute = response

기준이 되는 user_name의 이름을 네이버에서는 response로 해야 한다. 이유는 네이버의 회원 조회 시 반환되는 JSON 형태 때문이다.

 

스프링 시큐리티에선 하위 필드를 명시할 수 없다. 최상위 필드들만 user_name 으로 지정 가능하다. 하지만 네이버의 응답값 최상위 필드는 resultCode, message, response 이다. 따라서 스프링 시큐리티에서 인식 가능한 필드는 이 3개 중에 골라야 하고 책에서 담고 있는 response를 user_name으로 지정하고 이후 자바 코드로 response의 id를 user_name으로 지정하겠다.

스프링 시큐리티 설정 등록

/oauth2/authorization/naver

<a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>

- 네이버 로그인 URL은 application-oauth.properties에 등록한 redirect-uri 값에 맞춰 자동으로 등록된다.

- /oauth2/authorization/ 까지는 고정이고 마지막 Path만 각 소셜 로그인 코드를 사용하면 된다. 여기서는 naver가 마지막 Path가 된다.

SecurityConfig 예전 버전

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity // Spring Security 설정들을 활성화시켜 준다.
public class SecurityConfig {
    private final CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .headers().frameOptions().disable() // h2-console 화면을 사용하기 위해 해당 옵션들을 disable 한다.
                .and()
                .authorizeRequests() // URL별 권한 관리를 설정하는 옵션의 시작점이다.
                // authorizeRequests가 선언되어야만 antMatchers 옵션을 사용할 수 있다.
                .antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
                .antMatchers("/api/v1/**").hasRole(Role.USER.name())
                // 권한 관리 대상을 지정하는 옵션, URL,HTTP 메소드별로 관리가 가능
                // "/" 등 지정된 URL들은 permitAll()옵션을 통해 전체 열람 권한을 주었다.
                // "/api/v1/**" 주소를 가진 API는 USER 권한을 가진 사람만 가능하도록 했다.
                .anyRequest().authenticated() // anyRequest : 설정된 갓들 이외 나머지 URL들을 나타냄
                // 여기서는 authenticated()를 추가해 나머지 URL들은 모두 인증죈 사용자들에게만 허용하게 한다. 인증된 사용자 = 로그인한 사용자
                .and()
                .logout()
                .logoutSuccessUrl("/") // 로그아웃 기능에 대한 여러 설정의 진입점, 로그아웃 성공 시 / 주소로 이동한다.
                .and()
                .oauth2Login() // OAuth 2 로그인 기능에 대한 여러 설정의 진입점
                .userInfoEndpoint() // OAuth 2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당
                .userService(customOAuth2UserService); // 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록, 리소스 서버(즉, 소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있다.

        return http.build();
    }
}
package com.gom3rye.book.springboot.config.auth;

import com.gom3rye.book.springboot.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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; //WebSecurityConfigurerAdapter는 deprecated 되어, 상속을 받지 않고 모두 Bean으로 등록하여 사용하는 방식으로 변경 됨.
import org.springframework.security.web.SecurityFilterChain;

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity // Spring Security 설정들을 활성화시켜 준다.
public class SecurityConfig {
    private final CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .headers().frameOptions().disable() // h2-console 화면을 사용하기 위해 해당 옵션들을 disable 한다.
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll()
                        .antMatchers("/category/**","/categories","/write", "/api/**", "/post/**", "/posts").hasRole(Role.USER.name())
                        .anyRequest().authenticated()) // anyRequest : 설정된 갓들 이외 나머지 URL들을 나타냄
                // 여기서는 authenticated()를 추가해 나머지 URL들은 모두 인증죈 사용자들에게만 허용하게 한다. 인증된 사용자 = 로그인한 사용자
                .logout(logout -> logout
                        .logoutSuccessUrl("/")) // 로그아웃 기능에 대한 여러 설정의 진입점, 로그아웃 성공 시 / 주소로 이동한다.
                .oauth2Login(oauth2Login -> oauth2Login
                        .userInfoEndpoint() // OAuth 2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당
                        .userService(customOAuth2UserService)); // 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록, 리소스 서버(즉, 소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있다.

        return http.build();
    }
}

변경 후

728x90
반응형