일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 리눅스
- network configuration
- 네트워크 설정
- 자바편
- 처음 만나는 AI수학 with Python
- Kernighan의 C언어 프로그래밍
- 처음 만나는 AI 수학 with Python
- GIT
- 알파회계
- 이터레이터
- resttemplate
- 자료구조와 함께 배우는 알고리즘 입문
- 티스토리 쿠키 삭제
- 목록처리
- iterator
- d
- baeldung
- 코드로배우는스프링웹프로젝트
- 친절한SQL튜닝
- 스프링 시큐리티
- /etc/network/interfaces
- 페이징
- ㅒ
- 데비안
- 서버설정
- 스프링부트핵심가이드
- 구멍가게코딩단
- 선형대수
- 자료구조와함께배우는알고리즘입문
- 코드로배우는스프링부트웹프로젝트
- Today
- Total
bright jazz music
blog32 : 데이터베이스를 통한 토큰 검증 본문
이 포스팅에서는 UUID를 통해서 세션 토큰을 발급받고 인증을 요청하는 부분을 진행한다.
로그인 - 토큰 발급 - DB를 통해서 인증을 확인한 뒤 - 인증 관련 페이지로 라우팅
- 로그인하면 응답에 토큰이 날아옴.
- 이 토큰을 향후 게시글 쓰기 같은 인증이 필요한 페이지에 진입하기 위해,
- header에 Authorization값으로 넣어준다.
- 인증이 필요한 해당 요청했을 때 인증이 되고 해당 회원의 primary id를 출력해 준다.
- 아마 숫자 1, 또는 2가 결괏값으로 나올 것이다.
0.현황
발급 받은 세션을 보내면 서버에서 검증하는 코드 작성.
현재는 WebMvcConfigurer에 등록한 AuthResolver(implements HandlerMethodArgumentResolver)를 사용하여 인증 처리를 하고 있다. 이 클래스는 컨트롤러 메서드 파라미터 타입으로 특정 객체(ex. UserSession)가 존재하는 경우 무조건 인증을 거치게 한다.
//AuthResolver.java
package com.endofma.blog.config;
import com.endofma.blog.config.data.UserSession;
import com.endofma.blog.exception.Unauthorized;
import org.springframework.core.MethodParameter;
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;
public class AuthResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
//컨트롤러에서 사용할 어노테이션이나 DTO가 사용자가 사용하려는 것이 맞는지, 지원하는지 체크한다.
return parameter.getParameterType().equals(UserSession.class); //UserSession 클래스를 사용하는 것이 맞는지 확인. 맞으면 컨트롤러에 값 할당
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//실제로 해당 DTO에 값을 세팅해준다.
//인증 작업 추가
// String accessToken = webRequest.getParameter("accessToken");
String accessToken = webRequest.getHeader("Authorization"); //헤더의 값으로 확인해준다.
if (accessToken == null || accessToken.equals("")) {
throw new Unauthorized();
}
//데이터베이스 사용자 확인작업
//.. 추후 추가
return new UserSession(1L);
}
}
예를 들면 PostController의 @GetMapping("/foo")와 @GetMapping("/bar")는 파라미터로 UserSession 객체를 가지고 있다. 따라서 해당 경로의 API를 사용하려면 반드시 ArgumentResolver를 통한 인증을 거쳐야 한다.
@GetMapping("/foo")
public Long foo(UserSession userSession){
log.info(">>> {}", userSession.id);
return userSession.id;
}
@GetMapping("/bar")
public String bar(UserSession userSession){
log.info(">>> {}", userSession.id);
return "인증이 필요한 페이지";
}
지난 포스팅에서 로그인 시 토큰을 발급 받도록 하였으니 이번 포스팅에서는 요청을 받아 보도록 하자.
1. 테스트 코드 작성
//AuthControllerTest.java
@Test
@DisplayName("로그인 후 권한이 필요한 페이지에 접속한다. /foo")
void test4() throws Exception {
//given
// //users 테이블에 사용자 저장
// User user = userRepository.save(User.builder()
// .name("catnails")
// .email("catnails@gmail.com")
// .password("1234")
// .build());
//
// Login login = Login.builder()
// .email("catnails@gmail.com")
// .password("1234")
// .build();
//
// String json = objectMapper.writeValueAsString(login);
//expected
mockMvc.perform(get("/foo")
.contentType(APPLICATION_JSON))
.andExpect(status().isUnauthorized())
.andDo(print());
}
토큰 발급 받는 부분을 주석처리 하였다.
결과
MockHttpServletRequest:
HTTP Method = GET
Request URI = /foo
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8"]
Body = null
Session Attrs = {}
Handler:
Type = com.endofma.blog.controller.PostController
Method = com.endofma.blog.controller.PostController#foo(UserSession)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = com.endofma.blog.exception.Unauthorized
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 401
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"code":"401","message":"인증이 필요합니다.","validation":{}}
Forwarded URL = null
Redirected URL = null
Cookies = []
2023-02-16 08:32:00.312 INFO 11688 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-02-16 08:32:00.314 INFO 11688 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2023-02-16 08:32:00.317 WARN 11688 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 90121, SQLState: 90121
2023-02-16 08:32:00.318 ERROR 11688 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-02-16 08:32:00.318 WARN 11688 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 90121, SQLState: 90121
2023-02-16 08:32:00.318 ERROR 11688 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-02-16 08:32:00.319 WARN 11688 --- [ionShutdownHook] o.s.b.f.support.DisposableBeanAdapter : Invocation of destroy method failed on bean with name 'entityManagerFactory': org.hibernate.exception.JDBCConnectionException: Unable to release JDBC Connection used for DDL execution
2023-02-16 08:32:00.319 INFO 11688 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2023-02-16 08:32:00.323 INFO 11688 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 13s
5 actionable tasks: 2 executed, 3 up-to-date
AM 8:32:00: Execution finished ':test --tests "com.endofma.blog.controller.AuthControllerTest.test4"'.
로그인 하지 않아 토큰을 발급받지 못해 접근하지 못하는 결과를 확인하였다(성공)
만약 토큰을 발급받는 테스트를 하고 싶다면 AuthService의 signin() 메서드를 테스트 코드 내에 삽입할 수도 있다. 그러나 이 경우 signin()의 내용이 변경될 경우 sidepack이 생겨날 수 있다.
따라서 서비스를 거치지 않고 테스트 내에서 직접 사용자 엔티티에 세션 값까지 넣어서 repository에 save하는 네이티브 한 코드를 만들어 보자.
1. AuthResolver 수정
DB에서 accessToken 값을 조회하여 있는 경우 Session 객체에 담고, 없으면 Unauthorized 에러 발생시키는 코드 추가.
존재하는 경우 session.getUser().getId() 해준다. DB에서의 Long id값을 가져오는 것이다.
//AuthResolver.java
package com.endofma.blog.config;
import com.endofma.blog.config.data.UserSession;
import com.endofma.blog.domain.Session;
import com.endofma.blog.exception.Unauthorized;
import com.endofma.blog.repository.SessionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
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 java.util.Optional;
@RequiredArgsConstructor
public class AuthResolver implements HandlerMethodArgumentResolver {
//주입
private final SessionRepository sessionRepository;
@Override
public boolean supportsParameter(MethodParameter parameter) {
//컨트롤러에서 사용할 어노테이션이나 DTO가 사용자가 사용하려는 것이 맞는지, 지원하는지 체크한다.
return parameter.getParameterType().equals(UserSession.class); //UserSession 클래스를 사용하는 것이 맞는지 확인. 맞으면 컨트롤러에 값 할당
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//실제로 해당 DTO에 값을 세팅해준다.
//인증 작업 추가
// String accessToken = webRequest.getParameter("accessToken");
String accessToken = webRequest.getHeader("Authorization"); //헤더의 값으로 확인해준다.
if (accessToken == null || accessToken.equals("")) {
throw new Unauthorized();
}
//데이터베이스 사용자 확인작업
// Optional<Session> session = sessionRepository.findByAccessToken(accessToken);
Session session = sessionRepository.findByAccessToken(accessToken)
.orElseThrow(Unauthorized::new);
// return new UserSession(1L);
return new UserSession(session.getUser().getId()); //컨트롤러로 넘겨준다.
}
}
값을 컨트롤러에 반환한다.
//postController.java
@GetMapping("/foo")
public Long foo(UserSession userSession){
log.info(">>> {}", userSession.id);
return userSession.id;
}
2. SessionRepository 수정
원래는 아무런 메소드도 보유하고 있지 않았다. findByAccessToken 메서드를 추가한다.
package com.endofma.blog.repository;
import com.endofma.blog.domain.Session;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface SessionRepository extends CrudRepository<Session, Long> {
//추가
Optional<Session> findByAccessToken(String accessToken);
}
3. WebMvcConfig 수정
원래 파라미터가 없는 생성자만 등록한 상태였다. @RequiredArgs와 SessionRepository를 추가하고 파라미터를 넣어준다.
//WebMvcConfig.java
package com.endofma.blog.config;
import com.endofma.blog.repository.SessionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
//인터셉터 추가
// @Override
// public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(new AuthInterceptor()) //생성한 인터셉터 등록
//// .addPathPatterns() //인증 적용할 패턴
// .excludePathPatterns("/error", "/favicon.ico"); //인증 제외할 패턴
// }
private final SessionRepository sessionRepository; //blog32: 추가
@Override //인터셉터를 추가해줬듯이 아규먼트 리졸버를 추가해준다.
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthResolver(sessionRepository));
}
}
4. 테스트 코드 수정
//AuthControllerTest.java
@Test
@DisplayName("로그인 후 권한이 필요한 페이지에 접속한다. /foo")
void test4() throws Exception {
//given
//User 엔티티 생성
User user = User.builder()
.name("catnails")
.email("catnails@gmail.com")
.password("1234")
.build();
//User 엔티티에 세션 추가
//accessToken값이 uuid가 지정된 세션 객체가 반환된다. User객체는 이렇게 반환된 세션을 세션 리스트에 저장하여 보유한다.
Session session = user.addSession();
//이렇게 세션까지 보유하게 된 user를 저장한다.
userRepository.save(user);
//expected
mockMvc.perform(get("/foo")
.header("Authorization", session.getAccessToken())
.contentType(APPLICATION_JSON))
// .andExpect(status().isUnauthorized())
.andExpect(status().isOk())
.andDo(print());
}
성공 : Body에 id인 2가 반환된다.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.6)
2023-02-16 12:17:26.456 INFO 22780 --- [ Test worker] c.e.blog.controller.AuthControllerTest : Starting AuthControllerTest using Java 11.0.12 on DESKTOP-Q7HBM41 with PID 22780 (started by user in D:\personal\blog)
2023-02-16 12:17:26.459 INFO 22780 --- [ Test worker] c.e.blog.controller.AuthControllerTest : No active profile set, falling back to 1 default profile: "default"
2023-02-16 12:17:27.587 INFO 22780 --- [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-02-16 12:17:27.701 INFO 22780 --- [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 100 ms. Found 3 JPA repository interfaces.
2023-02-16 12:17:28.425 INFO 22780 --- [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-02-16 12:17:28.766 INFO 22780 --- [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2023-02-16 12:17:28.908 INFO 22780 --- [ Test worker] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-02-16 12:17:28.986 INFO 22780 --- [ Test worker] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.14.Final
2023-02-16 12:17:29.292 INFO 22780 --- [ Test worker] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2023-02-16 12:17:29.525 INFO 22780 --- [ Test worker] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-02-16 12:17:30.699 INFO 22780 --- [ Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-02-16 12:17:30.710 INFO 22780 --- [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-02-16 12:17:31.742 WARN 22780 --- [ Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-02-16 12:17:32.186 INFO 22780 --- [ Test worker] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:blog'
2023-02-16 12:17:32.667 INFO 22780 --- [ Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2023-02-16 12:17:32.668 INFO 22780 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
2023-02-16 12:17:32.670 INFO 22780 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 2 ms
2023-02-16 12:17:32.697 INFO 22780 --- [ Test worker] c.e.blog.controller.AuthControllerTest : Started AuthControllerTest in 6.779 seconds (JVM running for 9.963)
2023-02-16 12:17:33.298 INFO 22780 --- [ Test worker] c.e.blog.controller.PostController : >>> 2
MockHttpServletRequest:
HTTP Method = GET
Request URI = /foo
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Authorization:"4950b232-7c95-48f8-8bea-8d931102feb6"]
Body = null
Session Attrs = {}
Handler:
Type = com.endofma.blog.controller.PostController
Method = com.endofma.blog.controller.PostController#foo(UserSession)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = 2
Forwarded URL = null
Redirected URL = null
Cookies = []
2023-02-16 12:17:33.392 INFO 22780 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-02-16 12:17:33.393 INFO 22780 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2023-02-16 12:17:33.396 WARN 22780 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 90121, SQLState: 90121
2023-02-16 12:17:33.396 ERROR 22780 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-02-16 12:17:33.397 WARN 22780 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 90121, SQLState: 90121
2023-02-16 12:17:33.397 ERROR 22780 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-02-16 12:17:33.397 WARN 22780 --- [ionShutdownHook] o.s.b.f.support.DisposableBeanAdapter : Invocation of destroy method failed on bean with name 'entityManagerFactory': org.hibernate.exception.JDBCConnectionException: Unable to release JDBC Connection used for DDL execution
2023-02-16 12:17:33.397 INFO 22780 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2023-02-16 12:17:33.403 INFO 22780 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 14s
5 actionable tasks: 3 executed, 2 up-to-date
PM 12:17:33: Execution finished ':test --tests "com.endofma.blog.controller.AuthControllerTest.test4"'.
5. 테스트 코드 추가
그러나 정말로 검증된 토큰만 접근이 가능한지 확인하기 위해 테스트 코드를 하나 더 만든다.
즉, 발급되지 않은 토큰을 보내서 인증 절차를 통과하는지 확인한다.
//AuthControllerTest.java
@Test
@DisplayName("로그인 후 검증되지 않은 세션값으로 권한이 필요한 페이지에 접속할 수 없다.")
void test5() throws Exception {
//given
//User 엔티티 생성
User user = User.builder()
.name("catnails")
.email("catnails@gmail.com")
.password("1234")
.build();
//User 엔티티에 세션 추가
//accessToken값이 uuid가 지정된 세션 객체가 반환된다. User객체는 이렇게 반환된 세션을 세션 리스트에 저장하여 보유한다.
Session session = user.addSession();
//이렇게 세션까지 보유하게 된 user를 저장한다.
userRepository.save(user);
//expected
mockMvc.perform(get("/foo")
.header("Authorization", session.getAccessToken() + "-other") //검증되지 않은 세션값을 만들기 위해 스트링을 추가한다.
.contentType(APPLICATION_JSON))
// .andExpect(status().isUnauthorized())
.andExpect(status().isOk())
.andDo(print());
}
정말로 오류가 나는지 확인하기 위해 isOk()로 test5 수행
isOk를 주석처리하고 isUnauthorized의 주석을 해제하였다.
.andExpect(status().isUnauthorized())
// .andExpect(status().isOk())
추후에는 /post 와 같은 핸들러에도 UserSession 객체를 파라미터로 넣어서 인가된 사용자인지를 확인해야 할 것이다. write() 메소드에 사용자의 request뿐만 아니라 userSession.id 처럼 추가 정보 역시 넘겨줄 수 있도록 만들어야 할 것이다.
'Projects > blog' 카테고리의 다른 글
blog 35 : JWT를 이용한 인증 - 만들어보자 (0) | 2023.02.19 |
---|---|
blog33 : 쿠키를 통한 인증 및 검증 (0) | 2023.02.16 |
blog 31 : 세션 토큰 발급기능 추가 2 (0) | 2023.02.15 |
blog 31 : 세션 토큰 발급기능 추가 1 (0) | 2023.02.15 |
blog 30 : 고정인증 로그인 기능 구현 2 (0) | 2023.02.14 |