」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > Spring Security 與 JWT

Spring Security 與 JWT

發佈於2024-11-08
瀏覽:901

Spring Security with JWT

In this article, we will explore how to integrate Spring Security with JWT to build a solid security layer for your application. We will go through each step, from basic configuration to implementing a custom authentication filter, ensuring you have the necessary tools to protect your APIs efficiently and at scale.

Configuration

At the Spring Initializr we're gonna build a project with Java 21, Maven, Jar and these dependencies:

  • Spring Data JPA
  • Spring Web
  • Lombok
  • Spring Security
  • PostgreSQL Driver
  • OAuth2 Resource Server

Set up the PostgreSQL database

With Docker you're going to create a PostgreSql database with Docker-compose.
Create a docker-compose.yaml file at the root of you project.

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:

Run the command to start the container.

docker compose up -d

Set up application.properties file

This file is the configuration for the spring boot application.

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

The jwt.public.key and jwt.private.key are keys that we are going to create further.

Generate the private and public keys

NEVER commit those keys to your github

Run at the console to generate the private key at the resources directory

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

After, create the public key linked to the private key.

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

Code

Create a SecurityConfig file

Closer to the main function create a directory configs and inside that a SecurityConfig.java file.

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

Explanation

  • @EnableWebScurity: When you use @EnableWebSecurity, it automatically triggers Spring Security's configuration for securing web applications. This configuration includes setting up filters, securing endpoints, and applying various security rules.

  • @EnableMethodSecurity: is an annotation in Spring Security that enables method-level security in your Spring application. It allows you to apply security rules directly at the method level using annotations like @PreAuthorize, @PostAuthorize, @Secured, and @RolesAllowed.

  • privateKey and publicKey: are the RSA public and private keys used for signing and verifying JWTs. The @Value annotation injects the keys from the properties file(application.properties) into these fields.

  • CSRF: Disables CSRF (Cross-Site Request Forgery) protection, which is often disabled in stateless REST APIs where JWT is used for authentication.

  • authorizeHttpRequests: Configures URL-based authorization rules.

    • requestMatchers(HttpMethod.POST, "/signin").permitAll(): Allows unauthenticated access to the /signin and /login endpoints, meaning anyone can access these routes without being logged in.
    • anyRequest().authenticated(): Requires authentication for all other requests.
  • oauth2ResourceServer: Configures the application as an OAuth 2.0 resource server that uses JWT for authentication.

    • config.jwt(jwt -> jwt.decoder(jwtDecoder())): Specifies the JWT decoder bean (jwtDecoder) that will be used to decode and validate the JWT tokens.
  • BCryptPasswordEncoder: This bean defines a password encoder that uses the BCrypt hashing algorithm to encode passwords. BCrypt is a popular choice for securely storing passwords due to its adaptive nature, making it resistant to brute-force attacks.

  • JwtEncoder: This bean is responsible for encoding (signing) JWT tokens.

    • RSAKey.Builder: Creates a new RSA key using the provided public and private RSA keys.
    • ImmutableJWKSet(new JWKSet(jwk)): Wraps the RSA key in a JSON Web Key Set (JWKSet), making it immutable.
    • NimbusJwtEncoder(jwks): Uses the Nimbus library to create a JWT encoder that will sign tokens with the RSA private key.
  • JwtDecoder: This bean is responsible for decoding (verifying) JWT tokens.

    • NimbusJwtDecoder.withPublicKey(publicKey).build(): Creates a JWT decoder using the RSA public key, which is used to verify the signature of JWT tokens.

Entity

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

Repository

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 {
    Optional findByEmail(String email);

    Optional findByCpf(String cpf);

    Optional findByEmailOrCpf(String email, String cpf);
}

Services

Client Service

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

Token Service

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;

    }
}

Controllers

Client Controller

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 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 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> 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
版本聲明 本文轉載於:https://dev.to/mspilari/spring-security-with-jwt-2bl6?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 如何在Java字串中正確地將單反斜線替換為雙反斜線?
    如何在Java字串中正確地將單反斜線替換為雙反斜線?
    在字符串中用雙反斜杠替換單反斜杠當嘗試使用replaceAll將像“\something\”這樣的字串轉換為“\”時某事”,開發人員經常遇到錯誤。使用ReplaceAll("\", "\\")方法的常見方法會導致異常「java.util.regex.Patte...
    程式設計 發佈於2024-12-22
  • 儘管在 Eclipse 的查找和替換中工作,為什麼我的 Java Regex 電子郵件驗證失敗?
    儘管在 Eclipse 的查找和替換中工作,為什麼我的 Java Regex 電子郵件驗證失敗?
    Java 正規表示式電子郵件驗證出現問題在嘗試使用正規表示式驗證電子郵件地址時,Java 使用者遇到了以下問題:即使對於格式正確的電子郵件地址,驗證也會失敗。儘管事實上,當在 Eclipse 中的「尋找和取代」功能中使用正規表示式時,該正規表示式會符合電子郵件地址,但在與 Java 的 Patter...
    程式設計 發佈於2024-12-22
  • Bootstrap 4 Beta 中的列偏移發生了什麼事?
    Bootstrap 4 Beta 中的列偏移發生了什麼事?
    Bootstrap 4 Beta:列偏移的刪除和恢復Bootstrap 4 在其Beta 1 版本中引入了重大更改柱子偏移了。然而,隨著 Beta 2 的後續發布,這些變化已經逆轉。 從 offset-md-* 到 ml-auto在 Bootstrap 4 Beta 1 中, offset-md-*...
    程式設計 發佈於2024-12-22
  • 為什麼在將 std::min/max 與 #define NOMINMAX 一起使用時會出現錯誤?
    為什麼在將 std::min/max 與 #define NOMINMAX 一起使用時會出現錯誤?
    使用std::min/max 和#define NOMINMAX在main.cpp 檔案的最近更新中,您引入了以下預處理器指令: #define NOMINMAX #include <Windows.h> #include <algorithm>此操作可讓您在程式碼中使用 s...
    程式設計 發佈於2024-12-22
  • JHat 如何協助識別和調試 Java 記憶體洩漏?
    JHat 如何協助識別和調試 Java 記憶體洩漏?
    使用JHat 識別Java 中的記憶體洩漏在Java 中查找記憶體洩漏可能具有挑戰性,但是JHat(JDK 中包含的一個工具)提供有關堆使用情況的寶貴見解。雖然 JHat 提供了堆分配的基本視圖,但要找出記憶體洩漏的根本原因可能很困難。本文提供了一種系統方法來識別大型物件樹並定位導致記憶體洩漏的潛在...
    程式設計 發佈於2024-12-22
  • 大批
    大批
    方法是可以在物件上呼叫的 fns 數組是對象,因此它們在 JS 中也有方法。 slice(begin):將陣列的一部分提取到新數組中,而不改變原始數組。 let arr = ['a','b','c','d','e']; // Usecase: Extract till index ...
    程式設計 發佈於2024-12-22
  • 使用 Python 的「w+」檔案模式有什麼意義?
    使用 Python 的「w+」檔案模式有什麼意義?
    Python 文件模式的混亂"w 在Python 中,有許多文件模式允許您以不同的方式與文件交互'w '就是這樣一種模式,它引起了一些混亂,讓我們澄清一下它的用法:理解文件。模式開啟一個檔案以進行寫入和更新。 &&&]為了更清楚地了解不同的文件模式,這裡有一個表格概述了它們...
    程式設計 發佈於2024-12-22
  • Java 的內建功能如何可靠地驗證電子郵件地址?
    Java 的內建功能如何可靠地驗證電子郵件地址?
    探討 Java 中的電子郵件驗證方法電子郵件地址的有效性在各種應用中至關重要。雖然 Apache Commons Validator 一直是 Java 電子郵件驗證的熱門選擇,但開發人員經常尋求替代解決方案。本文深入研究了使用官方 Java 電子郵件包驗證電子郵件地址的綜合方法。 isValidEm...
    程式設計 發佈於2024-12-22
  • 掌握 JavaScript 中的對象
    掌握 JavaScript 中的對象
    JavaScript 中的对象 在 JavaScript 中,对象是键值对的集合,其中值可以是数据(属性)或函数(方法)。对象是 JavaScript 的基础,因为 JavaScript 中几乎所有内容都是对象,包括数组、函数,甚至其他对象。 1.创建对象 ...
    程式設計 發佈於2024-12-22
  • C++ 中與運算子 (&) 的使用方式有哪些不同?
    C++ 中與運算子 (&) 的使用方式有哪些不同?
    && 在 C 語言中如何運作 && 在 C 語言中如何運作 理解 & 運算子& C中的運算子有多種用途,包括:取得某個位址變數: &x 傳回變數 x 的記憶體位址。 透過引用傳遞參數: void foo(CDummy& x);透過引用將變數 x 傳遞給函數 foo,允許在 foo 內部所做的修改反映...
    程式設計 發佈於2024-12-22
  • 馬尼拉 DevFest 推動創新、包容性和負責任的人工智慧
    馬尼拉 DevFest 推動創新、包容性和負責任的人工智慧
    图片来自GDG Manila Facebook页面(https://m.facebook.com/story.php?story_fbid=pfbid02Xh4ED8NwUnfrh9wrDS2pJKhYbpya4QxCMFWcNCeKuCpg9LgkmQ96B85FUSqo5w7bl&id=6156...
    程式設計 發佈於2024-12-22
  • 在 Go 中使用 WebSocket 進行即時通信
    在 Go 中使用 WebSocket 進行即時通信
    构建需要实时更新的应用程序(例如聊天应用程序、实时通知或协作工具)需要一种比传统 HTTP 更快、更具交互性的通信方法。这就是 WebSockets 发挥作用的地方!今天,我们将探讨如何在 Go 中使用 WebSocket,以便您可以向应用程序添加实时功能。 在这篇文章中,我们将介绍: WebSoc...
    程式設計 發佈於2024-12-22
  • C++中靜態工廠方法和工廠類別如何選擇?
    C++中靜態工廠方法和工廠類別如何選擇?
    如何在C 中正確實現工廠方法模式工廠方法模式是一種設計模式,允許創建對象而無需指定對象的確切類別要創建的對象。當運行時確定要建立的物件的類別時,或者需要提供統一的介面來建立不同類型的物件時,通常會使用這種模式。 在 C 中,有以下幾種方式實現工廠方法模式。一種常見的方法是使用在要為其建立物件的類別中...
    程式設計 發佈於2024-12-22
  • Java 中的 HashMap 或 Hashtable:對於單執行緒應用程式來說,哪個更有效率?
    Java 中的 HashMap 或 Hashtable:對於單執行緒應用程式來說,哪個更有效率?
    Java 中的HashMap 與Hashtable:非線程應用程式的主要區別和效率HashMap 和Hashtable 是Java 中的基本資料結構,它們儲存鍵值對。了解它們的差異對於選擇最合適的選項至關重要。 主要差異:同步: Hashtable 是同步的,而 HashMap 是同步的不是。同步意...
    程式設計 發佈於2024-12-22
  • MySQL 能否處理遙遠過去的日期,例如 1200 年?
    MySQL 能否處理遙遠過去的日期,例如 1200 年?
    MySQL 對歷史日期的支援許多資料庫系統,包括 MySQL,在處理歷史日期時都有限制。本文探討了儲存和使用公曆之前的日期的限制和替代方案。 MySQL 可以處理像 1200 這樣的日期嗎? 從技術上講,MySQL 可以儲存日期早在 1000 年。然而,對於在此之前的日期,存在潛在的問題考量。 歷史...
    程式設計 發佈於2024-12-22

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3