1. 어노테이션 기반으로 개선
일반적인 프로그래밍에서 같은코드가 반복적되는 부분은 대표적으로 개선이 필요한 나쁜코드이다.
같은 코드를 계속해서 복사/붙여넣기로 반복하게 된다면 이후 수정할 때에도 모든 부분을 하나씩 찾아가며 수정해야한다. 이럴 경우 유지보수성이 떨어질 수 밖에 없고, 만약 수정이 반영되지 않은 반복 코드가 있다면 문제가 발생할 수 밖에 없다.
앞서 만든 코드중 IndexController에서 세션을 가져오는 부분을 개선할 필요가 있다.
SessionUser user = (SessionUser) hyttpSession.getAttribute("user");
index 메소드 외에 다른 컨트롤러와 메소드에서 세션값이 필요하면 그때마다 직접 세션에서 값을 가져와야 한다. 같은 코드가 계속해서 반복되는 것은 불필요하다.
이 부분을 메서드 인자로 세션값을 바로 받을 수 있도록 변경한다.
1) config/auth에 @LoginUser라는 어노테이션 생성
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 {
}
(1) @Target(ElementType.PARAMETER)
- 이 어노테이션이 생성될 수 있는 위치를 지정
- PARAMETER로 지정했으니 메소드의 파라미터로 선언된 객체에서만 사용할 수 있다.
(2) @interface
- 이 파일을 어노테이션 클래스로 지정
- LoginUser라는 이름을 가진 어노테이션이 생성
(3) @Retention
- 해당 어노테이션의 라이프사이클 지정
- RUNTIME으로 지정하면 JVM 메모리에 올라간 상태에서 값을 가져올 수 있는 상태를 의미
2) LoginUserArgumentResolver 생성
HandlerMethodArgumentResolver를 상속받아 구현체로 만들어야한다. 이 인터페이스는 지원 조건을 검사하고 참인 경우 해당하는 파라미터를 리턴하는 기능을 지원한다.
기본적으로 스프링에서는 ArgumentResolver에서 해당하는 파라미터를 지원하는지 과정이 수행된다.
import com.prac.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");
}
}
(1) supportsParameter()
- 컨트롤러 메서드의 특정 파라미터를 지원하는지 판단
- LoginUser 클래스가 @LoginUser로 선언되어있는지 확인 -> 반환 객체가 null이 아닌지를 확인
- 파라미터 클래스타입이 SessionUser.class인지 확인
(2) resolveArgument()
- 파라미터에 전달할 객체를 생성
- 이 프로젝트에서 세션에서 객체를 가져온다.
3) config에 WebConfig.java 생성
LginUserArgumentResolver가 스프링에서 인식할 수 있도록 WebMvcConfigurer에 추가한다.
import com.freitag.admin.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의 addArgumentResolver()를 통해 추가해야한다. 다른 HandlerMethodArguementResolver가 필요하다면 같은 방식으로 추가하면 된다.
4) IndexController.java 반복 예상 코드 수정
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user){
model.addAttribute("posts", postsService.findAllDesc());
if(user!= null){
model.addAttribute("userName", user.getName());
}
return "index";
}
(1) @LoginUser SessionUser user
- 기존에 (User) httpSession.getAttribute("user")로 가져오던 세션 정보값이 개선
- 어느 컨트롤러든지 @LoginUser만 사용하면 세션정보를 가져올 수 있다.
2. 세션 저장소로 데이터베이스 사용
현재 우리의 서비스는 애플리케이션을 재실행하면 로그인이 풀린다. 그 이유는 세션이 내장 톰캣의 메모리에 저장되기 때문이다. 기본적으로 세션은 실행되는 WAS의 메모리에서 저장되고 호출된다. 메모리에 저장되다 보니 배포할 때 마다 톰캣이 재시작되는 것이다. 그리고 2대 이상의 서버에서 서비스하고 있다면, 톰캣마다 세션 동기화 설정을 해야만 한다. 그래서 현업에서는 세션 저장소에 대해 다음의 3가지 중 한가지를 선택한다
- 톰캣
- 일반적으로 별다른 설정을 하지 않을 때 기본적으로 선택되는 방식
- 이렇게 될 경우 톰캣에 세션이 저장되기 때문에 2대 이상의 WAS가 구동외는 환경에서는 톰캣들 간의 세션 공유를 위한 추가설정이 필요하다
- MySQL과 같은 데이터베이스
- 여러 WAS간의 공용 세션을 사용할 수 있는 가장 쉬운 방법
- 많은 설정이 필요 없지만, 로그인 요청마다 DB IO가 발생하여 성능상 이슈 발생 가능성 있음
- 보통 로그인 요청이 많이 없는 백오피스, 사내 시스템 용도에서 사용
- Redis, Memcached와 같은 메모리 DB
- B2C에서 가장 많이 사용하는 방식
- 실제 서비스로 사용하기 위해서는 Embedded Redis와 같은 방식이 아닌 외부 메모리 서버가 필요
이 프로젝트에서는 데이터베이스를 세션 저장소로 사용하는 방식을 선택하여 진행한다. 그 이유는 설정이 간단하고 사용자가 많은 서비스가 아니며, 비용 절감을 위하여이다.
이후 AWS에서 이 서비스를 배포하고 운영할 때를 생각하면 레디스와 같은 메모리 DB는 별도 사용료를 지불해야하기 때문에 부담스럽다.
3. 세션 저장소 생성
1) 의존성 등록
- build.gradle
implementation('org.springframework.session:spring-session-jdbc')
- application.properties
세션 저장소를 jdbc로 선택하도록 함
spring.session.store-type=jdbc
2) 실행 및 테스트
- 로그인 후 h2-console에서 세션 등록 확인
세션 저장소를 데이터베이스로 교체했다. 물론 지금도 기존과 동일하게 스프링을 재시작하면 세션이 풀린다. 그 이유는 H2기반으로 스프링이 재실행될 때 H2도 재시작되기 때문이다. 이후 AWS로 배포하게 된다면 AWS의 데이터베이스 서비스인 RDS를 사용하기에 세션이 풀리지 않는다.
4. 네이버 로그인 추가
1) 네이버 API 등록
>> https://developers.naver.com/products/login/api/api.md
서비스 URL은 필수이다. localhost:8080으로 등록한다. 또한 Callback URL은 구글에서 등록한 리디렉션 URL과 같은 역할을 한다. /local/oauth2/code/naver로 등록한다.
2) 해당 키값들을 application-oauth.properties에 등록
# registration
spring.security.oauth2.client.registration.naver.client-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
(1) user_name_attribute=response
- 기준이 되는 user_name의 이름을 네이버에서는 response로 해야한다.
- 이유는 네이버의 회원 조회시 반환되는 JSON 형태 때문이다.
스프링 시큐리티에선 하위 필드를 명시할 수 없다. 최상위 필드들만 user_name으로 지정가능하다. 하지만 네이버의 응답값 최상위 필드는 resultCode, message, response이다. 그래서 스프링 시큐리티에서 인식가능한 필드는 저 3가지 중에서 골라야한다. 그래서 response를 user_name으로 지정하고, 이후 자바 코드로 response의 id를 user_name으로 지정할 것 이다.
3) 스프링 시큐리티 설정 등록
구글 로그인을 등록하면서 코드를 확장성 있게 작성하여 네이버는 쉽게 등록이 가능하다. OAuthAttributes에 네이버인지 판단하는 코드와 네이버 생성자만 추가해주면 된다.
- OAuthAttributes에 코드 추가
...
public static OAuthAttributes of(String registrationId,
String userNameAttributeName,
Map<String, Object> attributes){
// if 문으로 naver 판단 코드 추가
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 네이버 버튼 로그인 추가
<a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
(1) /oauth2/authorization/naver
- 네이버 로그인 URI는 application-oauth.properties에 등록한 redirect-uri 값에 맞춰 자동으로 등록된다.
- /oauth2/authorization/naver/ 까지는 고정이고 마지막 Path만 각 소셜 로그인 코드를 사용하면 된다.
4) 테스트
'스프링부트와 AWS로 혼자 구현하는 웹서비스' 카테고리의 다른 글
Part13. AWS RDS 구축 (0) | 2024.01.19 |
---|---|
Part12. AWS 서버 환경 구축 (0) | 2024.01.18 |
Part 10. 로그인 기능 구현 (1) (1) | 2024.01.15 |
Part 9. 게시글 화면 만들기 (2) (0) | 2024.01.13 |
Part 8. 게시글 화면 만들기 (1) (2) | 2024.01.13 |