이 기사에서는 Spring Security를 JWT와 통합하여 애플리케이션을 위한 견고한 보안 계층을 구축하는 방법을 살펴보겠습니다. 기본 구성부터 사용자 정의 인증 필터 구현까지 각 단계를 진행하여 API를 효율적이고 대규모로 보호하는 데 필요한 도구를 갖추도록 하겠습니다.
Spring 초기화에서는 Java 21, Maven, Jar 및 다음 종속성을 사용하여 프로젝트를 빌드합니다.
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
이 파일은 스프링 부트 애플리케이션에 대한 구성입니다.
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
주 기능에 더 가깝게 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 기반 인증 규칙을 구성합니다.
oauth2ResourceServer: 인증을 위해 JWT를 사용하는 OAuth 2.0 리소스 서버로 애플리케이션을 구성합니다.
BCryptPasswordEncoder: 이 빈은 BCrypt 해싱 알고리즘을 사용하여 비밀번호를 인코딩하는 비밀번호 인코더를 정의합니다. BCrypt는 무차별 대입 공격에 저항하는 적응형 특성으로 인해 비밀번호를 안전하게 저장하는 데 널리 사용됩니다.
JwtEncoder: 이 Bean은 JWT 토큰 인코딩(서명)을 담당합니다.
JwtDecoder: 이 빈은 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"); } }
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)); } }
위 내용은 JWT를 사용한 스프링 보안의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!