DB를 통한 토큰 발급 및 검증 1
지금까지 공부한 지식을 활용해서 한번 마음대로 인증 로직을 작성할것이다. 인증 과정은 아래와 같다.
- 로그인 성공 시 응답으로 세션 토큰 발급
- 인증이 필요한 페이지에 요청 시 헤더에 담긴 세션 토큰 정보로 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());
}
}
테스트는 성공하고 성공 시 응답값으로 세션토큰 또한 발급되는걸 확인할 수 있다.
너무 길어져서 발급된 세션 토큰을 이용해 권한 인증이 필요한 페이지에 접속하는건 다음글로 넘기겠다.
'API 만들어 보기 > 게시판 API' 카테고리의 다른 글
[API 인증] 쿠키를 통한 인증 및 검증 (0) | 2023.10.03 |
---|---|
[API 인증] DB를 통한 토큰 발급 및 검증 2 (0) | 2023.10.02 |
[API 인증] ArgumentResolver 사용해보기 (0) | 2023.09.30 |
[API 인증] Interceptor 사용해보기 (0) | 2023.09.29 |
[API 인증] 가장 기본적인 요청 인증값 확인 (0) | 2023.09.28 |