-
Cursor + SpringBoot 개발설정4 + JWTTool/VSCode&Cursor 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
'Tool > VSCode&Cursor' 카테고리의 다른 글
JWT란? (2) 2024.12.13 Cursor + SpringBoot 개발설정3 + openapi (0) 2024.12.13 Cursor + SpringBoot 개발설정2 (0) 2024.12.11 Cursor Activity Bar 변경 (0) 2024.12.11 Cursor + SpringBoot 개발설정1 (0) 2024.12.11