首頁 > Java > java教程 > 主體

使用 Spring Security、JWT 和 JDBC 範本在 Spring Boot 中實現基於令牌的身份驗證

Susan Sarandon
發布: 2024-10-29 02:22:30
原創
1017 人瀏覽過

程式碼參考的 Github 連結

介紹

在現代 Web 應用程式中,安全的使用者身份驗證至關重要。傳統上,基於會話的身份驗證已被廣泛使用,但隨著應用程式變得更加分散和可擴展,基於令牌的身份驗證提供了多種優勢。

基於令牌的身份驗證允許應用程式無狀態,這意味著伺服器不需要儲存任何會話數據,使其非常適合可擴展的 RESTful API。

本教學將引導您使用 Spring Security 和 JDBC 範本在 Spring Boot 應用程式中實作 JWT(JSON Web Token)身份驗證。

JWT(JSON Web Token)是一種緊湊的、URL 安全的方式來表示在兩方之間傳輸的聲明。它通常用於無狀態身份驗證,其中每個請求都使用簽名令牌進行身份驗證。

為什麼選擇 JWT 和基於令牌的身份驗證?

無狀態驗證
JWT 令牌是獨立的,直接在令牌有效負載中攜帶使用者的身份驗證訊息,這減少了伺服器記憶體使用並提高了可擴展性。

跨平台支援
令牌很容易在行動和 Web 應用程式中使用,因為它們可以安全地儲存在用戶端中(例如,本地儲存或 cookie)。

安全
每個令牌都經過數位簽名,確保其完整性並允許伺服器驗證它,而無需在每個請求上查詢資料庫。

你將學到什麼

在本教程中,您將學習如何:

  1. 使用 Spring Security 設定 Spring Boot 應用程式。
  2. 使用 JDBC 範本實作基於 JWT 令牌的身份驗證,以安全地管理使用者和儲存刷新令牌。
  3. 設定用於登入、存取令牌產生和刷新令牌處理的端點。

在本教程結束時,您將擁有一個安全、無狀態的身份驗證系統,該系統利用 Spring Boot 和 JWT 為您的應用程式提供無縫且可擴展的存取控制。


API 流程:

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template


技術需求:

  • Java 17 / 11 / 8
  • Spring Boot 3 / 2(附 Spring Security、Spring Web)
  • jjwt-api 0.11.5
  • PostgreSQL/MySQL
  • Maven

1. 設定您的 Spring Boot 項目

使用 Spring Web 工具或您的開發工具(STS、Intellij 或任何 IDE)建立 Spring Boot 專案。

開啟 pom.xml 並新增 Spring Security、JWT 和 JDBC 範本的依賴項:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>
登入後複製
登入後複製
登入後複製
登入後複製

2.配置資料庫、應用程式屬性

在 src/main/resources 資料夾下,開啟 application.properties 並新增以下設定。我將在本教程中使用 postgres 資料庫。

spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000
登入後複製
登入後複製
登入後複製
登入後複製

3. 設定資料庫表

為使用者資訊、角色、使用者角色映射和刷新令牌定義一個簡單的表格結構:

CREATE SCHEMA IB;
-------------------------------------------------------------------------

create sequence users_uniqueid_seq START 1;

create table ib.users(
        uniqueid bigint not null default nextval('users_uniqueid_seq') PRIMARY KEY,
        email varchar(75),
        password varchar(200),
        username varchar(20)
);

insert into ib.users(email,password,username) values ('admin@ib.com','a$VcdzH8Q.o4KEo6df.XesdOmXdXQwT5ugNQvu1Pl0390rmfOeA1bhS','admin');

#(password = 12345678)
-------------------------------------------------------------------------

create sequence roles_id_seq START 1;

create table ib.roles(
        id int not null default nextval('roles_id_seq') PRIMARY KEY,
        name varchar(20)
);

INSERT INTO ib.roles(name) VALUES('ROLE_USER');
INSERT INTO ib.roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO ib.roles(name) VALUES('ROLE_ADMIN');

-------------------------------------------------------------------------

create table ib.user_roles(
        user_uniqueid bigint not null,
        role_id int not null,
        primary key(user_uniqueid,role_id)
);
insert into ib.user_roles (user_uniqueid,role_id) values (1,3);
-------------------------------------------------------------------------

create sequence refresh_tokens_id_seq START 1;

create table ib.refresh_tokens(
        id bigint not null default nextval('refresh_tokens_id_seq') PRIMARY KEY,
        uniqueid bigint,
        token varchar(500) not null,
        expiryDate TIMESTAMP WITH TIME ZONE not null
);

-------------------------------------------------------------------------
登入後複製
登入後複製
登入後複製

4. 創建模型

讓我們定義以下模型。
在 models 套件中,建立以下 4 個檔案:

模型/ERole.java

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}
登入後複製
登入後複製
登入後複製

model/Role.java

package com.security.authmanager.model;

public class Role {
    private Integer id;
    private ERole name;

    public Role() {
    }

    public Role(ERole name) {
        this.name = name;
    }

    //generate getters and setters
}
登入後複製
登入後複製
登入後複製

model/User.java

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

登入後複製
登入後複製
登入後複製
登入後複製

model/RefreshToken.java

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

登入後複製
登入後複製
登入後複製
登入後複製

5. 實作 CustomUserDetailsRepository 類

CustomUserDetailsRepository 類別是一個 Spring @Repository,用於處理與 User 和 Role 實體相關的自訂資料庫操作。它使用 JdbcTemplate 執行 SQL 查詢來執行諸如獲取使用者、透過使用者名稱或電子郵件檢查使用者是否存在、建立新使用者以及取得角色等任務的 SQL 查詢。

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

@Repository
public class CustomUserDetailsRepository {

    private static final Logger logger = LoggerFactory.getLogger(CustomUserDetailsRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User fetchUserByUserName(String userName){
        try{
            return jdbcTemplate.query((conn) ->{
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_USER);
                ps.setString(1, userName.toUpperCase());
                return ps;
            },rs->{
                User user = null;
                Set<Role> roles = new HashSet<>();
                while (rs.next()) {
                    if (user == null) {
                        user = new User();
                        user.setEmail(rs.getString("email"));
                        user.setId(rs.getLong("uniqueid"));
                        user.setPassword(rs.getString("password"));
                        user.setUsername(rs.getString("username"));
                    }
                    Role role = new Role();
                    role.setId(rs.getInt("id"));
                    role.setName(ERole.valueOf(rs.getString("name")));
                    roles.add(role);
                }
                if (user != null) {
                    user.setRoles(roles);
                }
                return user;
            });
        }catch(Exception e){
            logger.error("Exception in fetchUserByUserName()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByUsername(String userName) {
        try{
              return jdbcTemplate.query((conn) -> {
                 final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_USERNAME);
                 ps.setString(1, userName.toUpperCase());
                 return ps;
             }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByUsername()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByEmail(String email) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_EMAIL);
                ps.setString(1, email.toUpperCase());
                return ps;
            }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }

    public Role findRoleByName(ERole eRole) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_ROLE_BY_NAME);
                ps.setString(1, String.valueOf(eRole));
                return ps;
            }, rs -> {
                Role role=null;
                while(rs.next()){
                    role = new Role();
                    role.setName(ERole.valueOf(rs.getString("name")));
                    role.setId(rs.getInt("id"));
                }
                return role;
            });
        }catch(Exception e){
            logger.error("Exception in findRoleByName()",e);
            throw new RuntimeException(e);
        }
    }

    public void createUser(User user) {
        try(Connection conn = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection()){
            try (PreparedStatement userStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USERS,Statement.RETURN_GENERATED_KEYS)) {
                userStatement.setString(1, user.getEmail().toUpperCase());
                userStatement.setString(2, user.getPassword());
                userStatement.setString(3, user.getUsername().toUpperCase());
                userStatement.executeUpdate();
                // Retrieve generated userId
                try (ResultSet generatedKeys = userStatement.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        Long userId = generatedKeys.getLong(1); // Assuming userId is of type VARCHAR
                        logger.info("gen userid {}",userId.toString());
                        user.setId(userId);
                    }
                }
            }
            if (user.getRoles() != null && !user.getRoles().isEmpty()) {
                try (PreparedStatement userRoleStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USER_ROLES)) {
                    for (Role role : user.getRoles()) {
                        userRoleStatement.setLong(1, user.getId());
                        userRoleStatement.setLong(2, role.getId());
                        userRoleStatement.executeUpdate();
                    }
                }
            }
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }
}

登入後複製
登入後複製

此儲存庫執行基於 SQL 的自訂 CRUD 操作,用於管理資料庫中的使用者和角色資料。

主要功能:

  • 透過使用者名稱取得使用者並檢索關聯的角色。
  • 透過使用者名稱或電子郵件檢查使用者是否存在。
  • 將新使用者及其角色插入資料庫。
  • 根據角色名稱檢索角色詳細資訊。

6. 實作 RefreshTokenRepository 類

RefreshTokenRepository 類別是一個 Spring @Repository,用於處理與 RefreshToken 實體相關的資料庫操作。它使用 Spring 的 JdbcTemplate 透過原始 SQL 查詢與資料庫進行交互,封裝了保存、刪除和檢索刷新令牌的邏輯。

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.RefreshToken;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.util.Optional;

@Repository
public class RefreshTokenRepository {
    private static final Logger logger = LoggerFactory.getLogger(RefreshTokenRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void deleteRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.DELETE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setString(1,refreshToken.getToken());
            });
        }catch (Exception e){
            logger.error("Exception in deleteRefreshToken()",e);
            throw new RuntimeException(e);
        }
    }

    public int deleteRefreshTokenByUser(User user) {
        return 0;
    }

    public RefreshToken saveRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.SAVE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setLong(1,refreshToken.getUser().getId());
                ps.setString(2,refreshToken.getToken());
                ps.setTimestamp(3, Timestamp.from(refreshToken.getExpiryDate()));
            });
        }catch (Exception e){
            logger.error("Exception in saveRefreshToken()",e);
            throw new RuntimeException(e);
        }
        return refreshToken;
    }

    public Optional<RefreshToken> findByToken(String token) {
        RefreshToken refreshToken = new RefreshToken();
        try{
            return Optional.ofNullable(jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FIND_BY_TOKEN);
                ps.setString(1, token);
                return ps;
            }, rs -> {
                User user = new User();
                while (rs.next()) {
                    refreshToken.setId(rs.getLong("id"));
                    refreshToken.setToken(rs.getString("token"));
                    refreshToken.setExpiryDate(rs.getTimestamp("expiryDate").toInstant());
                    user.setId(rs.getLong("uniqueid"));
                    user.setEmail(rs.getString("email"));
                    user.setUsername(rs.getString("username"));
                    refreshToken.setUser(user);
                }
                return refreshToken;
            }));
        }catch(Exception e){
            logger.error("Exception in findByToken()",e);
            throw new RuntimeException(e);
        }
    }
}

登入後複製
登入後複製

此儲存庫直接與資料庫交互,以對 RefreshToken 實體進行 CRUD 操作。

主要功能:

  • 儲存刷新令牌。
  • 透過令牌值檢索刷新令牌。
  • 按令牌或使用者刪除刷新令牌。

7.配置Spring Security

  • WebSecurityConfig 將 Spring Security 設定為使用基於 JWT 的令牌驗證。

  • 它為驗證所需的各種元件定義了Bean,例如AuthTokenFilter(用於處理JWT 令牌)、DaoAuthenticationProvider(用於檢索使用者詳細資訊和驗證密碼)和BCryptPasswordEncoder(用於雜湊和比較密碼)。

SecurityFilterChain 設定如何保護傳入的 HTTP 要求:
- 允許存取某些公共路由(/auth/**、/test/**)。
- 保護所有其他路由,需要身份驗證。
- 停用會話管理(使系統無狀態)。
- 配置過濾器來攔截和處理
的 JWT 令牌 用戶身份驗證。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>
登入後複製
登入後複製
登入後複製
登入後複製

8. 實作UserDetailsImpl

該類別主要在 Spring Security 中用來表示目前經過驗證的使用者。

當使用者嘗試登入時:

  1. Spring Security 呼叫 UserDetailsS​​ervice.loadUserByUsername() 來載入 資料庫中的使用者。
  2. 它使用使用者的詳細資料來建立 UserDetailsImpl 的實例 (包括他們的角色)。
  3. Spring Security 使用這個 UserDetailsImpl 物件來 對使用者進行身份驗證並檢查他們的權限 會議。

UserDetailsImpl 是您的 User 實體和 Spring Security 的內部驗證和授權機制之間的橋樑。

spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000
登入後複製
登入後複製
登入後複製
登入後複製

以下是查詢:(QueryConstants.java)

CREATE SCHEMA IB;
-------------------------------------------------------------------------

create sequence users_uniqueid_seq START 1;

create table ib.users(
        uniqueid bigint not null default nextval('users_uniqueid_seq') PRIMARY KEY,
        email varchar(75),
        password varchar(200),
        username varchar(20)
);

insert into ib.users(email,password,username) values ('admin@ib.com','a$VcdzH8Q.o4KEo6df.XesdOmXdXQwT5ugNQvu1Pl0390rmfOeA1bhS','admin');

#(password = 12345678)
-------------------------------------------------------------------------

create sequence roles_id_seq START 1;

create table ib.roles(
        id int not null default nextval('roles_id_seq') PRIMARY KEY,
        name varchar(20)
);

INSERT INTO ib.roles(name) VALUES('ROLE_USER');
INSERT INTO ib.roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO ib.roles(name) VALUES('ROLE_ADMIN');

-------------------------------------------------------------------------

create table ib.user_roles(
        user_uniqueid bigint not null,
        role_id int not null,
        primary key(user_uniqueid,role_id)
);
insert into ib.user_roles (user_uniqueid,role_id) values (1,3);
-------------------------------------------------------------------------

create sequence refresh_tokens_id_seq START 1;

create table ib.refresh_tokens(
        id bigint not null default nextval('refresh_tokens_id_seq') PRIMARY KEY,
        uniqueid bigint,
        token varchar(500) not null,
        expiryDate TIMESTAMP WITH TIME ZONE not null
);

-------------------------------------------------------------------------
登入後複製
登入後複製
登入後複製

9. 實作UserDetailsS​​erviceImpl

UserDetailsS​​erviceImpl 類別充當應用程式的資料庫和 Spring Security 的身份驗證過程之間的橋樑。它使用 CustomUserDetailsRepository 從資料庫中獲取用戶詳細信息,將 User 物件轉換為 UserDetailsImpl (一種 Spring Security 友好的格式),並透過拋出異常來處理找不到用戶的情況。該服務允許 Spring Security 根據使用者的角色和權限對使用者進行身份驗證並管理授權。

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}
登入後複製
登入後複製
登入後複製

10. 過濾請求

AuthTokenFilter 類別擴充了 Spring 的 OncePerRequestFilter,使其成為在請求鏈中處理每個 HTTP 請求一次的過濾器。它的主要作用是從請求中提取並驗證 JWT(JSON Web 令牌),如果令牌有效,則在 Spring Security 的 SecurityContext 中設定使用者的身份驗證。

package com.security.authmanager.model;

public class Role {
    private Integer id;
    private ERole name;

    public Role() {
    }

    public Role(ERole name) {
        this.name = name;
    }

    //generate getters and setters
}
登入後複製
登入後複製
登入後複製

每次提出請求時:

  • 過濾器檢查請求是否包含有效的 JWT。
  • 如果有效,它將透過在 SecurityContext 中設定 UsernamePasswordAuthenticationToken 來對使用者進行身份驗證。
  • 如果 JWT 無效或遺失,則不會設定身份驗證,並且請求將作為未經身份驗證的請求繼續進行。

此過濾器可確保所有攜帶有效 JWT 令牌的請求都會自動進行身份驗證,而無需使用者在登入後在後續請求中提供憑證(如使用者名稱/密碼)。

11. 實作RefreshTokenService

RefreshTokenService 類別提供與在基於令牌的驗證系統中管理刷新令牌相關的服務。刷新令牌用於在初始 JWT 過期後取得新的 JWT 令牌,而不需要使用者重新進行身份驗證。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>
登入後複製
登入後複製
登入後複製
登入後複製

RefreshTokenService 處理刷新令牌的建立、驗證和刪除。它使用儲存庫從資料庫中保存和獲取令牌和使用者。

此服務是身份驗證系統的重要組成部分,其中刷新令牌用於使用戶保持登入狀態,而無需他們在 JWT 過期後再次提供憑證。

12. 實施 JWT 實用程序

JwtUtils 類別是一個實用程式類,它處理 JWT(JSON Web Token)的建立、解析和驗證,以在 Spring Boot 應用程式中進行身份驗證。它使用 jjwt 函式庫來處理 JWT。

spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000
登入後複製
登入後複製
登入後複製
登入後複製

JwtUtils 類別負責產生、解析和驗證 JWT 令牌。它使用金鑰 (HMAC-SHA256) 安全地對令牌進行簽名,並確保令牌只能由擁有正確金鑰的各方讀取或驗證。

該類別也會從令牌中提取使用者名,並在授予使用者存取權限之前檢查令牌是否有效。此實用程式對於在應用程式中維護安全性、基於令牌的身份驗證至關重要。

13. 處理認證異常

AuthEntryPointJwt 類別實作 Spring Security 的 AuthenticationEntryPoint 介面。它處理發出未經授權的請求時發生的情況,通常是當使用者嘗試在沒有有效身份驗證的情況下存取受保護的資源時(例如,沒有 JWT 或無效的 JWT)。

CREATE SCHEMA IB;
-------------------------------------------------------------------------

create sequence users_uniqueid_seq START 1;

create table ib.users(
        uniqueid bigint not null default nextval('users_uniqueid_seq') PRIMARY KEY,
        email varchar(75),
        password varchar(200),
        username varchar(20)
);

insert into ib.users(email,password,username) values ('admin@ib.com','a$VcdzH8Q.o4KEo6df.XesdOmXdXQwT5ugNQvu1Pl0390rmfOeA1bhS','admin');

#(password = 12345678)
-------------------------------------------------------------------------

create sequence roles_id_seq START 1;

create table ib.roles(
        id int not null default nextval('roles_id_seq') PRIMARY KEY,
        name varchar(20)
);

INSERT INTO ib.roles(name) VALUES('ROLE_USER');
INSERT INTO ib.roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO ib.roles(name) VALUES('ROLE_ADMIN');

-------------------------------------------------------------------------

create table ib.user_roles(
        user_uniqueid bigint not null,
        role_id int not null,
        primary key(user_uniqueid,role_id)
);
insert into ib.user_roles (user_uniqueid,role_id) values (1,3);
-------------------------------------------------------------------------

create sequence refresh_tokens_id_seq START 1;

create table ib.refresh_tokens(
        id bigint not null default nextval('refresh_tokens_id_seq') PRIMARY KEY,
        uniqueid bigint,
        token varchar(500) not null,
        expiryDate TIMESTAMP WITH TIME ZONE not null
);

-------------------------------------------------------------------------
登入後複製
登入後複製
登入後複製

AuthEntryPointJwt 類別是一個自訂入口點,它攔截未經授權的存取嘗試並返回結構化 JSON 回應,其中包含 401 錯誤代碼、錯誤訊息以及有關請求的詳細資訊。
它會記錄錯誤,並在身份驗證失敗時向客戶端提供清晰、用戶友好的回應。

14. 為Controller建立有效負載類

以下是我們的 RestAPI 的有效負載:

1。要求:

- LoginRequest.java :

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}
登入後複製
登入後複製
登入後複製

- SignupRequest.java :

package com.security.authmanager.model;

public class Role {
    private Integer id;
    private ERole name;

    public Role() {
    }

    public Role(ERole name) {
        this.name = name;
    }

    //generate getters and setters
}
登入後複製
登入後複製
登入後複製

- TokenRefreshRequest.java :

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

登入後複製
登入後複製
登入後複製
登入後複製

2。回覆:

- JwtResponse.java

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

登入後複製
登入後複製
登入後複製
登入後複製

- MessageResponse.java

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

@Repository
public class CustomUserDetailsRepository {

    private static final Logger logger = LoggerFactory.getLogger(CustomUserDetailsRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User fetchUserByUserName(String userName){
        try{
            return jdbcTemplate.query((conn) ->{
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_USER);
                ps.setString(1, userName.toUpperCase());
                return ps;
            },rs->{
                User user = null;
                Set<Role> roles = new HashSet<>();
                while (rs.next()) {
                    if (user == null) {
                        user = new User();
                        user.setEmail(rs.getString("email"));
                        user.setId(rs.getLong("uniqueid"));
                        user.setPassword(rs.getString("password"));
                        user.setUsername(rs.getString("username"));
                    }
                    Role role = new Role();
                    role.setId(rs.getInt("id"));
                    role.setName(ERole.valueOf(rs.getString("name")));
                    roles.add(role);
                }
                if (user != null) {
                    user.setRoles(roles);
                }
                return user;
            });
        }catch(Exception e){
            logger.error("Exception in fetchUserByUserName()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByUsername(String userName) {
        try{
              return jdbcTemplate.query((conn) -> {
                 final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_USERNAME);
                 ps.setString(1, userName.toUpperCase());
                 return ps;
             }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByUsername()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByEmail(String email) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_EMAIL);
                ps.setString(1, email.toUpperCase());
                return ps;
            }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }

    public Role findRoleByName(ERole eRole) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_ROLE_BY_NAME);
                ps.setString(1, String.valueOf(eRole));
                return ps;
            }, rs -> {
                Role role=null;
                while(rs.next()){
                    role = new Role();
                    role.setName(ERole.valueOf(rs.getString("name")));
                    role.setId(rs.getInt("id"));
                }
                return role;
            });
        }catch(Exception e){
            logger.error("Exception in findRoleByName()",e);
            throw new RuntimeException(e);
        }
    }

    public void createUser(User user) {
        try(Connection conn = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection()){
            try (PreparedStatement userStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USERS,Statement.RETURN_GENERATED_KEYS)) {
                userStatement.setString(1, user.getEmail().toUpperCase());
                userStatement.setString(2, user.getPassword());
                userStatement.setString(3, user.getUsername().toUpperCase());
                userStatement.executeUpdate();
                // Retrieve generated userId
                try (ResultSet generatedKeys = userStatement.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        Long userId = generatedKeys.getLong(1); // Assuming userId is of type VARCHAR
                        logger.info("gen userid {}",userId.toString());
                        user.setId(userId);
                    }
                }
            }
            if (user.getRoles() != null && !user.getRoles().isEmpty()) {
                try (PreparedStatement userRoleStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USER_ROLES)) {
                    for (Role role : user.getRoles()) {
                        userRoleStatement.setLong(1, user.getId());
                        userRoleStatement.setLong(2, role.getId());
                        userRoleStatement.executeUpdate();
                    }
                }
            }
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }
}

登入後複製
登入後複製

- TokenRefreshResponse.java

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.RefreshToken;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.util.Optional;

@Repository
public class RefreshTokenRepository {
    private static final Logger logger = LoggerFactory.getLogger(RefreshTokenRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void deleteRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.DELETE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setString(1,refreshToken.getToken());
            });
        }catch (Exception e){
            logger.error("Exception in deleteRefreshToken()",e);
            throw new RuntimeException(e);
        }
    }

    public int deleteRefreshTokenByUser(User user) {
        return 0;
    }

    public RefreshToken saveRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.SAVE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setLong(1,refreshToken.getUser().getId());
                ps.setString(2,refreshToken.getToken());
                ps.setTimestamp(3, Timestamp.from(refreshToken.getExpiryDate()));
            });
        }catch (Exception e){
            logger.error("Exception in saveRefreshToken()",e);
            throw new RuntimeException(e);
        }
        return refreshToken;
    }

    public Optional<RefreshToken> findByToken(String token) {
        RefreshToken refreshToken = new RefreshToken();
        try{
            return Optional.ofNullable(jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FIND_BY_TOKEN);
                ps.setString(1, token);
                return ps;
            }, rs -> {
                User user = new User();
                while (rs.next()) {
                    refreshToken.setId(rs.getLong("id"));
                    refreshToken.setToken(rs.getString("token"));
                    refreshToken.setExpiryDate(rs.getTimestamp("expiryDate").toInstant());
                    user.setId(rs.getLong("uniqueid"));
                    user.setEmail(rs.getString("email"));
                    user.setUsername(rs.getString("username"));
                    refreshToken.setUser(user);
                }
                return refreshToken;
            }));
        }catch(Exception e){
            logger.error("Exception in findByToken()",e);
            throw new RuntimeException(e);
        }
    }
}

登入後複製
登入後複製

15. 建立 Rest API 控制器類

- AuthController.java

AuthController 類別是一個 Spring @RestController,負責處理應用程式中與驗證相關的端點。它提供使用者登入、註冊和令牌刷新操作的端點。

主要功能:

  • 使用者使用 JWT 登入並刷新令牌產生。
  • 使用有效的刷新令牌進行令牌刷新。
  • 使用者註冊以及驗證和角色分配。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>
登入後複製
登入後複製
登入後複製
登入後複製

- TestController.java

TestController 類別是一個 Spring @RestController,它提供了多個端點來測試基於使用者角色的存取控制。它示範如何使用 Spring Security 基於角色的授權來限制對應用程式某些部分的存取。

主要功能:

  • 任何人都可以存取的公共存取端點。
  • 基於使用者角色(使用者、主持人、管理員)限制存取的特定角色的端點。
spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000
登入後複製
登入後複製
登入後複製
登入後複製

16. 測試 API

1。註冊為模組和使用者。 (登入)

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

2。登入以取得存取令牌。

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

3。取得刷新令牌 API。

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

4。透過傳遞存取令牌來測試使用者存取。

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

5。透過傳遞存取令牌來測試 mod 存取。

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

6。透過傳遞相同的存取令牌來測試管理員存取權限。

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

未經授權,因為該使用者沒有管理員存取權限(使用者只有 mod 和使用者角色)

快樂學習!又見面了。 ?

以上是使用 Spring Security、JWT 和 JDBC 範本在 Spring Boot 中實現基於令牌的身份驗證的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板