개발/Spring Security(Hodol)

Spring - 데이터베이스를 통한 토큰 검증 / 쿠키를 통한 인증 및 검증

잇(IT) 2023. 10. 6. 00:28
728x90
1. 데이터베이스를 통한 토큰 검증

 

- Test 코드
@Test
    @DisplayName("로그인 후 권한이 필요한 페이지 접속한다. /test2")
    void test4() throws Exception {
        //given
        Member member = Member.builder()
                .name("백인수")
                .email("saymay10@naver.com")
                .password("1234")
                .build();

        Session session = member.addSession();
        //addSeeion()을 하게 되면 Session 객체가 생성되고, Session 객체가 생성되는 것만으로도
        //accessToken 필드에 값이 들어간다.

        memberRepository.save(member);

        //expected
        mockMvc.perform(get("/test2")
                        .header("Authorization", session.getAccessToken())
                        .contentType(APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(print());
    }

1. Member 객체를 생성 및 Session을 생성한 뒤, mockMvc를 통해 /test2 경로로 header에 Authorization의 값으로 session의 Token 값을 넣어 요청을 보낸다.

 

- Controller
@GetMapping("/test2")
    
    public Long test2(UserSession userSession){
        log.info(">>> {}", userSession.id);
        return userSession.id;
    }

1. 파라미터로 UserSession 클래스를 포함하고 있고, UserSession 클래스는 AuthResolver의 supportsParameter() 메서드에 포함되어 있기 때문에 resolveArgument 메서드를 호출하고, resolveArgument 메서드의 반환값이 Usersession 파라미터에 전달된다.

- 위와 같이 Header에 Authorization에 값을 넣어 보내는 것을 확인 할 수 있다.


- AuthResolver 데이터베이스 검증 코드 추가

 

- AuthResolver.java

@Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String accessToken = webRequest.getHeader("Authorization");
        if (accessToken == null || accessToken.equals("")) {
            throw new Unauthorized();
        }

        //데이터베이스 사용자 확인 작업
        Session session = sessionRepository.findByAccessToken(accessToken)
                .orElseThrow(Unauthorized::new);

        return new UserSession(session.getMember().getId());
    }

1. NativeWebRequest를 통해 웹 요청과 관련된 정보를 얻을 수 있고, 현재 위에서 /test2의 경로로 Header의 Authorization 속성의 값으로 accessToken 값이 넘어온 상태이다.

2. accessToken이 존재할 경우 DB에서 해당 accessToken과 일치한 Session 객체를 가져오고 해당 Session의 Member 객체의 id를 가져온다.


* 흐름 정리

1. /auth/login을 통해 사용자가 정상적으로 로그인을 하게 되면, Session을 생성함과 동시에 accessToken을 발급 받고, 해당 Session과 연관관계에 있는 Member 객체의 Session 리스트에 추가된다.

2. /test2 경로의 경우 AuthResolver 클래스에 의해 인증을 거친 이후에 접근 할 수 있는데, 이 때 검증 조건이 DB에 있는 사용자들 중, 요청과 함께 넘어온 Authorization 속성 accessToken의 값과 일치하는 경우 해당 경로의 요청에 대한 응답을 받을 수 있다.


2. 쿠키를 통한 인증 및 검증

 

* 흐름

1. /auth/login을 통해 body로 email, password가 넘어온다.

2. Login(DTO) 클래스를 통해 매핑되어 /auth/login 컨트롤러의 파라미터로 Login 클래스를 받는다.

3. Service 클래스를 통해 controller로 넘어온 login 객체가 DB에 존재하는지 검사한다.

4. DB에 해당 email, password가 존재할 경우 Session 객체를 생성한다. -> Session 객체 생성과 동시에 해당 사용자에 대한 accessToken도 생성된다. 해당 accessToken을 반환한다. 

---

5. ResponseCookie를 통해 응답을 통해 전달할 쿠키 객체를 생성한다. (HttpServletRequest를 통해 Cookie를 생성해도 된다.) 쿠키의 'name' = SESSION, 'value'=accessToken으로 저장한다. 그 외 속성들으 지정해준다.

6. 해당 컨트롤러의 반환값으로 ResponseEntity로 HTTP 응답을 생성한다. (ResponseEntity를 통해 status 상태 코드, header, body 값을 지정하여 응답으로 전달 할 수 있다.)

7. resolveArgument 메서드에는 요청이 들어오면, 요청 header에 cookie를 확인하여, 여러개의 쿠키 중 첫번째 쿠키의 value 즉, SESSION : accessToken 중 accessToken 값을 가져온다. accessToken은 UUID를 통해 생성된 문자열이다.

8. 쿠키를 통해 가져온 accessToken 값이 일치하는 Session을 찾아 해당 Session 객체와 연관관계를 맺은 Member의 Id를 반환하게 되면 해당 사용자는 반복된 인증 없이 쿠키를 통해 사용자를 식별 할 수 있다.


- AuthController.java

@Slf4j
@RestController
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    @PostMapping("/auth/login")
    public ResponseEntity<Object> login(@RequestBody Login login){

        String accessToken = authService.signin(login);

        // 쿠키 만들기
        ResponseCookie cookie = ResponseCookie.from("SESSION", accessToken)
                .domain("localhost") // todo 서버 환경에 따른 분리 필요
                .path("/")
                .httpOnly(true)
                .secure(false)
                .maxAge(Duration.ofDays(30))
                .sameSite("Strict")
                .build();//from을 통해 쿠키의 key, value를 넣을 수 있다.

        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, cookie.toString())
                .build();

    }

1. /auth/login(로그인 경로)를 통해 사용자가 로그인 한 경우 쿠키를 생성한다.

2. HttpServletRequest의 setCookie를 사용하여 Cookie를 생성 할 수 있지만 Spring이 지원하는 ResponseCookie를 이용하여 Set-Cookie와 같이 응답 데이터에 넣을 쿠키를 생성 할 수 있다.

3. Cookie의 name, value를 넣고, 원하는 조건에 맞게 설정한다.

 3.1 ...(속성에 대해서는 추후에 또 알아 볼 것이다.)

4. ResponseEntity를 반홥 타입으로 하여 반환 값으로 Http 응답 header, body, 상태 코드 등을 설정하여 응답 데이터로 넘길 수 있다.

 4.1 현재 응답 데이터로 ok() = 200, header에 위에서 생성한 cookie를 문자열로 생성한 ResponseEntity 객체를 생성하여 응답 데이터로 전달한다.

5. 로그인이 이루어지면 현재 DB에 해당 사용자에 대해 Session 객체 생성 및 accessToken이 발급된다.

 

- PostController.java

@GetMapping("/test2")

    public Long test2(UserSession userSession){
        log.info(">>> {}", userSession.id);
        return userSession.id;
    }

1. 로그인 이후 /test2라는 Controller에 요청을 보내게 되면 Resolver에 의해 인증 과정을 거치게 된다. (파라미터에 Resolver에서 검사를 해야하는 클래스가 존재하기 때문이다.)

 

- AuthResolver.java

@Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
       
       HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
       
       if(servletRequest == null){
            log.error("servletRequest null");
            throw new Unauthorized();
        }
        
        Cookie[] cookies = servletRequest.getCookies();
        if (cookies.length == 0){
            log.error("쿠키가 없음");
            throw new Unauthorized();
        }

        String accessToken = cookies[0].getValue();

        //데이터베이스 사용자 확인 작업
        Session session = sessionRepository.findByAccessToken(accessToken)
                .orElseThrow(Unauthorized::new);

        return new UserSession(session.getMember().getId());
    }

1. 인증을 위한 resolveArgument가 실행되고, webRequest.getNativeRequest() 메서드를 통해 요청에 대한HttpServletRequest 객체를 받아온다.

2. 요청에 대한 null 예외를 처리하고, request에 포함된 cookie를 가져온다. 쿠키도 마찬가지로 예외 처리를 한다.

3. 쿠키에 저장된 value 즉, 'SESSION : accessToken'을 통해 저장한 값 중 accessToken에 대한 값을 받아오고, Session 객체 중 해당 accessToken을 가진 객체가 있는지 확인한다. -> 해당 accessToken을 가진 사용자가 있다면 해당 사용자는 로그인이 확인된 사용자이기 때문에 반복적인 로그인을 통한 인증이 필요없고, 쿠키를 통해 사용자임을 증명할 수 있다.

4. 반환 값으로 해당 인증된 Session 객체를 가진 사용자의 Id를 반환한다. 

 

- auth.http

### 로그인

POST http://localhost:8080/auth/login
Content-Type: application/json
#헤더와 본문을 구분하기 위해 한 줄 띄어야 한다.

{
  "email": "saymay10@naver.com",
  "password": "1234"
}

### 인증 페이지 요청
GET http://localhost:8080/test2
Content-Type: application/json
#Authorization: d012fff2-b893-4ebe-b1f1-f171f9dfc0dd
Cookie: SESSION=f3b91d7b-8f46-4ff1-b379-536ea090bd4c

1. 로그인을 요청하게 되면 해당 body 데이터를 통해 가입된 사용자인지 확인하고, Session 객체 생성 및 accessToken을 생성한다.

2. 추가로 cookie를 생성하여 응답 데이터로 보낸다. 클라이언트는 Cookie를 전달받아 Cookie 저장소에 전달받은 Cookie를 저장한다.

3. 로그인 이후 웹 애플리케이션에서 인증이 필요한 경로로 Cookie의 SESSION = accessToken 값을 포함하여 전달한다. (웹 브라우저에서는 쿠키 저장소에 쿠키를 저장해 두었다가 인증이 필요한 페이지로 요청이 오면 알아서 쿠키 저장소에 저장된 쿠키를 서버로 전달한다.)

4.  /test2 즉, 인증이 필요한 경로로 header에 cookie 값을 포함하여 요청을 전달하게 되면, 서버에서는 request에 해당 cookie 값이 들어가게 되고, 해당 cookie 값에 저장된 name, value를 통해 resolveArgument() 메서드를 통해 인증 로직을 실행 시킨다. request로 전달 받은 SESSION의 accessToken 값이 DB에 존재한다면 해당 사용자는 인증된 사용자임을 알 수 있다.

728x90