반응형
2. JWT 유틸리티 클래스 (Spring Boot 3 버전)
JwtUtil.java
package com.example.jwt.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration-time}")
private long accessTokenExpTime;
private SecretKey key;
@PostConstruct
public void init() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
// JWT 토큰 생성
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime tokenValidity = now.plusSeconds(accessTokenExpTime);
return Jwts.builder()
.subject(authentication.getName())
.claim("auth", authorities)
.issuedAt(Date.from(now.toInstant()))
.expiration(Date.from(tokenValidity.toInstant()))
.signWith(key, Jwts.SIG.HS256)
.compact();
}
// JWT 토큰에서 Authentication 정보 추출
public Authentication getAuthentication(String token) {
Claims claims = parseClaims(token);
if (claims.get("auth") == null) {
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
}
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("auth").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(
principal, "", authorities);
}
// JWT 토큰 유효성 검사
public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(key).build().parseSignedClaims(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.", e);
}
return false;
}
// JWT Claims 추출
private Claims parseClaims(String token) {
try {
return Jwts.parser().verifyWith(key).build()
.parseSignedClaims(token).getPayload();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
// 토큰에서 사용자명 추출
public String getUsernameFromToken(String token) {
return parseClaims(token).getSubject();
}
}
3. JWT 필터 (Spring Boot 3 버전)
JwtAuthenticationFilter.java
package com.example.jwt.filter;
import com.example.jwt.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.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. Request Header에서 JWT 토큰 추출
String jwt = resolveToken(request);
// 2. validateToken으로 토큰 유효성 검사
if (StringUtils.hasText(jwt) && jwtUtil.validateToken(jwt)) {
// 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
Authentication authentication = jwtUtil.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
// Request Header에서 토큰 정보를 꺼내오는 메서드
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
}
4. Entity 클래스들 (Spring Boot 3 버전)
User.java
package com.example.jwt.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.Set;
@Entity
@Table(name = "users")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", length = 50, unique = true)
private String email;
@Column(name = "password")
private String password;
@Column(name = "name", length = 50)
private String name;
@Column(name = "created_date")
private LocalDateTime createdDate;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_authority",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")}
)
private Set<Authority> authorities;
}
Authority.java
package com.example.jwt.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "authority")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Authority {
@Id
@Column(name = "authority_name", length = 50)
private String authorityName;
}
5. DTO 클래스들
LoginDto.java
package com.example.jwt.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginDto {
@NotBlank
private String email;
@NotBlank
private String password;
}
TokenDto.java
package com.example.jwt.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class TokenDto {
private String jwt;
private String type;
private String email;
public TokenDto(String jwt) {
this.jwt = jwt;
this.type = "Bearer";
}
}
6. Custom UserDetailsService
CustomUserDetailsService.java
package com.example.jwt.service;
import com.example.jwt.entity.User;
import com.example.jwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(final String email) {
return userRepository.findByEmail(email)
.map(user -> createUser(email, user))
.orElseThrow(() -> new UsernameNotFoundException(email + " -> 데이터베이스에서 찾을 수 없습니다."));
}
private org.springframework.security.core.userdetails.User createUser(String email, User user) {
List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getEmail(),
user.getPassword(),
grantedAuthorities);
}
}
7. Repository
UserRepository.java
package com.example.jwt.repository;
import com.example.jwt.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
8. Controller
AuthController.java
package com.example.jwt.controller;
import com.example.jwt.dto.LoginDto;
import com.example.jwt.dto.TokenDto;
import com.example.jwt.util.JwtUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@CrossOrigin(origins = "http://localhost:3000")
public class AuthController {
private final JwtUtil jwtUtil;
private final AuthenticationManager authenticationManager;
@PostMapping("/authenticate")
public ResponseEntity<?> authenticate(@RequestBody @Valid LoginDto loginDto) {
try {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
String jwt = jwtUtil.createToken(authentication);
return ResponseEntity.ok(new TokenDto(jwt, "Bearer", loginDto.getEmail()));
} catch (AuthenticationException e) {
return ResponseEntity.status(401).body("인증에 실패했습니다.");
}
}
@GetMapping("/user")
public ResponseEntity<String> getMyUserInfo(Authentication authentication) {
return ResponseEntity.ok("안녕하세요, " + authentication.getName() + "님!");
}
}
9. Security 설정 (Spring Boot 3 버전)
SecurityConfig.java
package com.example.jwt.config;
import com.example.jwt.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF 비활성화
.csrf(AbstractHttpConfigurer::disable)
// CORS 설정
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// H2 콘솔을 위한 설정
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
// 세션 관리 상태 없음으로 구성 (JWT 사용)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// FormLogin, BasicHttp 비활성화
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
// JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 앞에 추가
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 권한 규칙 작성
.authorizeHttpRequests(authorize -> authorize
// 인증이 필요하지 않은 URL
.requestMatchers("/api/authenticate", "/h2-console/**").permitAll()
// 나머지 요청은 인증 필요
.anyRequest().authenticated()
);
return http.build();
}
// CORS 설정
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD", "POST", "GET", "DELETE", "PUT", "PATCH"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
10. 설정 파일
application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:testdb
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
h2:
console:
enabled: true
jwt:
secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHaVlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa
expiration-time: 86400 # 1일 (초 단위)
반응형