首頁 > Java > java教程 > 主體

Spring Security 與 JWT

WBOY
發布: 2024-08-16 06:35:32
原創
910 人瀏覽過

Spring Security with JWT

在本文中,我們將探討如何將 Spring Security 與 JWT 集成,為您的應用程式建立堅實的安全層。我們將完成從基本配置到實現自訂身份驗證過濾器的每個步驟,確保您擁有必要的工具來有效且大規模地保護您的 API。

配置

在 Spring Initializr 中,我們將使用 Java 21MavenJar 和這些依賴項來建立一個專案:

  • Spring Data JPA
  • Spring Web
  • 龍目島
  • 春季安全
  • 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 
登入後複製

程式碼

建立安全性設定檔

靠近主函數建立一個目錄 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的配置以保護Web應用程式。此配置包括設定過濾器、保護端點以及套用各種安全規則。

  • @EnableMethodSecurity:是 Spring Security 中的一個註釋,可在 Spring 應用程式中啟用方法級安全性。它允許您使用 @PreAuthorize、@PostAuthorize、@Secured 和 @RolesAllowed 等註解直接在方法層級套用安全規則。

  • privateKeypublicKey:是用於簽署和驗證 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 金鑰。
    • ImmutableJWKSet(new JWKSet(jwk)):將 RSA 金鑰包裝在 JSON Web 金鑰集 (JWKSet) 中,使其不可變。
    • NimbusJwtEncoder(jwks):使用 Nimbus 庫建立 JWT 編碼器,該編碼器將使用 RSA 私鑰對令牌進行簽署。
  • 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

以上是Spring Security 與 JWT的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!