2024. 1. 3. 14:22ㆍ백엔드/Spring Boot
오늘은 Spring Security를 이용하여 JWT 토큰을 발급받고, 토큰의 만료 시간을 검증하는 기능을 개발하려고 합니다.
Spring Boot 3.0 이상에서는 Spring Security 설정이 변경되었으므로 구글링을 통해 새로운 방법을 참고해야 합니다.
개발은 경록김님의 유튜브를 참고하여 진행했습니다. JWT 토큰 발급 및 검증을 위한 유틸리티 클래스를 작성하고, Spring Security 설정 클래스를 생성하여 JWT 토큰 사용을 설정했습니다. 추후에는 스프링 시큐리티에 관한 게시글도 작성해보겠습니다.
# JWT(JSON Web Token)
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
[JWT를 사용하는 경우]
JWT는 정보를 안전하게 전송하기 위한 간단하고 경량의 토큰 기반 인증 방식으로 회원인증과 정보 교류 시 아주 유용하게 사용된다.
회원인증(로그인)
: 서버가 클라이언트의 요청을 받을 때 마다 헤더에 있는 토큰 값을 검증하여 사용자에게 권한이 있는 경우에 응답을 하기 때문에 서버 입장에서는 사용자가 로그인되어 있느지에 대하여 확인을 할 필요가 없어진다.
정보 교류
: 정보가 이미 서명이 되어 있기에 정보를 보낸이가 바뀌었는지 정보가 도중에 조작이 되었는지에 대하여 검증할 수 있다.
[JWT의 구조]
JWT 토큰은 헤더(header), 페이로드(payload), 서명(signature) 의 구조로 이루어져 있으며 각 구역이 . 기호로 구분된다.
토큰을 Header에 넣어 전송할 때에는 토큰 앞에 `Bearer` 이라는 문자열을 추가하여 전송한다.
(이것은 암묵적인 룰이라고 한다...)
✍️ 로그인 후 토큰을 리턴 받는 코드를 만들어보자.
Controller
@RestController
@Slf4j // logging
@RequestMapping("/users")
@RequiredArgsConstructor // final or nonNull 필드 생성자 자동 생성
public class UserController {
private final UserService userService;
@PostMapping("/login")
public ApiResponse login(@RequestBody @Valid UserLoginRequestDto userLoginRequestDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
log.info(bindingResult.toString());
return ApiResponse.error(404, "err");
}
String token = userService.login(userLoginRequestDto);
return ApiResponse.success(token);
}
}
Service
@Service
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final ModelMapper modelMapper;
public UserService(UserRepository userRepository, ModelMapper modelMapper) {
this.userRepository = userRepository;
this.modelMapper = modelMapper;
}
// 로그인
@Value("${jwt.secret-key}") // 토큰 시크릿 키 (application.properties에 작성한 값)
private String secretKey;
private Long expiredMs = 1000 * 60L; // 토큰 유효기간
public String login(UserLoginRequestDto userLoginRequestDto) {
// 인증과정 생략
return JwtUtil.createToken(userLoginRequestDto.getEmail(), secretKey, expiredMs);
}
}
Security Config - 스프링 시큐리티 관련 설정
- permitAll() 설정을 하지 않은 페이지는 403 에러가 발생
- 공식문서를 참고하여 작성하는것을 추천
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
// 생성자
private final UserService userService;
@Value("${jwt.secret-key}")
private String secretKey;
// PasswordEncoder는 BCryptPasswordEncoder를 사용
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic(AbstractHttpConfigurer::disable) // UI기반의 인증차앙이 뜨는것을 비활성화
.formLogin(AbstractHttpConfigurer::disable) // form 로그인 기능 비활성화
.csrf(AbstractHttpConfigurer::disable) // rest api이므로 csrf 보안이 필요없으므로 비활성화 처리
.cors(Customizer.withDefaults())
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/users/login", "/users/join").permitAll()
.requestMatchers(HttpMethod.POST).authenticated()
.anyRequest().authenticated()
)
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}
JwtUtil - 토큰을 발급받고, 만료 시간을 검증하는 코드
public class JwtUtil {
// token에서 특정 정보 꺼내기
public static String getUserEmail(String token, String secretKey) {
return Jwts.parserBuilder().setSigningKey(secretKey)
.build().parseClaimsJws(token)
.getBody().get("userEmail", String.class);
}
// Token 만료시간 확인
public static boolean isExpired(String token, String secretKey) {
return Jwts.parserBuilder()
.setSigningKey(secretKey).build()
.parseClaimsJws(token).getBody().getExpiration().before(new Date());
}
// Token 발급
public static String createToken(String userEmail, String secretKey, Long expiredMs) {
Claims claims = Jwts.claims();
claims.put("userEmail", userEmail);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}
JwtFilter
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private final UserService userService;
private final String secretKey;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info("authorization : {}", authorization);
if (authorization == null || !authorization.startsWith("Bearer ")) {
log.error("authorization을 잘못 보냈습니다.");
filterChain.doFilter(request, response);
return ;
}
// Token 꺼내기
String token = authorization.split(" ")[1];
// Token 기간 만료 여부 확인
if (JwtUtil.isExpired(token, secretKey)) {
log.error("토큰이 만료되었습니다.");
filterChain.doFilter(request, response);
return;
}
// get user email
String userEmail = JwtUtil.getUserEmail(token, secretKey);
log.info("user email : {} ", userEmail);
// 권한 부여
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userEmail, null, List.of(new SimpleGrantedAuthority("USER")));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
ExceptionManager - 런타임 Exception 발생 시 실행되는 코드
@RestControllerAdvice
public class ExceptionManager {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<?> runtimeExceptionHandler(RuntimeException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage());
}
}
오늘은 간략하게만 개발을 했는데 여기서 검증에 관련된 코드, 예외 처리에 대한 코드를 공부를 하면서 추가해가면 좋을 것 같다.
'백엔드 > Spring Boot' 카테고리의 다른 글
[Spring Security] Security Config 파일 만들기 (0) | 2024.01.04 |
---|---|
[Spring Security] 의존성 추가해보기 (1) | 2024.01.03 |
DTO ↔ Entity 변환하기 (1) | 2023.11.16 |
JPA - Entity 생성을 위한 어노테이션 (0) | 2023.11.15 |
JPA : Java Persistence API (1) | 2023.11.15 |