https://github.com/newspeed8/newspeed/commit/f3e90739a9f5a1c56c11732f4b29453344bfe4d8
선정 이유 : 세션 기반 로그인 방식의 한계를 극복하기 위해, jwt를 사용함으로써 서버의 상태 관리를 없애 분산 환경에서도 원활한 인증 처리를 통해 stateless 인증 체계로 전환하여 서버 부하를 줄이고 확장성을 개선했습니다
크게 기능을 나누면,
User, Post, Like, comment, profile등이 있습니다.
이에 저는 Post를 담당했습니다.
@Value("${jwt.expirationMs}")
1일로 설정 @Value("${jwt.secret}")
를 통해 토큰 및 유효기간을 숨켰으며, properties에 토큰 세팅해뒀습니다.JPQL 검토 및 오타수정, 성능 향상을 위해 User로만 조인 가능하도록 재구성.
트러블 슈팅에 관련해선 정말 많이 작성할 것이 많지만, 제일 큰 jwt 해결에 대해 작성하려합니다.
세션기반으로 돌아가던 프로젝트를 jwt기반로 동작하도록 코드를 전반적으로 수정했습니다.
jwt를 채택하고 개발한 이유 : 세션 기반 로그인 방식의 한계를 극복하기 위해, jwt를 사용함으로써 서버의 상태 관리를 없애 분산 환경에서도 원활한 인증 처리를 통해 stateless 인증 체계로 전환하여 서버 부하를 줄이고 확장성을 개선했습니다.
업데이트와 삭제 API에서 작성자 본인 여부를 체크하지 않고 있던 코드였습니다.
이로 인해 다른 사용자가 임의로 게시물을 수정하거나 삭제할 수 있는 보안 취약점이 발생할 수 있습니다.
개선 방안: 컨트롤러나 서비스 레이어에서 요청한 사용자가 해당 게시물의 작성자인지 확인하는 로직을 추가합니다.
예를 들어, 보안 컨텍스트 또는 JWT 토큰을 활용해 현재 로그인한 사용자 정보를 가져와 게시물의 작성자와 비교하도록 합니다.
성능 및 보안 개선 효과: 불필요한 데이터 수정/삭제를 방지하여, 잘못된 접근 시 DB 작업을 막음으로써 시스템 부하를 줄이고, 보안성이 향상됩니다.
‼️ 제일 중요한 이유 : jwt를 사용해보고 싶었으며, 지금 아니면 기회가 아니라고 생각되어 jwt에 대해 학습하며 코드 구현을 하였으며, 리팩토링까지 했습니다.
테스트 과정
이렇게 jwt를 정상적으로 코드 작성 후에 postman으로 테스트를 하게 되면, 이런 화면이 보일 것입니다.
아래 화면 같은 경우 "jwt.io"라는 사이트에 접속하셔서 본인의 토큰을 작성하면 userId를 받는지 여부를 확인할 수 있습니다.
다시 말해, jwt.io에 토큰을 붙여넣으면 자동으로 디코딩되어 Header, Payload, Signature 세 부분이 표시됩니다.
아무 추가 조작 없이, 단순히 붙여넣은 후 Payload 영역을 확인하면 됩니다. 여기서 "userId" 클레임이 "userId": "1"
과 같이 포함되어 있는지 확인하시면 됩니다.
즉, 로그인 후 받은 토큰을 jwt.io의 Encoded 필드에 붙여넣고, 자동으로 표시되는 디코딩 결과에서 Payload에 포함된 클레임을 검토하면 됩니다.
발급 받은 토큰을 인증인가 부분의 헤더에
요러케 넣어주시고 필요한 테스트를 진행해주시면 전부 다 정상 동작하는 것을 확인할 수 있습니다.
/auth/login 에서 500Error 발생.
구글링에서 공부한 이전 코드는 아래와 같아서, 혹시라도 최신 버전은 0.11.5라서 다른건가? 라는 생각을 하게 됌 -> 이전에 필터체인이었나? 거기서 최신 버전으로 바뀌면서 문법이 바껴서 코드 적용이 제대로 안되던 기억을 토대로 해당 코드도 그럴까봐 수정.
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
를 jjwt 0.11.5버전 부턴
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS512)
.compact();
로 수정.
이제 나머지 코드를 주석 처리한 부분에 내용을 추가하면 되는 것이었습니다.
즉, 나머지 메서드에서도 동일하게 서명 키를 Key 객체로 반환하는 것이 안전하며,
getEmailFromJwtToken과 validateJwtToken 메서드에서도 jwtSecret을 바로 사용하기보다는 아래와 같이 수정하는 것이 좋겠다고 생각되어 수정했습니다.
public boolean validateJwtToken(String token) {
try {
Jwts.parser().setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))).parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
// 잘못된 JWT 서명
} catch (ExpiredJwtException e) {
// 만료된 JWT
} catch (UnsupportedJwtException e) {
// 지원되지 않는 JWT
} catch (IllegalArgumentException e) {
// JWT가 빈 값
}
return false;
}
이제 코드 수정.
public boolean validateJwtToken(String token) {
try {
Jwts.parser()
.setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)))
.parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
// 잘못된 JWT 서명
System.err.println("Invalid JWT signature: " + e.getMessage());
} catch (ExpiredJwtException e) {
// 만료된 JWT
System.err.println("JWT token is expired: " + e.getMessage());
} catch (UnsupportedJwtException e) {
// 지원되지 않는 JWT
System.err.println("JWT token is unsupported: " + e.getMessage());
} catch (IllegalArgumentException e) {
// JWT가 빈 값
System.err.println("JWT claims string is empty: " + e.getMessage());
}
return false;
}
이렇게 하면, JWT 토큰 생성과 검증 시 모두 동일한 방식으로 키를 처리하게 되어, 보안상 더 안전하며 예외 발생도 줄어듭니다.
코드 설명을 덧 붙이자면, generateJwtToken 메서드에서 jwtSecret을 byte 배열로 변환하여 Keys.hmacShaKeyFor()로 처리하고 있으며, getEmail~~토큰 및 valid토큰 메서드도 동일한 방식으로 key객체를 사용합니다.
-> 이는 jjwt 최신 버전 기준으로 서명 과정에서 발생하는 문제를 해결하는 것입니다.
하지만 호락호락하지 않죠.
아직 403 에러가 뜰 것입니다. 좀 더 확인해봐야합니다 어디서 문제가 있는지.
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/users/signup", "/users/findall", "/users/find/*").permitAll()
.anyRequest().authenticated()
)
기존 LoginFilter 제거 확인
이전에 세션 기반 인증을 위해 작성했던 LoginFilter가 완전히 주석 처리되거나 제거되었는지 확인해야합니다.
만약, LoginFilter가 여전히 등록되어 있는 상태라면, /auth/login 요청이 세션 검증 로직에 의해 거부될 수 있기 때문입니다.
Postman 요청 시 헤더 확인
Authorization 헤더가 포함되어 있지 않은지 확인하면 됩니다.
/auth/login 엔드포인트는 토큰 없이 접근해야 합니다.
Postman에서 요청 시 Authorization 헤더가 비어있거나 불필요하게 포함되어 있으면, JwtAuthenticationFilter가 이를 처리하면서 문제가 발생할 수 있습니다.
-> security 컨피그 설정 정상 확인. 로그인 필터 제거화니 동작 잘 됌.
500 Error.
JWT 토큰 서명에 사용하는 jwt.secret 값의 길이 문제일 수도 있어서 확인.
HS512 알고리즘은 최소 512비트(즉, 64바이트)의 비밀키가 필요한데, 저는
properties에 jwt.secret=aXJYwM+Ehr0bPq5Xv4gX3Ov6E9... 로 설정.
-> 너무 짧았어서 이 값이 64바이트보다 짧아, Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)) 호출 시 IllegalArgumentException이 발생하여 500 에러가 발생했습니다.
~~ 정상 동작~~
팀원 분께서 질의하시길, return user를 사용한 이유?
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional(readOnly = true)
public User login(UserLoginRequestDto dto) {
User user = userRepository.findByEmail(dto.getEmail())
.orElseThrow(() -> new InvalidCredentialException("이메일이 존재하지 않습니다."));
if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
throw new InvalidCredentialException("비밀번호가 틀렸습니다.");
}
return user;
}
}
Auth서비스에 이제 유저로그인응답Dto 대신 user 엔티티를 반환하는 이유는 jwt 토큰 생성시 사용자에 대한 추가 정보를 사용하기 위해서라고 생각했으며, 기존에는 단순히 userId만 반환했다면, 이제는 JwtUtil.generate.JwtToken 메서드에서 이메일을 토큰의 subject로 사용합니다.
즉, User 엔티티 전체를 반환해서 컨트롤러에서 이를 받아 email 정보를 추출하고, 이를 바탕으로 jwt 토큰을 생성할 수 있는 것입니다.
또한, 이렇게 함으로써 로그인 성공 후, 추가 정보를 활용할 수 있으며, SecurityContext 설정 등에도 편리하게 활용할 수 있습니다.
팀원 질문, jwt 기반 인증의 동작 흐름을 알려주세요
->
1. 로그인 요청
클라이언트 요청 : 사용자가 Postman 등에서 로그인 엔드포인트(예: POST /auth/login)를 호출하면서, 이메일과 비밀번호를 JSON으로 전송합니다.
AuthService 처리
- 사용자 조회: AuthService는 전달받은 이메일을 기반으로 데이터베이스에서 사용자를 찾습니다.
JWT 토큰 생성 (AuthController)
- AuthController는 AuthService에서 반환된 User 엔티티를 받고, 그 중에서 이메일 정보를 사용하여 JWT 토큰을 생성.
점점 산으로 가는 것 같다. 쉽게 생각하면
사용자가 이메일과 비밀번호로 /auth/login에 로그인 → AuthService가 사용자 인증 후 User 엔티티 반환 → AuthController가 JwtUtil을 사용해 JWT 토큰 생성 후 클라이언트에 반환.
클라이언트는 보호된 API 호출 시, Authorization 헤더에 JWT 토큰을 포함 → JwtAuthenticationFilter가 토큰을 추출, 검증, 그리고 해당 사용자의 인증 정보를 SecurityContext에 설정 → 보호된 엔드포인트는 SecurityContext의 인증 정보를 확인하여 정상 처리.
팀원 질문, session을 쓸떄는 userId를 찾아와서 jwt에 접근을해서 userId를 가져와야 하는데, 어떻게 가져 올 수 있는지 ??
-> JWT 기반 인증에서 세션처럼 사용자 정보를 바로 가져오려면, JWT 토큰 내에 포함된 클레임(claims)을 통해 userId 같은 정보를 추출해야 합니다. 저의 의도는 클라이언트가 임의로 전달하는 값이 아니라, 인증 토큰 자체에서 안전하게 사용자 정보를 획득하자는 것이 취지였습니다.
코드와 함께 설명드리겠습니다.
public String createToken(String username, Long userId) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("userId", userId); // userId를 클레임에 추가
public Long getUserId(String token) {
return ((Number) Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.get("userId")).longValue();
}
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = tokenProvider.resolveToken(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsername(token);
Long userId = tokenProvider.getUserId(token);
// CustomUserDetails에 userId 포함해서 생성
CustomUserDetails userDetails = new CustomUserDetails(username, userId, /* 권한 정보 등 추가 */);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
그리고 JwtUserDetails는 UserDetails를 구현하면서 추가 정보를 보관할 수 있게 합니다.
public class CustomUserDetails implements UserDetails {
private String username;
private Long userId;
private Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(String username, Long userId, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.userId = userId;
this.authorities = authorities;
}
public Long getUserId() {
return userId;
}
// UserDetails의 다른 메서드 구현
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@GetMapping("/profile")
public ResponseEntity<?> getProfile(@AuthenticationPrincipal CustomUserDetails userDetails) {
Long userId = userDetails.getUserId(); // JWT에서 추출한 userId
// userId를 기반으로 필요한 작업 수행
return ResponseEntity.ok("Hello, userId: " + userId);
}
이는 보편적인 JwtAuthenticationFilter
와 SecurityContextHolder
를 통해 JWT를 검증하고 인증 정보를 설정하는 과정입니다.(구글링) 사용자가 작성한 코드대로, JwtTokenProvider
클래스는 JWT를 생성하고 검증하며, JwtAuthenticationFilter
필터가 요청을 처리할 때 JWT 토큰을 검증합니다. SecurityContextHolder에 인증된 사용자 정보를 설정한 뒤, 컨트롤러에서 @AuthenticationPrincipal
을 통해 이 정보를 가져오는 방식으로 JWT 인증이 작동합니다. 이는 다시 말해 , 사용자가 요청한 대로 JWT 인증을 통해 안전하게 사용자 정보를 처리할 수 있도록 돕기 위한 것입니다.
제 기억이 맞다면, 3~4번은 패스했습니다. 굳이라는 생각이 들었기 떄문입니다.
원칙이라고 생각되지만,
500 에러는 내부에서 발생한 예외 때문에 나타나며, 구체적인 원인을 파악하기 위해서는 서버 로그의 스택 트레이스를 확인해야 합니다.
다만, 아래와같이 몇 가지 일반적인 원인과 점검할 사항들이 있습니다.
application.properties에서 jwt.secret과 jwt.expirationMs가 제대로 설정되고, JwtUtil에 주입되고 있는지 확인.
만약 값이 null이거나 올바르지 않다면, 토큰 생성 시 오류가 발생할 수 있습니다.
사용 중인 jjwt 라이브러리의 버전과 Spring Boot 3.x, jakarta.servlet 관련 의존성이 올바르게 설정되어 있는지 확인하.
Gradle 의존성을 최신 상태로 업데이트한 후, 프로젝트를 클린 빌드해보기.
서버 콘솔 로그에 발생한 구체적인 예외 메시지를 확인하기!!!(개인적으로 제일 중요하다고 생각합니다)
"Invalid key length" 또는 "The signing key's size is not sufficient" 등의 메시지가 보인다면 키 길이가 문제일 가능성이 큽니다.
JwtAuthenticationFilter나 SecurityConfig 설정에서, /auth/login 엔드포인트에 대해 잘못된 접근 제어가 없는지 확인하기.
혹시 다른 보안 필터가 500 에러를 발생시키고 있는지도 점검.
해결하기 위한 나의 추가 노력.
public Long getUserIdFromJwtToken(String token) {
// Bearer 접두사 있으면 제거
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
Claims claims = Jwts.parser()
.setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)))
.parseClaimsJws(token)
.getBody();
String userIdStr = claims.get("userId", String.class);
System.out.println("추출된 userId 클레임: " + userIdStr);
if (userIdStr == null) {
throw new IllegalArgumentException("jwt 토큰에 userId 클레임이 없어요");
}
return Long.parseLong(userIdStr);
지금 로그인은 잘 되고있고, comment부분에서 안되고 있는중...
우선, 로그인은 제대로 동작하는데 comment 관련 부분에서 문제가 발생하면 가장 흔한 원인은 보통
리포지토리와 그 커멘트 부분의 메서드의 코드영역이 문제입니다. (출처 : 구글링) LikeRepository의 countByComment 메서드에서 문제가 발생하는 경우를 확인해보면 됩니다.
여기서 제일 중요한 것은 @Transactional 어노테이션이라고 생각합니다.
기본 조회는 Spring Data JPA가 트랜잭션을 관리하지만, 복잡한 비즈니스 로직에는 명시적으로 @Transactional을 붙이는 것이 좋기 때문에, 이부분도 안되어있어 리팩토링 진행했습니다.
문득, "why 붙이지 않을 수 있을까?"라는 의구심이 들었습니다.
일부 Spring Data JPA Repository 메서드는 이미 내부에서 트랜잭션을 관리합니다.
예를 들어, 단순 조회의 경우 기본적으로 read-only 트랜잭션이 적용됩니다.
그러나 비즈니스 로직을 수행하는 서비스 계층에서는 여러 DB 작업이 연관되어 있을 경우,
명시적으로 @Transactional
어노테이션을 붙여 트랜잭션 경계를 지정하는 것이 좋습니다.
그렇다면, 따라야 하는 권장 사항이 있는 것인가 ?
-> 네.
쓰기 작업(등록, 수정, 삭제)이 포함된 서비스 메서드에는 반드시 @Transactional을 붙이는 것이 좋습니다.
단순 조회의 경우, @Transactional(readOnly=true)를 붙이는 것도 좋습니다.
만약 트랜잭션 어노테이션이 누락된 상태에서 여러 DB 작업이 연관되어 있다면, 데이터 일관성 문제가 발생할 수 있으므로 확인이 필요합니다.
따라서, 만약에 서비스 계층 메서드에 @Transactional 어노테이션이 없다면,
읽기 전용 작업에는 @Transactional(readOnly = true),
쓰기 작업에는 @Transactional을 붙이는 것이 좋습니다.
구글링을 해보니 똑같은 사례를 가진 분을 따라
Authorization부분에 eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtdW5AZXhhbXBsZS5jb20iLCJ1c2VySWQiOiIxIiwiaWF0IjoxNzM5OTc2MTM1LCJleHAiOjE3NDAwNjI1MzV9.u5vjmNSaAf5qKoJtCVhZV6drmU78q_aCnWwIgDuaROh0ettdiQXzjMspSMRTw1fkU8fWxHM-2GfSzkK8rB0qMQ
를 헤더에 넣어서 요청하니 200ok 떳습니다. 😱
Postman에서 JWT 토큰을 Authorization 헤더에 넣어서 테스트하는 방식은 표준적이고 올바른 방식이며,
로그인에 토큰을 생성했으니, request 헤더에 추가로 넣고 진행하면 되는 건데...🤯
"Bearer " 접두사는 HTTP Authorization 헤더에 토큰을 전달할 때 표준적으로 사용하는 형식입니다. 이 접두사는 다음과 같은 역할과 특징을 갖습니다.
What?
Bearer 토큰은 클라이언트가 서버에 인증 정보를 제공하기 위해 사용하는 토큰입니다.
HTTP 요청의 Authorization 헤더에 "Bearer <토큰>" 형식으로 전달됩니다.
여기서 "Bearer"는 인증 방식의 종류를 나타내며, 뒤따르는 실제 토큰 값은 인증에 사용됩니다.
When?
OAuth 2.0 표준 및 JWT 기반 인증 시스템에서, 클라이언트가 서버에 보호된 리소스에 접근할 때 토큰을 전달하는 표준 방식입니다.
예를 들어, 로그인 후 서버에서 발급된 JWT 토큰을 클라이언트가 API 요청 시 포함시켜 인증을 수행할 때 사용됩니다.
How?
클라이언트(예:Postman)는 API 호출 시 Authorization 헤더에 "Bearer <JWT 토큰>"을 설정합니다.
서버에서는 이를 받아서, "Bearer " 접두사를 제거한 후 나머지 토큰 값만 추출하여 검증 및 파싱 작업을 수행합니다.
이를 통해 토큰에 포함된 클레임(예:사용자 ID, 이메일 등)을 확인하고, 인증된 사용자로서 요청을 처리할 수 있게 됩니다.
Why?
JWT 라이브러리나 파싱 로직은 보통 토큰 자체만 필요로 합니다.
그래서 Authorization 헤더에 "Bearer "가 포함되어 있다면, 해당 문자열을 제거하여 순수한 토큰 값만 전달합니다.
예를 들어, 아래와 같은 코드에서
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
이는 토큰 문자열이 "Bearer "로 시작할 경우, 앞의 7글자를 잘라내고 실제 토큰만 남기는 역할을 합니다.
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtdW5AZXhhbXBsZS5jb20iLCJpYXQiOjE2NDY2MzI5NzUsImV4cCI6MTY0NjYzNjU3NX0.XXXXXXX
서버에서는 이 헤더에서 "Bearer "를 제거한 후, 남은 eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtdW5AZXhhbXBsZS5jb20iLCJpYXQiOjE2NDY2MzI5NzUsImV4cCI6MTY0NjYzNjU3NX0.XXXXXXX
토큰 값을 사용하여 JWT를 검증합니다.
정리하자면, "Bearer " 접두사는 토큰 인증 방식의 표준 형식으로, 클라이언트가 토큰을 전달할 때 사용되며, 서버에서는 이 접두사를 제거하여 순수한 토큰 값을 추출하고 검증하는 과정에서 활용되는 것입니다.
인스타그램, 트위터, 페이스북 등과 같이 sns서비스에서 사용자가 CRUD할 수 있고, 친구의 최신 게시물을 뉴스피드로 자유롭게 볼 수 있도록 하며, 사용자 간의 소통과 정보 공유를 원활하게 하는것이 목표입니다.
이를 통해 사용자들이 서로의 일상을 공유하고, 더 나아가 댓글을 통해 정보와 감정을 교류하며, 나아가 플랫폼 내에서 자연스럽게 커뮤니티가 활성화되어 사용자 경험과 참여도를 극대화하는 시너지를 창출하기 위해 서비스를 개발하게 되었습니다.
Jwt 적용한 로그인 및 댓글, 게시물 CRUD 기능
헤당 프로젝트의 서비스는 JWT 기반 인증 시스템을 도입하였으며,
클라이언트가 안전하게 로그인하고, 발급받은 토큰을 통해 보호된 API에 접근할 수 있도록 구현했습니다.
이를 기반으로 사용자는 게시물과 댓글에 대해 CURD를 수행할 수 있습니다. 특히, 게시물과 댓글 수정/삭제 기능은 해당 콘텐츠의 작성자만이 접근할 수 있도록 권한 검증 로직을 포함하여 보안을 강화했습니다. 이와 더불어, 좋아요 및 프로필 기능을 통해 사용자 간의 원활한 소통과 커뮤니티 활성화를 지원하여, 서비스의 전반적인 사용자 경험과 참여도를 극대화하였습니다. JWT를 활용한 인증 및 권한 관리는 서버의 상태 관리 부담을 줄이는 동시에 확장성과 보안성을 확보하는 데 큰 역할을 하고 있습니다.
기존 세션 기반 인증 대신 JWT를 사용하여 인증/인가 로직을 전환하는 방법을 학습했습니다.
이를 통해 로그인 시 서버가 상태 정보를 유지하지 않고도, 클라이언트가 발급받은 토큰을 이용해 보호된 API에 접근할 수 있다는 것을 알게되었으며, Spring Security와 JJWT 라이브러리를 활용하여 토큰 생성, 검증, 그리고 SecurityContext에 인증 정보를 설정하는 과정에서 에러를 자주 만나 jwt를 더욱 자세히 익혔습니다.
저는 JWT와 게시물 관리 영역을 구현하면서, 기존 세션 기반 방식에서 벗어나 보다 확장성과 보안성이 뛰어난 토큰 기반 인증 방식을 경험할 수 있었습니다. 특히, Spring Security의 필터 체인과 SecurityContext를 활용하는 방법을 익히면서, 인증과 권한 관리에 대한 이해도가 크게 향상되었습니다. 전반적으로 코드 리팩토링과 JWT 적용 과정은 도전적이었지만, 결과적으로 해당 프로젝트의 서비스 안정성과 확장성을 높이는 데 큰 도움이 되었다고 느낍니다.
실제로 JWT 기반 인증을 구현하는 과정에서 Spring Security의 필터 체인과 SecurityContext가 내부적으로 활용되고 있으며, 제가 작성한 코드는 JwtAuthenticationFilter는 OncePerRequestFilter를 상속받아 모든 HTTP 요청마다 JWT를 추출하고 검증하며, 유효한 토큰이 있을 경우 SecurityContextHolder에 인증 정보를 설정하도록 구현되었습니다. 이 과정이 바로 Spring Security의 핵심 메커니즘을 활용하는 것이며, 이를 통해 Spring Security의 내부 작동 원리를 더욱 깊이 이해할 수 있었습니다.