todo 프로젝트의 두 번째 기능인 로그인 기능이다.
로그인 기능은 JWT를 이용하여 Client와 Server 모두에 JWT를 직접 Http Header에 담아서 전달하는 방식으로 구현했다.
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
private Key key;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// header에서 JWT 가져오기
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
public String createToken(String username) {
Date date = new Date();
// 토큰 만료시간 60분
long TOKEN_TIME = 60*60*1000;
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
}
@Slf4j(topic = "JWT 검증 및 인가")
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
private final ObjectMapper objectMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.resolveToken(request);
if(Objects.nonNull(token)) {
if(jwtUtil.validateToken(token)) {
Claims info = jwtUtil.getUserInfoFromToken(token);
// 인증정보에 유저정보(username) 넣기
// username -> user 조회
String username = info.getSubject();
SecurityContext context = SecurityContextHolder.createEmptyContext();
// -> userDetails에 담고
UserDetails userDetails = userDetailsService.getUserDetails(username);
// -> authentication의 principal에 담고
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// -> securityContent에 담고
context.setAuthentication(authentication);
// -> securityContextHolder에 담고
SecurityContextHolder.setContext(context);
// -> @AuthenticationPrincipal로 조회 가능
} else {
// 인증정보가 존재하지 않을 때
CommonResponseDto commonResponseDto = new CommonResponseDto("토큰이 유효하지 않습니다.", HttpStatus.BAD_REQUEST.value());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(commonResponseDto));
return;
}
}
filterChain.doFilter(request, response);
}
}
@PostMapping("/login")
public ResponseEntity<CommonResponseDto> login(@RequestBody UserRequestDto userRequestDto, HttpServletResponse response) {
try {
userService.login(userRequestDto);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(new CommonResponseDto(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
}
response.setHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(userRequestDto.getUsername()));
return ResponseEntity.ok().body(new CommonResponseDto("로그인 성공", HttpStatus.OK.value()));
}
public void login(UserRequestDto userRequestDto) {
String username = userRequestDto.getUsername();
String password = userRequestDto.getPassword();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new IllegalArgumentException("등록된 유저가 없습니다."));
if(!passwordEncoder.matches(password, user.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
}


