> Java > java지도 시간 > 본문

JWT를 사용한 스프링 보안

WBOY
풀어 주다: 2024-08-16 06:35:32
원래의
909명이 탐색했습니다.

Spring Security with JWT

이 기사에서는 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와 같은 주석을 사용하여 메서드 수준에서 직접 보안 규칙을 적용할 수 있습니다.

  • privateKeypublicKey: 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!