Gom3rye
스프링 부트와 AWS로 혼자 구현하는 웹 서비스 5장 (로그인 기능 구현) -2 본문
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가지 중 한 가지를 선택한다.
- 톰캣 세션 사용
- MySQL과 같은 DB를 세션 저장소로 사용
- 여러 WAS 간의 공용 세션을 사용할 수 있는 가장 쉬운 방법
- 많은 설정이 필요 없지만, 결국 로그인 요청마다 DB IO가 발생해 성능상 이슈가 발생할 수 있다.
- 보통 로그인 요청이 많이 없는 백오피스, 사내 시스템 용도에서 사용한다. - 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();
}
}
변경 후
'웹 개발' 카테고리의 다른 글
스프링부트] POJO 상품 등록 기능 구현하기 (0) | 2023.06.20 |
---|---|
Who Am I] MBTI 테스트 만들기 (0) | 2022.10.07 |
Who Am I] 이벤트 핸들링 (0) | 2022.09.01 |
Who Am I] Components와 Props / State 다루기 (0) | 2022.09.01 |
Who Am I] Virtual DOM (0) | 2022.08.31 |