개요
보안을 강화하기 위해 JWT를 적용해보자.
JWT 토큰의 인증방식은 다음 포스팅에서 살펴보고 바로 개발해보도록 하겠다.
2023.02.10 - [공부/Tech] - 인증 방식 (Cookie & Session & Token)
인증 방식 (Cookie & Session & Token)
서버가 클라이언트를 인증하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. 각각의 특징에 대해서 간단하게 살펴보자. 1. Cookie (쿠키) 쿠키는 Key-Value 방식으로 저장되는 문자열이다.
taek2.tistory.com
기능 구현
- JWT 라이브러리 추가
<build.gradle>
dependencies {
...
// JWT Token
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
}
실무에서는 JJWT 0.9.1 버전으로 사용했는데, JDK 버전을 17로 바꿔서 하니 ClassNotFoundException 오류가 떴었다. 알아보니 JDK 11 이상에서는 'javax.xml.bind.DatatypeConverter' 클래스가 포함되지 않아서 발생했던 오류이다. JJWT 0.9.1 버전은 JDK 11 이상에서 사용하기 적합하지 않다고 한다.
따라서, 버전을 JJWT 0.11.2 로 올려서 종속성을 추가했다.
주의할 점은, JJWT 0.11.2 버전은 종속성 라이브러리를 각각 추가해줘야한다. 이전처럼 'io.jsonwebtoken:jjwt:0.11.2' 만 추가하면 필요한 모든 종속성이 포함되지 않아서 오류가 발생할 수 있다.
- JWT 유틸리티 클래스 생성
: 토큰 생성 및 검증을 담당할 유틸리티 클래스
<JwtUtil.java>
package com.example.auth.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512);
private static final long ACCESS_TOKEN_VALIDITY = 1000 * 60 * 15; // 15 minutes
private static final long REFRESH_TOKEN_VALIDITY = 1000 * 60 * 60 * 24 * 7; // 7 days
public String generateAccessToken(String memberId) {
return generateToken(memberId, ACCESS_TOKEN_VALIDITY);
}
public String generateRefreshToken(String memberId) {
return generateToken(memberId, REFRESH_TOKEN_VALIDITY);
}
private String generateToken(String memberId, long validity) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(memberId)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + validity))
.signWith(SECRET_KEY)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public String getMemberIdFromToken(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
- SecurityConfig 수정
: JWT 필터를 추가하여 요청을 검증한다.
<SecurityConfig.java>
public class SecurityConfig {
private final JwtUtil jwtUtil;
public SecurityConfig(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/register").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
...
public static class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && jwtUtil.validateToken(token)) {
String memberId = jwtUtil.getMemberIdFromToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(memberId, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
}
- DTO 업데이트
: 로그인 응답을 위한 DTO를 추가한다.
<LoginResponse.java>
package com.example.auth.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class LoginResponse {
private String accessToken;
private String refreshToken;
}
- MemberService 업데이트
: 로그인 시 JWT 토큰을 생성해서 반환한다.
<MemberService.java>
@Service
public class MemberService {
...
@Autowired PasswordEncoder passwordEncoder;
@Autowired JwtUtil jwtUtil;
public LoginResponse validateMember(LoginRequest loginRequest) {
Member member = memberRepository.findByMemberId(loginRequest.getId())
.orElseThrow(() -> new UsernameNotFoundException("User not found with id: " + loginRequest.getId()));
if (passwordEncoder.matches(loginRequest.getPw(), member.getPassword())) {
String accessToken = jwtUtil.generateAccessToken(member.getMemberId());
String refreshToken = jwtUtil.generateRefreshToken(member.getMemberId());
return new LoginResponse(accessToken, refreshToken);
} else {
throw new RuntimeException("Invalid credentials");
}
}
...
}
- MemberController 업데이트
<MemberController.java>
public class MemberController {
...
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest loginRequest) {
return memberService.validateMember(loginRequest);
}
...
}
검증
- Token 발급 검증
검증 방법은 전 포스팅과 같고 결과만 보겠다.
accessToken과 refreshToken이 둘다 발급되는 것을 확인할 수 있다.
'백엔드 개발 > Spring&JPA' 카테고리의 다른 글
[Spring Data JPA] p6spy 커스텀 포맷, 로그 파일, 로그 레벨 설정 (0) | 2024.08.29 |
---|---|
[Spring Data JPA] Spring boot 3에 p6spy 적용하기 (4) | 2024.08.28 |
[Spring Data JPA] 로그인 기능 구현(2) - Password Encode (0) | 2024.06.25 |
[Spring Data JPA] 로그인 기능 구현 (1) | 2024.06.24 |
좋은 객체 지향 설계의 5가지 원칙 (SOLID) (0) | 2023.03.09 |