JWT を使用した Spring Security

WBOY
リリース: 2024-08-16 06:35:32
オリジナル
909 人が閲覧しました

Spring Security with JWT

この記事では、Spring Security を JWT と統合してアプリケーションの強固なセキュリティ層を構築する方法を検討します。基本構成からカスタム認証フィルターの実装までの各ステップを説明し、API を効率的かつ大規模に保護するために必要なツールを確実に入手できるようにします。

構成

Spring Initializr では、Java 21MavenJar、およびこれらの依存関係を使用してプロジェクトを構築します。

  • Spring Data 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 Boot アプリケーションの構成です。

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 を使用すると、Web アプリケーションを保護するための Spring Security の構成が自動的にトリガーされます。この構成には、フィルターのセットアップ、エンドポイントの保護、さまざまなセキュリティ ルールの適用が含まれます。

  • @EnableMethodSecurity: Spring アプリケーションでメソッドレベルのセキュリティを有効にする Spring Security のアノテーションです。 @PreAuthorize、@PostAuthorize、@Secured、@RolesAllowed などのアノテーションを使用して、セキュリティ ルールをメソッド レベルで直接適用できます。

  • privateKey および publicKey: JWT の署名と検証に使用される RSA 公開キーと秘密キーです。 @Value アノテーションは、プロパティ ファイル (application.properties) からこれらのフィールドにキーを挿入します。

  • CSRF: CSRF (クロスサイト リクエスト フォージェリ) 保護を無効にします。これは、認証に 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: この Bean は、BCrypt ハッシュ アルゴリズムを使用してパスワードをエンコードするパスワード エンコーダを定義します。 BCrypt は、その適応性が高く、ブルート フォース攻撃に耐性があるため、パスワードを安全に保存するための一般的な選択肢です。

  • JwtEncoder: この Bean は、JWT トークンのエンコード (署名) を担当します。

    • RSAKey.Builder: 提供された公開 RSA キーと秘密 RSA キーを使用して新しい RSA キーを作成します。
    • ImmutableJWKSet<>(new JWKSet(jwk)): RSA キーを JSON Web キー セット (JWKSet) でラップし、不変にします。
    • NimbusJwtEncoder(jwks): Nimbus ライブラリを使用して、RSA 秘密キーでトークンに署名する JWT エンコーダーを作成します。
  • JwtDecoder: この Bean は、JWT トークンのデコード (検証) を担当します。

    • NimbusJwtDecoder.withPublicKey(publicKey).build(): RSA 公開キーを使用して JWT デコーダーを作成します。これは、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 を使用した Spring Securityの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!