Api서버로 구축한 스프링 부트 프로젝트에서 웹 브라우저가 아닌 윈도우폼 같은 클라이언트에 적합한 세션 관리 방법을 찾아보니 JWT![]
가 있었다.
JWT(JSON Web Token)는 JSON 포맷으로 작성된 인증 정보를 포함하는 토큰으로, 웹 애플리케이션 간 안전한 데이터 교환에 사용됩니다. JWT는 주로 사용자 인증 및 정보 전달에 사용되며, 간단하고 독립적인 방법으로 데이터를 검증할 수 있다는 장점이 있습니다.
exp)을 설정할 수 있어 보안성을 강화할 수 있습니다.| 특징 | JWT | 세션 기반 인증 |
|---|---|---|
| 상태 저장 | 서버가 상태를 저장하지 않음 | 서버가 세션 상태를 저장함 |
| 스케일링 | 확장성 좋음 | 확장성 낮음 |
| 클라이언트-서버 의존성 | 의존성 적음 | 의존성이 높음 |
| 토큰 저장 위치 | 클라이언트 (쿠키, 로컬 스토리지 등) | 서버 |
Edit Starters에서 Spring Security 추가

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 처리
# JWT 설정
## 256비트 이상의 랜덤 비밀키
jwt.secret=256비트 이상의 랜덤 비밀키
# 만료 시간 (밀리초, 1시간)
jwt.expiration=3600000
package com.okdk.board.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "jwt") // jwt.* 설정값을 매핑
public class JwtProperties {
private String secret; // 비밀키
private long expiration; // 만료 시간
}
설정 프로퍼티 등록 클래스를 생성하여 위 properties에서 설정 프로퍼티 정의.
package com.okdk.board.util;
import com.okdk.board.config.JwtProperties;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@RequiredArgsConstructor
public class JwtUtil {
private final JwtProperties jwtProperties;
// JWT 생성
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username) // 사용자 이름
.setIssuedAt(new Date()) // 현재 시간
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration())) // 만료 시간
.signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()), SignatureAlgorithm.HS256)
.compact();
}
// JWT 검증
public String validateToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject(); // 토큰에서 사용자 이름 추출
} catch (JwtException e) {
return null; // 유효하지 않은 토큰
}
}
}
package com.okdk.board.filter;
import com.okdk.board.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.substring(7); // "Bearer "를 제외한 토큰 추출
String username = jwtUtil.validateToken(token); // 토큰 검증 및 사용자명 추출
if (username != null) { // 유효한 토큰일 경우
UserDetails userDetails = User.withUsername(username)
.password("") // 비밀번호는 JWT 기반 인증에 필요하지 않음
.authorities(Collections.emptyList()) // 권한 리스트를 빈 값으로 설정
.build();
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보 설정
}
}
filterChain.doFilter(request, response); // 다음 필터로 요청 전달
}
}
package com.okdk.board.config;
import com.okdk.board.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // csrf 설정 비활성화
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/user/login").permitAll() // 로그인은 인증 필요 없음
.anyRequest().authenticated() // 나머지 요청은 인증 필요
)
.addFilterBefore(jwtAuthenticationFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class)
.build();
}
}
/api/user/login로 들어온 로그인 url 요청을 제외한 나머지 요청은 인증이 필요하도록 설정@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserApiController {
private final UserService userService;
// 로그인 기능
@PostMapping("/login")
public ResponseEntity<HashMap<String, Object>> login(@RequestBody LoginRequest loginRequest) {
return userService.authenticate(loginRequest);
}
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
// 사용자 로그인
@Transactional
public ResponseEntity<HashMap<String, Object>> authenticate(LoginRequest loginRequest) {
HashMap<String, Object> retMap = new HashMap<String, Object>();
// 사용자 조회
Optional<User> result = userRepository.findByUserId(loginRequest.getUserId());
if (result.isPresent()) {
User user = result.get();
if (user.getUserPassword().equals(loginRequest.getUserPassword())) {
// JWT 토큰 생성
String token = jwtUtil.generateToken(user.getUserId());
// 사용자 정보와 성공 메시지 추가
retMap.put("resUser", toDto(user)); // User 엔티티를 DTO로 변환
retMap.put("resMsg", "SUCCESS"); // 성공 메시지
// JWT를 헤더에 추가하고 응답 반환
return ResponseEntity.ok()
.header("Authorization", "Bearer " + token) // JWT를 헤더에 추가
.body(retMap); // 본문에 메시지와 사용자 정보 포함
} else {
// 비밀번호 불일치 처리
retMap.put("resMsg", "PASSWORD_INCORRECT");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(retMap);
}
} else {
// 사용자 없음 처리
retMap.put("resMsg", "USER_NOT_FOUND");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(retMap);
}
}

포스트맨으로 다음과 같이 요청을 보낸다
응답 결과


헤더를 확인해보면 인증 토큰이 담겨있다

위와같이 조회요청을 보낼때

Authorization 탭에서 Auth Type을 Bearer Token으로 선택하고 로그인때 응답받은 토큰을 복사에서 붙여 넣는다
결과

200으로 조회가 정상적으로 잘 완료된 모습
정상적으로 구현이 완료 되었다면 조회 요청같은 로그인 요청이 아닌 요청이 들어오면 보안 필터에서 토큰을 통해 검증이 완료되어야 정상적인 응답이 온다

검증이 실해하면 403 Forbidden에러 발생