✨개요
🏃 목표
📢 로그인 기능을 구현하자.
📢 요구사항
- Spring Security와 JWT를 활용하여 구현한다.
- 로그인 성공 시 토큰을 리턴하고,
userName
과 password
가 틀릴 시 예외처리를 한다.
- POST
/login
- 입력폼 (JSON 형식)
{
"userName" : "user1",
"password" : "user1234"
}
- 리턴 (JSON 형식)
{
"resultCode": "SUCCESS",
"result": {
"jwt": "eyJhbGciOiJIU",
}
}
✅ TO-DO
🔧 구현
로그인 테스트 작성
<@WebMvcTest
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
UserService userService;
@Autowired
ObjectMapper objectMapper;
@Test
@DisplayName("로그인 성공")
@WithMockUser
void login_SUCCESS()throws Exception{
String userName = "홍길동1";
String password = "0000";
when(userService.login(any(),any()))
.thenReturn(new UserLoginResponse("token"));
mockMvc.perform(post("/api/v1/users/login")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new UserLoginRequest(userName, password))))
.andDo(print())
.andExpect(status().isOk());
}
@Test
@DisplayName("로그인 실패_userName이 존재하지 않는 경우")
@WithMockUser
void login_FAILED_userName()throws Exception{
String userName = "홍길동1";
String password = "0000";
when(userService.login(any(),any()))
.thenThrow(new AppException(ErrorCode.USERNAME_NOT_FOUND,ErrorCode.USERNAME_NOT_FOUND.getMessage()));
mockMvc.perform(post("/api/v1/users/login")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new UserLoginRequest(userName, password))))
.andDo(print())
.andExpect(status().isNotFound());
}
@Test
@DisplayName("로그인 실패_password가 다른 경우")
@WithMockUser
void login_FAILED_password()throws Exception{
String userName = "홍길동1";
String password = "0000";
when(userService.login(any(),any()))
.thenThrow(new AppException(ErrorCode.INVALID_PASSWORD,ErrorCode.INVALID_PASSWORD.getMessage()));
mockMvc.perform(post("/api/v1/users/login")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new UserLoginRequest(userName, password))))
.andDo(print())
.andExpect(status().isUnauthorized());
}
}
로그인 컨트롤러 구현
UserController 메서드 추가
@PostMapping("/login")
public ResponseEntity<Response> login(@RequestBody UserLoginRequest userLoginRequest){
UserLoginResponse userLoginResponse = userService.login(userLoginRequest.getUserName(),userLoginRequest.getPassword());
return ResponseEntity.ok().body(Response.toResponse("SUCCESS",userLoginResponse));
}
UserLoginRequest 구현
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Setter
@Getter
public class UserLoginRequest {
private String userName;
private String password;
}
UserLoginResponse 구현
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class UserLoginResponse {
private String token;
public static UserLoginResponse of(String token){
return UserLoginResponse.builder()
.token(token)
.build();
}
}
로그인 서비스 구현
UserService 메서드 추가
public UserLoginResponse login(String userName, String password) {
User selectedUser = userRepository.findByUserName(userName).orElseThrow(() -> {
throw new AppException(ErrorCode.USERNAME_NOT_FOUND,ErrorCode.USERNAME_NOT_FOUND.getMessage());
});
if(!encoder.matches(password,selectedUser.getPassword())){
throw new AppException(ErrorCode.INVALID_PASSWORD,ErrorCode.INVALID_PASSWORD.getMessage());
}
return UserLoginResponse.of("token");
}
JWT 토큰 발행
jjwt 의존성 추가
implementation 'io.jsonwebtoken:jjwt:0.9.1'
JwtTokenUtil
public class JwtTokenUtil {
public static String createToken(String userName, String key, long expireTimeMs) {
Claims claims = Jwts.claims();
claims.put("userName", userName);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expireTimeMs))
.signWith(SignatureAlgorithm.HS256, key)
.compact();
}
}
UserService 수정
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder encoder;
@Value("${jwt.token.key}")
private String key;
private long expireTimeMs = 1000 * 60 * 60l;
public UserJoinResponse join(String userName, String password) {
userRepository.findByUserName(userName).ifPresent(user -> {
throw new AppException(ErrorCode.DUPLICATED_USER_NAME, ErrorCode.DUPLICATED_USER_NAME.getMessage());
});
User savedUser = User.of(userName, encoder.encode(password));
savedUser = userRepository.save(savedUser);
UserJoinResponse userJoinResponse = UserJoinResponse.of(savedUser.getUserId(), savedUser.getUserName());
return userJoinResponse;
}
public UserLoginResponse login(String userName, String password) {
User selectedUser = userRepository.findByUserName(userName).orElseThrow(() -> {
throw new AppException(ErrorCode.USERNAME_NOT_FOUND, ErrorCode.USERNAME_NOT_FOUND.getMessage());
});
if (!encoder.matches(password, selectedUser.getPassword())) {
throw new AppException(ErrorCode.INVALID_PASSWORD, ErrorCode.INVALID_PASSWORD.getMessage());
}
String token = JwtUtil.createToken(selectedUser.getUserName(), key, expireTimeMs);
return UserLoginResponse.of(token);
}
}
환경변수 추가
jwt:
token:
key: ${KEY}
🌉 회고
- 예전에는 로그인까지 부랴부랴 구현을 할 수 있었지만 토큰 발행은 구현하기 힘들었다.
- 오늘 프로젝트를 진행하면서 토큰 발행의 대략적인 과정을 조금 이해하게 된 것 같다.