Rumah  >  Artikel  >  Java  >  Keselamatan Musim Bunga dengan JWT

Keselamatan Musim Bunga dengan JWT

WBOY
WBOYasal
2024-08-16 06:35:32904semak imbas

Spring Security with JWT

Dalam artikel ini, kami akan meneroka cara mengintegrasikan Spring Security dengan JWT untuk membina lapisan keselamatan yang kukuh untuk aplikasi anda. Kami akan melalui setiap langkah, daripada konfigurasi asas kepada melaksanakan penapis pengesahan tersuai, memastikan anda mempunyai alatan yang diperlukan untuk melindungi API anda dengan cekap dan pada skala.

Konfigurasi

Di Spring Initializr kami akan membina projek dengan Java 21, Maven, Jar dan kebergantungan ini:

  • Data Musim Bunga JPA
  • Jaman Musim Bunga
  • Lombok
  • Keselamatan Musim Bunga
  • Pemandu PostgreSQL
  • Pelayan Sumber OAuth2

Sediakan pangkalan data PostgreSQL

Dengan Docker anda akan mencipta pangkalan data PostgreSql dengan Docker-compose.
Buat fail docker-compose.yaml di akar projek anda.

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:

Jalankan arahan untuk memulakan bekas.

docker compose up -d

Sediakan fail application.properties

Fail ini ialah konfigurasi untuk aplikasi but spring.

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

kunci jwt.public.key dan jwt.private.key ialah kunci yang akan kami buat selanjutnya.

Hasilkan kunci peribadi dan awam

JANGAN PERNAH serahkan kunci tersebut pada github anda

Jalankan pada konsol untuk menjana kunci peribadi di direktori sumber

cd src/main/resources
openssl genrsa > private.key

Selepas itu, buat kunci awam yang dipautkan kepada kunci persendirian.

openssl rsa -in private.key -pubout -out public.key 

Kod

Buat fail SecurityConfig

Lebih dekat dengan fungsi utama buat konfigurasi direktori dan di dalamnya fail 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();
    }
}

Penjelasan

  • @EnableWebScurity: Apabila anda menggunakan @EnableWebSecurity, ia secara automatik mencetuskan konfigurasi Spring Security untuk melindungi aplikasi web. Konfigurasi ini termasuk menyediakan penapis, menjamin titik akhir dan menggunakan pelbagai peraturan keselamatan.

  • @EnableMethodSecurity: ialah anotasi dalam Spring Security yang mendayakan keselamatan peringkat kaedah dalam aplikasi Spring anda. Ia membolehkan anda menggunakan peraturan keselamatan secara langsung pada peringkat kaedah menggunakan anotasi seperti @PreAuthorize, @PostAuthorize, @Secured dan @RolesAllowed.

  • privateKey dan publicKey: ialah kunci awam dan peribadi RSA yang digunakan untuk menandatangani dan mengesahkan JWT. Anotasi @Value menyuntik kunci daripada fail sifat (application.properties) ke dalam medan ini.

  • CSRF: Melumpuhkan perlindungan CSRF (Cross-Site Request Forgery), yang sering dilumpuhkan dalam REST API tanpa kewarganegaraan di mana JWT digunakan untuk pengesahan.

  • authorizeHttpRequests: Mengkonfigurasi peraturan kebenaran berasaskan URL.

    • requestMatchers(HttpMethod.POST, "/signin").permitAll(): Membenarkan akses yang tidak disahkan ke titik akhir /signin dan /login, bermakna sesiapa sahaja boleh mengakses laluan ini tanpa log masuk.
    • anyRequest().authenticated(): Memerlukan pengesahan untuk semua permintaan lain.
  • oauth2ResourceServer: Mengkonfigurasi aplikasi sebagai pelayan sumber OAuth 2.0 yang menggunakan JWT untuk pengesahan.

    • config.jwt(jwt -> jwt.decoder(jwtDecoder())): Menentukan kacang penyahkod JWT (jwtDecoder) yang akan digunakan untuk menyahkod dan mengesahkan token JWT.
  • BCryptPasswordEncoder: Kacang ini mentakrifkan pengekod kata laluan yang menggunakan algoritma pencincangan BCrypt untuk mengekod kata laluan. BCrypt ialah pilihan popular untuk menyimpan kata laluan dengan selamat kerana sifat penyesuaiannya, menjadikannya tahan terhadap serangan kekerasan.

  • JwtEncoder: Kacang ini bertanggungjawab untuk pengekodan (menandatangani) token JWT.

    • RSAKey.Builder: Mencipta kunci RSA baharu menggunakan kunci RSA awam dan peribadi yang disediakan.
    • ImmutableJWKSet<>(JWKSet(jwk) baharu): Membungkus kekunci RSA dalam JSON Web Key Set (JWKSet), menjadikannya tidak berubah.
    • NimbusJwtEncoder(jwks): Menggunakan perpustakaan Nimbus untuk mencipta pengekod JWT yang akan menandatangani token dengan kunci peribadi RSA.
  • JwtDecoder: Kacang ini bertanggungjawab untuk menyahkod (mengesahkan) token JWT.

    • NimbusJwtDecoder.withPublicKey(publicKey).build(): Mencipta penyahkod JWT menggunakan kunci awam RSA, yang digunakan untuk mengesahkan tandatangan token JWT.

Entiti

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);
    }
}

Repositori

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);
}

Perkhidmatan

Khidmat Pelanggan

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);
    }
}

Perkhidmatan Token

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;

    }
}

Pengawal

Pengawal Pelanggan

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

Atas ialah kandungan terperinci Keselamatan Musim Bunga dengan JWT. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel sebelumnya:Pembelajaran Graf Set TerpisahArtikel seterusnya:Pembelajaran Graf Set Terpisah