Tool/VSCode&Cursor
Cursor + SpringBoot 개발설정4 + JWT
건담아빠
2024. 12. 13. 15:38
SpringSecurity를 설정하면서 인증은 JWT을 사용하며 accessToken을 발급 받고 refreshToken으로 재발급 받는 부분을 작업해보자.
개발 환경 설정
의존성 추가
- JWT
- Spring Security
- JPA
- H2
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// H2 (테스트용 인메모리 DB)
runtimeOnly 'com.h2database:h2'
환경설정
- application.yml
spring:
application:
name: spring-boot-api
# H2 데이터베이스 설정
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: update
show-sql: true
# H2 콘솔 활성화
h2:
console:
enabled: true
path: /h2-console
jwt:
# secret: your-256-bit-secret-key-here
secret: 12345678901234567890123456789012212313223121321312dsfasdf
access-token-validity: 3600000 # 1시간
refresh-token-validity: 604800000 # 7일
...
SpringSecurity 및 JWT 인증 추가
Auth Controller
로그인 후 accessToken을 발급하고 refresh토큰으로 갱신하는 컨트롤러 추가
- AuthController
package com.test.spring_boot_demo.controller.v1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import com.test.spring_boot_demo.constants.JwtConstants;
import com.test.spring_boot_demo.core.utils.JwtTokenUtil;
import com.test.spring_boot_demo.dto.JwtResponse;
import com.test.spring_boot_demo.dto.LoginRequest;
@RestController
@RequestMapping("/v1/api/auth")
public class AuthController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()));
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String accessToken = jwtTokenUtil.generateAccessToken(userDetails.getUsername());
String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails.getUsername());
return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestHeader(JwtConstants.REFRESH_TOKEN_HEADER) String refreshToken) {
if (jwtTokenUtil.validateToken(refreshToken)) {
String username = jwtTokenUtil.getUsernameFromToken(refreshToken);
String newAccessToken = jwtTokenUtil.generateAccessToken(username);
return ResponseEntity.ok(new JwtResponse(newAccessToken, refreshToken));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
SpringSecurity 및 Filter 설정
- SecurityConfig
package com.test.spring_boot_demo.core.config;
import org.springframework.beans.factory.annotation.Autowired;
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.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;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/v1/api/auth/**").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.requestMatchers("/h2-console/**").permitAll() // H2 콘솔 접근 허용
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
- CustomUserDetailsService
package com.test.spring_boot_demo.core.config;
import org.springframework.security.core.userdetails.User;
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;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// For testing purposes, create a hardcoded user
if ("admin".equals(username)) {
// UserEntity userEntity = userRepository.findByUsername(username);
return User.builder()
.username("admin")
// This is "password" encoded with BCrypt
.password("$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("ADMIN")
.build();
}
throw new UsernameNotFoundException("User not found: " + username);
}
}
- JwtAuthenticationFilter
package com.test.spring_boot_demo.core.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import org.springframework.lang.NonNull;
import com.test.spring_boot_demo.constants.JwtConstants;
import com.test.spring_boot_demo.core.utils.JwtTokenUtil;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
try {
String authHeader = request.getHeader(JwtConstants.HEADER_STRING);
if (authHeader != null && authHeader.startsWith(JwtConstants.TOKEN_PREFIX)) {
String token = authHeader.substring(7);
try {
if (jwtTokenUtil.validateToken(token)) {
String username = jwtTokenUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (io.jsonwebtoken.io.DecodingException e) {
System.err.println("JWT Token 디코딩 실패: " + e.getMessage());
System.err.println("Token: " + token);
} catch (Exception e) {
System.err.println("JWT Token 처리 중 오류 발생: " + e.getMessage());
}
}
filterChain.doFilter(request, response);
} catch (Exception e) {
System.err.println("Filter 처리 중 오류 발생: " + e.getMessage());
throw e;
}
}
}
Utils
- JwtTokenUtil
package com.test.spring_boot_demo.core.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-validity}")
private long accessTokenValidity;
@Value("${jwt.refresh-token-validity}")
private long refreshTokenValidity;
public String generateAccessToken(String username) {
return generateToken(username, accessTokenValidity);
}
public String generateRefreshToken(String username) {
return generateToken(username, refreshTokenValidity);
}
private String generateToken(String username, long validity) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + validity))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
}
Properties
- JwtProperties
package com.test.spring_boot_demo.core.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
private String secret;
private long accessTokenValidity;
private long refreshTokenValidity;
}
Constants
- JwtConstants
package com.test.spring_boot_demo.constants;
public class JwtConstants {
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
public static final String REFRESH_TOKEN_HEADER = "Refresh-Token";
}
Entity
- UserEntity
package com.test.spring_boot_demo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
private String role;
}
Dto
- JwtResponse
package com.test.spring_boot_demo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class JwtResponse {
private String accessToken;
private String refreshToken;
}
- LoginRequest
package com.test.spring_boot_demo.dto;
import lombok.Data;
@Data
public class LoginRequest {
private String username;
private String password;
}
깃허브
https://github.com/dchkang83/react-and-java/tree/main/spring-boot-api