DB를 통한 토큰 발급 및 검증 1

지금까지 공부한 지식을 활용해서 한번 마음대로 인증 로직을 작성할것이다. 인증 과정은 아래와 같다.

  1. 로그인 성공 시 응답으로 세션 토큰 발급
  2. 인증이 필요한 페이지에 요청 시 헤더에 담긴 세션 토큰 정보로 DB 에서 확인

 


1. 로그인 시 세션 토큰 발급

data.sql 을 이용해 DB에 초기데이터를 넣어둔다. 초기 데이터 설정은 이 글을 참고한다.

로그인 요청 데이터가 DB 데이터와 일치하면 세션 토큰(accessToken)을 발급하고 DB에 이 세션 토큰을 을 저장한다.

 

AuthController

JSON 을 통해 로그인 요청을 받는다. 로그인 성공 시 세션 토큰을 발급한다.

@PostMapping("/auth/login")
public SessionResponse login(@RequestBody LoginRequest request) {
    // DB 에서 조회
    String accessToken = authService.signin(request);
    // 토큰을 응답
    return new SessionResponse(accessToken);
}

 

LoginRequest

@Getter
@ToString
@NoArgsConstructor
public class LoginRequest {

    @NotBlank(message = "이메일을 입력해주세요.")
    private String email;

    @NotBlank(message = "비밀번호를 입력해주세요.")
    private String password;

    @Builder
    public LoginRequest(String email, String password) {
        this.email = email;
        this.password = password;
    }
}

 

AuthService

DB 데이터와 비교하여 일치하면 해당 유저에게 세션 토근을 발급한다.

User는 Session과 연관관계가 맺어져있으므로 유저가 세션을 발급받으면 트랜잭션 종료 시 세션 정보가 DB에 저장된다.

@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository userRepository;

    @Transactional
    public String signin(LoginRequest request) {
        User user = userRepository.findByEmailAndPassword(request.getEmail(), request.getPassword())
            .orElseThrow(() -> new InvalidSigninInformationException());

        Session session = user.addSession();
        return session.getAccessToken();
    }
}

 

InvalidSigninInformationException

로그인 요청 실패시 발생하는 커스텀 예외.

public class InvalidSigninInformationException extends DunpleException{

    private static final String MESSAGE = "아이디/비밀번호가 올바르지 않습니다.";

    public InvalidSigninInformationException() {
        super(MESSAGE);
    }

    @Override
    public int getStatusCode() {
        return HttpStatus.BAD_REQUEST.value();
    }
}

 

User

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    private String password;

    private LocalDateTime createdAt;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
    private List<Session> sessions = new ArrayList<>();

    @Builder
    public User(String name, String email, String password) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.createdAt = LocalDateTime.now();
    }

    public Session addSession() {
        Session session = Session.builder()
            .user(this)
            .build();
        sessions.add(session);
        return session;
    }
}

 

Session

세션토큰은 UUID 로 발급한다.

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Session {


    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    private String accessToken;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;

    @Builder
    public Session(User user) {
        this.accessToken = randomUUID().toString();
        this.user = user;
    }
}

 

SessionRespostiory

public interface SessionRepository extends JpaRepository<Session, Long> {
    Optional<Session> findByAccessToken(String accessToken);
}

 


테스트

AuthServiceTest

로그인 요청 성공 시 발급 받은 세션 토큰과 DB 의 세션 토큰이 일치하는지 확인하는 테스트와 로그인 실패 시

예외가 발생하는지 확인하는 테스트를 작성한다.

@SpringBootTest
class AuthServiceTest {

    @Autowired
    private AuthService authService;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SessionRepository sessionRepository;

    @AfterEach
    void tearDown() {
        sessionRepository.deleteAll();
    }

    @DisplayName("로그인 요청 시 DB 정보와 일치하면 accessToken 을 발급한다.")
    @Test
    void signin() throws JsonProcessingException {
        // given
        LoginRequest request = LoginRequest.builder()
            .email("hyukkind@naver.com")
            .password("1234")
            .build();

        // when
        String accessToken = authService.signin(request);
        Session session = sessionRepository.findByAccessToken(accessToken)
            .orElseThrow(() -> new InvalidSigninInformationException());

        // then
        assertEquals(accessToken, session.getAccessToken());
        assertEquals(1L, sessionRepository.count());
    }

    @DisplayName("로그인 요청 시 DB 정보와 일치하지 않으면 예외가 발생한다.")
    @Test
    void signin2() throws JsonProcessingException {
        // given
        LoginRequest request = LoginRequest.builder()
            .email("hyukkind@naver.com")
            .password("1111")
            .build();

        // expected
        assertThrows(
            InvalidSigninInformationException.class,
            () -> authService.signin(request)
        );
    }

 

테스트는 성공하고 의도한 대로 동작함을 알 수 있다.

 


AuthControllerTest

로그인 요청 시 DB 를 통해 성공 및 실패를 확인하는 테스트를 작성한다.

@SpringBootTest
@AutoConfigureMockMvc
class AuthControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private SessionRepository sessionRepository;
    
    @AfterEach
    void tearDown() {
        sessionRepository.deleteAll();
    }

    @DisplayName("로그인 성공")
    @Test
    void login() throws Exception {
        // given
        LoginRequest request = LoginRequest.builder()
            .email("hyukkind@naver.com")
            .password("1234")
            .build();

        String json = objectMapper.writeValueAsString(request);

        // expected
        mockMvc.perform(
            post("/auth/login")
                .contentType(APPLICATION_JSON)
                .content(json)
            )
            .andDo(print())
            .andExpect(status().isOk());

        assertEquals(1L, sessionRepository.count());
    }

    @DisplayName("로그인 실패")
    @Test
    void login2() throws Exception {
        // given
        LoginRequest request = LoginRequest.builder()
            .email("hyukkind@naver.com")
            .password("1111")
            .build();

        String json = objectMapper.writeValueAsString(request);

        // expected
        mockMvc.perform(
                post("/auth/login")
                    .contentType(APPLICATION_JSON)
                    .content(json)
            )
            .andDo(print())
            .andExpect(status().isBadRequest());

        assertEquals(0, sessionRepository.count());
    }
}

 

테스트는 성공하고 성공 시 응답값으로 세션토큰 또한 발급되는걸 확인할 수 있다.

 

너무 길어져서 발급된 세션 토큰을 이용해 권한 인증이 필요한 페이지에 접속하는건 다음글로 넘기겠다.

+ Recent posts