JWT를 사용한 스프링 보안
Aug 16, 2024 am 06:35 AM이 기사에서는 Spring Security를 JWT와 통합하여 애플리케이션을 위한 견고한 보안 계층을 구축하는 방법을 살펴보겠습니다. 기본 구성부터 사용자 정의 인증 필터 구현까지 각 단계를 진행하여 API를 효율적이고 대규모로 보호하는 데 필요한 도구를 갖추도록 하겠습니다.
구성
Spring 초기화에서는 Java 21, Maven, Jar 및 다음 종속성을 사용하여 프로젝트를 빌드합니다.
- 스프링 데이터 JPA
- 스프링웹
- 롬복
- 스프링 시큐리티
- PostgreSQL 드라이버
- OAuth2 리소스 서버
PostgreSQL 데이터베이스 설정
Docker를 사용하면 Docker-compose를 사용하여 PostgreSql 데이터베이스를 생성하게 됩니다.
프로젝트 루트에 docker-compose.yaml 파일을 생성하세요.
services: postgre: image: postgres:latest ports: - "5432:5432" environment: - POSTGRES_DB=database - POSTGRES_USER=admin - POSTGRES_PASSWORD=admin volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data:
컨테이너를 시작하려면 명령을 실행하세요.
docker compose up -d
application.properties 파일 설정
이 파일은 스프링 부트 애플리케이션에 대한 구성입니다.
spring.datasource.url=jdbc:postgresql://localhost:5432/database spring.datasource.username=admin spring.datasource.password=admin spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true jwt.public.key=classpath:public.key jwt.private.key=classpath:private.key
jwt.public.key 및 jwt.private.key는 앞으로 생성할 키입니다.
개인 및 공개 키 생성
절대 해당 키를 Github에 커밋하지 마세요
콘솔에서 실행하여 리소스 디렉터리에 개인 키를 생성하세요
cd src/main/resources openssl genrsa > private.key
이후 개인키와 연결된 공개키를 생성합니다.
openssl rsa -in private.key -pubout -out public.key
암호
SecurityConfig 파일 만들기
주 기능에 더 가깝게 configs 디렉터리를 만들고 그 안에 SecurityConfig.java 파일을 만듭니다.
import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.web.SecurityFilterChain; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Value("${jwt.public.key}") private RSAPublicKey publicKey; @Value("${jwt.private.key}") private RSAPrivateKey privateKey; @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.POST, "/signin").permitAll() .requestMatchers(HttpMethod.POST, "/login").permitAll() .anyRequest().authenticated()) .oauth2ResourceServer(config -> config.jwt(jwt -> jwt.decoder(jwtDecoder()))); return http.build(); } @Bean BCryptPasswordEncoder bPasswordEncoder() { return new BCryptPasswordEncoder(); } @Bean JwtEncoder jwtEncoder() { var jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build(); var jwks = new ImmutableJWKSet<>(new JWKSet(jwk)); return new NimbusJwtEncoder(jwks); } @Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withPublicKey(publicKey).build(); } }
설명
@EnableWebScurity: @EnableWebSecurity를 사용하면 웹 애플리케이션 보안을 위한 Spring Security의 구성이 자동으로 트리거됩니다. 이 구성에는 필터 설정, 엔드포인트 보안, 다양한 보안 규칙 적용이 포함됩니다.
@EnableMethodSecurity: Spring 애플리케이션에서 메서드 수준 보안을 활성화하는 Spring Security의 주석입니다. @PreAuthorize, @PostAuthorize, @Secured 및 @RolesAllowed와 같은 주석을 사용하여 메서드 수준에서 직접 보안 규칙을 적용할 수 있습니다.
privateKey 및 publicKey: JWT 서명 및 확인에 사용되는 RSA 공개 및 개인 키입니다. @Value 주석은 속성 파일(application.properties)의 키를 이러한 필드에 삽입합니다.
CSRF: CSRF(Cross-Site Request Forgery) 보호를 비활성화합니다. 이는 JWT가 인증에 사용되는 상태 비저장 REST API에서 종종 비활성화됩니다.
-
authorizeHttpRequests: URL 기반 인증 규칙을 구성합니다.
- requestMatchers(HttpMethod.POST, "/signin").permitAll(): /signin 및 /login 엔드포인트에 대한 인증되지 않은 액세스를 허용합니다. 즉, 누구나 로그인하지 않고도 이러한 경로에 액세스할 수 있습니다.
- anyRequest().authenticated(): 다른 모든 요청에는 인증이 필요합니다.
-
oauth2ResourceServer: 인증을 위해 JWT를 사용하는 OAuth 2.0 리소스 서버로 애플리케이션을 구성합니다.
- config.jwt(jwt -> jwt.decoder(jwtDecoder())): JWT 토큰을 디코딩하고 유효성을 검사하는 데 사용되는 JWT 디코더 Bean(jwtDecoder)을 지정합니다.
BCryptPasswordEncoder: 이 빈은 BCrypt 해싱 알고리즘을 사용하여 비밀번호를 인코딩하는 비밀번호 인코더를 정의합니다. BCrypt는 무차별 대입 공격에 저항하는 적응형 특성으로 인해 비밀번호를 안전하게 저장하는 데 널리 사용됩니다.
-
JwtEncoder: 이 Bean은 JWT 토큰 인코딩(서명)을 담당합니다.
- RSAKey.Builder: 제공된 공개 및 비공개 RSA 키를 사용하여 새 RSA 키를 생성합니다.
- ImmutableJWKSet<>(new JWKSet(jwk)): RSA 키를 JSON 웹 키 세트(JWKSet)로 래핑하여 변경할 수 없게 만듭니다.
- NimbusJwtEncoder(jwks): Nimbus 라이브러리를 사용하여 RSA 개인 키로 토큰에 서명하는 JWT 인코더를 만듭니다.
-
JwtDecoder: 이 빈은 JWT 토큰의 디코딩(검증)을 담당합니다.
- NimbusJwtDecoder.withPublicKey(publicKey).build(): JWT 토큰의 서명을 확인하는 데 사용되는 RSA 공개 키를 사용하여 JWT 디코더를 생성합니다.
실재
import org.springframework.security.crypto.password.PasswordEncoder; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Entity @Table(name = "tb_clients") @Getter @Setter @NoArgsConstructor public class ClientEntity { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) @Column(name = "client_id") private Long clientId; private String name; @Column(unique = true) private String cpf; @Column(unique = true) private String email; private String password; @Column(name = "user_type") private String userType = "client"; public Boolean isLoginCorrect(String password, PasswordEncoder passwordEncoder) { return passwordEncoder.matches(password, this.password); } }
저장소
import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import example.com.challengePicPay.entities.ClientEntity; @Repository public interface ClientRepository extends JpaRepository<ClientEntity, Long> { Optional<ClientEntity> findByEmail(String email); Optional<ClientEntity> findByCpf(String cpf); Optional<ClientEntity> findByEmailOrCpf(String email, String cpf); }
서비스
클라이언트 서비스
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import example.com.challengePicPay.entities.ClientEntity; import example.com.challengePicPay.repositories.ClientRepository; @Service public class ClientService { @Autowired private ClientRepository clientRepository; @Autowired private BCryptPasswordEncoder bPasswordEncoder; public ClientEntity createClient(String name, String cpf, String email, String password) { var clientExists = this.clientRepository.findByEmailOrCpf(email, cpf); if (clientExists.isPresent()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email/Cpf already exists."); } var newClient = new ClientEntity(); newClient.setName(name); newClient.setCpf(cpf); newClient.setEmail(email); newClient.setPassword(bPasswordEncoder.encode(password)); return clientRepository.save(newClient); } }
토큰 서비스
import java.time.Instant; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import example.com.challengePicPay.repositories.ClientRepository; @Service public class TokenService { @Autowired private ClientRepository clientRepository; @Autowired private JwtEncoder jwtEncoder; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; public String login(String email, String password) { var client = this.clientRepository.findByEmail(email) .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email not found")); var isCorrect = client.isLoginCorrect(password, bCryptPasswordEncoder); if (!isCorrect) { throw new BadCredentialsException("Email/password invalid"); } var now = Instant.now(); var expiresIn = 300L; var claims = JwtClaimsSet.builder() .issuer("pic_pay_backend") .subject(client.getEmail()) .issuedAt(now) .expiresAt(now.plusSeconds(expiresIn)) .claim("scope", client.getUserType()) .build(); var jwtValue = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); return jwtValue; } }
컨트롤러
클라이언트 컨트롤러
package example.com.challengePicPay.controllers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import example.com.challengePicPay.controllers.dto.NewClientDTO; import example.com.challengePicPay.entities.ClientEntity; import example.com.challengePicPay.services.ClientService; @RestController public class ClientController { @Autowired private ClientService clientService; @PostMapping("/signin") public ResponseEntity<ClientEntity> createNewClient(@RequestBody NewClientDTO client) { var newClient = this.clientService.createClient(client.name(), client.cpf(), client.email(), client.password()); return ResponseEntity.status(HttpStatus.CREATED).body(newClient); } @GetMapping("/protectedRoute") @PreAuthorize("hasAuthority('SCOPE_client')") public ResponseEntity<String> protectedRoute(JwtAuthenticationToken token) { return ResponseEntity.ok("Authorized"); } }
Explanation
The /protectedRoute is a private route that can only be accessed with a JWT after logging in.
The token must be included in the headers as a Bearer token, for example.
You can use the token information later in your application, such as in the service layer.
@PreAuthorize: The @PreAuthorize annotation in Spring Security is used to perform authorization checks before a method is invoked. This annotation is typically applied at the method level in a Spring component (like a controller or a service) to restrict access based on the user's roles, permissions, or other security-related conditions.
The annotation is used to define the condition that must be met for the method to be executed. If the condition evaluates to true, the method proceeds. If it evaluates to false, access is denied,"hasAuthority('SCOPE_client')": It checks if the currently authenticated user or client has the specific authority SCOPE_client. If they do, the method protectedRoute() is executed. If they don't, access is denied.
Token Controller: Here, you can log in to the application, and if successful, it will return a token.
package example.com.challengePicPay.controllers; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import example.com.challengePicPay.controllers.dto.LoginDTO; import example.com.challengePicPay.services.TokenService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @RestController public class TokenController { @Autowired private TokenService tokenService; @PostMapping("/login") public ResponseEntity<Map<String, String>> login(@RequestBody LoginDTO loginDTO) { var token = this.tokenService.login(loginDTO.email(), loginDTO.password()); return ResponseEntity.ok(Map.of("token", token)); } }
Reference
- Spring Security
- Spring Security-Toptal article
위 내용은 JWT를 사용한 스프링 보안의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

인기 기사

인기 기사

뜨거운 기사 태그

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











Java의 클래스로드 메커니즘은 다른 클래스 로더 및 대표 모델을 포함하여 어떻게 작동합니까?

2025 년 상위 4 개의 JavaScript 프레임 워크 : React, Angular, Vue, Svelte

고급 Java 프로젝트 관리, 구축 자동화 및 종속성 해상도에 Maven 또는 Gradle을 어떻게 사용합니까?

캐싱 및 게으른 하중과 같은 고급 기능을 사용하여 객체 관계 매핑에 JPA (Java Persistence API)를 어떻게 사용하려면 어떻게해야합니까?

카페인 또는 구아바 캐시와 같은 라이브러리를 사용하여 자바 애플리케이션에서 다단계 캐싱을 구현하려면 어떻게해야합니까?
