Rumah  >  Artikel  >  Java  >  Melaksanakan Pengesahan Berasaskan Token dalam But Spring Menggunakan Spring Security, JWT dan Templat JDBC

Melaksanakan Pengesahan Berasaskan Token dalam But Spring Menggunakan Spring Security, JWT dan Templat JDBC

Susan Sarandon
Susan Sarandonasal
2024-10-29 02:22:301019semak imbas

Pautan Github untuk Rujukan Kod

pengenalan

Dalam aplikasi web moden, pengesahan pengguna yang selamat adalah penting. Secara tradisinya, pengesahan berasaskan sesi telah digunakan secara meluas, tetapi apabila aplikasi menjadi lebih diedarkan dan berskala, pengesahan berasaskan token menawarkan beberapa kelebihan.

Pengesahan berasaskan token membolehkan aplikasi menjadi tanpa kewarganegaraan, bermakna pelayan tidak perlu menyimpan sebarang data sesi, menjadikannya sangat sesuai untuk API RESTful yang boleh berskala.

Tutorial ini akan membimbing anda melalui pelaksanaan pengesahan JWT (JSON Web Token) dalam aplikasi Spring Boot menggunakan Spring Security dengan Templat JDBC.

JWT(JSON Web Token) ialah cara padat, selamat URL untuk mewakili tuntutan yang dipindahkan antara dua pihak. Ia biasanya digunakan untuk pengesahan tanpa kewarganegaraan di mana setiap permintaan disahkan menggunakan token yang ditandatangani.

Mengapa JWT dan Pengesahan Berasaskan Token?

Pengesahan Tanpa Status
Token JWT adalah serba lengkap, membawa maklumat pengesahan pengguna terus dalam muatan token, yang mengurangkan penggunaan memori pelayan dan meningkatkan kebolehskalaan.

Sokongan Merentas Platform
Token mudah digunakan dalam aplikasi mudah alih dan web kerana ia boleh disimpan dengan selamat dalam klien (cth., dalam storan tempatan atau kuki).

Keselamatan
Setiap token ditandatangani secara digital, memastikan integritinya dan membenarkan pelayan mengesahkannya tanpa menanyakan pangkalan data pada setiap permintaan.

Apa yang Anda Akan Pelajari

Dalam tutorial ini, anda akan belajar cara:

  1. Sediakan aplikasi Spring Boot dengan Spring Security.
  2. Laksanakan pengesahan berasaskan token JWT menggunakan Templat JDBC untuk mengurus pengguna dan menyimpan token muat semula dengan selamat.
  3. Sediakan titik akhir untuk log masuk, penjanaan token akses dan muat semula pengendalian token.

Menjelang akhir tutorial ini, anda akan mempunyai sistem pengesahan tanpa kewarganegaraan yang selamat yang memanfaatkan Spring Boot dan JWT untuk menyediakan kawalan akses yang lancar dan berskala untuk aplikasi anda.


Aliran API:

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


Keperluan teknologi:

  • Jawa 17 / 11 / 8
  • But Musim Bunga 3 / 2 (dengan Keselamatan Musim Bunga, Web Musim Bunga)
  • jjwt-api 0.11.5
  • PostgreSQL/MySQL
  • Maven

1. Sediakan Projek But Spring Anda

Gunakan alat web Spring atau alat pembangunan anda (STS, Intellij atau mana-mana IDE) untuk mencipta projek Spring Boot.

buka pom.xml dan tambah kebergantungan untuk Spring Security, JWT dan Templat 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. Konfigurasi Pangkalan Data, sifat Apl

Di bawah folder src/main/resources, buka application.properties dan tambahkan konfigurasi di bawah. Saya akan menggunakan pangkalan data postgres untuk tutorial ini.

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. Sediakan Jadual Pangkalan Data

Tentukan struktur jadual ringkas untuk maklumat pengguna, peranan, pemetaan peranan pengguna dan token muat semula:

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. Cipta model

Mari kita tentukan model berikut.
Dalam pakej model, buat di bawah 4 fail:

model/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. Laksanakan kelas CustomUserDetailsRepository

Kelas CustomUserDetailsRepository ialah Spring @Repository yang mengendalikan operasi pangkalan data tersuai yang berkaitan dengan entiti Pengguna dan Peranan. Ia menggunakan JdbcTemplate untuk melaksanakan pertanyaan SQL untuk tugas seperti mengambil pengguna, menyemak sama ada pengguna wujud melalui nama pengguna atau e-mel, mencipta pengguna baharu dan mengambil peranan.

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

Repositori ini melaksanakan operasi CRUD berasaskan SQL tersuai untuk mengurus data Pengguna dan Peranan dalam pangkalan data.

Fungsi Utama:

  • Ambil pengguna mengikut nama pengguna dan dapatkan semula peranan yang berkaitan.
  • Semak sama ada pengguna wujud melalui nama pengguna atau e-mel.
  • Masukkan pengguna baharu dan peranan mereka ke dalam pangkalan data.
  • Dapatkan butiran peranan berdasarkan nama peranan.

6. Laksanakan kelas RefreshTokenRepository

Kelas RefreshTokenRepository ialah Spring @Repository yang mengendalikan operasi pangkalan data yang berkaitan dengan entiti RefreshToken. Ia menggunakan JdbcTemplate Spring untuk berinteraksi dengan pangkalan data melalui pertanyaan SQL mentah, merangkum logik untuk menyimpan, memadam dan mendapatkan semula token muat semula.

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

Repositori ini berinteraksi secara langsung dengan pangkalan data untuk operasi CRUD pada entiti RefreshToken.

Fungsi Utama:

  • Simpan token muat semula.
  • Dapatkan semula token muat semula mengikut nilai token.
  • Padamkan token muat semula sama ada dengan token atau pengguna.

7. Konfigurasikan Keselamatan Musim Bunga

  • WebSecurityConfig mengkonfigurasi Spring Security untuk menggunakan pengesahan token berasaskan JWT.

  • Ia mentakrifkan kacang untuk pelbagai komponen yang diperlukan untuk pengesahan seperti AuthTokenFilter (untuk mengendalikan token JWT), DaoAuthenticationProvider (untuk mendapatkan butiran pengguna dan mengesahkan kata laluan), dan BCryptPasswordEncoder (untuk pencincangan dan membandingkan kata laluan).

SecurityFilterChain mengkonfigurasi cara permintaan HTTP masuk dijamin:
- Membenarkan akses ke laluan awam tertentu (/auth/**, /test/**).
- Melindungi semua laluan lain, memerlukan pengesahan.
- Melumpuhkan pengurusan sesi (menjadikan sistem tanpa status).
- Mengkonfigurasikan penapis untuk memintas dan memproses token JWT untuk
pengesahan pengguna.

<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. Laksanakan UserDetailsImpl

Kelas ini digunakan terutamanya dalam Spring Security untuk mewakili pengguna yang sedang disahkan.

Apabila pengguna cuba log masuk:

  1. Spring Security memanggil UserDetailsService.loadUserByUsername() untuk memuatkan pengguna daripada pangkalan data.
  2. Ia mencipta contoh UserDetailsImpl dengan butiran pengguna (termasuk peranan mereka).
  3. Objek UserDetailsImpl ini kemudiannya digunakan oleh Spring Security untuk mengesahkan pengguna dan menyemak kebenaran mereka sepanjang sesi.

UserDetailsImpl ialah jambatan antara entiti Pengguna anda dan mekanisme dalaman Spring Security untuk pengesahan dan kebenaran.

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

Di bawah ialah pertanyaan : (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. Laksanakan UserDetailsServiceImpl

Kelas UserDetailsServiceImpl bertindak sebagai jambatan antara pangkalan data aplikasi anda dan proses pengesahan Spring Security. Ia mengambil butiran pengguna daripada pangkalan data menggunakan CustomUserDetailsRepository, menukar objek Pengguna kepada UserDetailsImpl (format mesra Spring Security) dan mengendalikan kes di mana pengguna tidak ditemui dengan membuang pengecualian. Perkhidmatan ini membenarkan Spring Security untuk mengesahkan pengguna dan mengurus kebenaran berdasarkan peranan dan kebenaran pengguna.

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}

10. Tapis Permintaan

Kelas AuthTokenFilter memanjangkan OncePerRequestFilter Spring, menjadikannya penapis yang memproses setiap permintaan HTTP sekali dalam rantaian permintaan. Peranan utamanya ialah untuk mengekstrak dan mengesahkan JWT (JSON Web Token) daripada permintaan dan menetapkan pengesahan pengguna dalam SecurityContext Spring Security jika token itu sah.

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
}

Setiap kali permintaan dibuat:

  • Penapis menyemak sama ada permintaan mengandungi JWT yang sah.
  • Jika sah, ia mengesahkan pengguna dengan menetapkan UsernamePasswordAuthenticationToken dalam SecurityContext.
  • Jika JWT tidak sah atau tiada, tiada pengesahan ditetapkan dan permintaan diteruskan sebagai permintaan yang tidak disahkan.

Penapis ini memastikan bahawa semua permintaan yang membawa token JWT yang sah disahkan secara automatik, tanpa perlu pengguna memberikan bukti kelayakan (seperti nama pengguna/kata laluan) dalam permintaan berikutnya selepas log masuk.

11. Laksanakan RefreshTokenService

Kelas RefreshTokenService menyediakan perkhidmatan yang berkaitan dengan menguruskan token muat semula dalam sistem pengesahan berasaskan token. Token muat semula digunakan untuk mendapatkan token JWT baharu selepas JWT awal tamat tempoh tanpa memerlukan pengguna untuk mengesahkan semula.

<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 mengendalikan penciptaan, pengesahan dan pemadaman token muat semula. Ia menggunakan repositori untuk menyimpan dan mengambil token dan pengguna daripada pangkalan data.

Perkhidmatan ini merupakan bahagian penting dalam sistem pengesahan di mana token muat semula digunakan untuk memastikan pengguna log masuk tanpa memerlukan mereka memberikan bukti kelayakan semula selepas JWT tamat tempoh.

12. Laksanakan JWT Utility

Kelas JwtUtils ialah kelas utiliti yang mengendalikan penciptaan, penghuraian dan pengesahan JWT (JSON Web Token) untuk tujuan pengesahan dalam aplikasi Spring Boot. Ia menggunakan perpustakaan jjwt untuk bekerja dengan 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

Kelas JwtUtils bertanggungjawab untuk menjana, menghuraikan dan mengesahkan token JWT. Ia menandatangani token dengan selamat menggunakan kunci rahsia (HMAC-SHA256) dan memastikan bahawa token hanya boleh dibaca atau disahkan oleh pihak yang memiliki kunci rahsia yang betul.

Kelas juga mengekstrak nama pengguna daripada token dan menyemak sama ada token itu sah sebelum memberikan akses kepada pengguna. Utiliti ini penting untuk mengekalkan pengesahan berasaskan token yang selamat dalam aplikasi anda.

13. Mengendalikan Pengecualian Pengesahan

Kelas AuthEntryPointJwt melaksanakan antara muka PengesahanEntryPoint Spring Security. Ia mengendalikan perkara yang berlaku apabila permintaan tanpa kebenaran dibuat, biasanya apabila pengguna cuba mengakses sumber yang dilindungi tanpa pengesahan yang sah (mis., tiada JWT atau JWT tidak sah).

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

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

Kelas AuthEntryPointJwt ialah titik masuk tersuai yang memintas percubaan akses tanpa kebenaran dan mengembalikan respons JSON berstruktur dengan kod ralat 401, mesej ralat dan butiran tentang permintaan.
Ia merekodkan ralat dan memberikan respons yang jelas dan mesra pengguna kepada pelanggan apabila pengesahan gagal.

14. Buat kelas muatan untuk Pengawal

Di bawah ialah muatan untuk RestAPI kami:

1. Permintaan:

- 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. Jawapan:

- 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. Cipta kelas Pengawal API Rehat

- AuthController.java

Kelas AuthController ialah Spring @RestController yang bertanggungjawab untuk mengendalikan titik akhir berkaitan pengesahan dalam aplikasi. Ia menyediakan titik akhir untuk log masuk pengguna, pendaftaran dan operasi muat semula token.

Fungsi Utama:

  • Log masuk pengguna dengan JWT dan muat semula penjanaan token.
  • Segar semula token menggunakan token muat semula yang sah.
  • Pendaftaran pengguna dengan pengesahan dan penetapan peranan.
<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

Kelas TestController ialah Spring @RestController yang menyediakan beberapa titik akhir untuk menguji kawalan akses berdasarkan peranan pengguna. Ia menunjukkan cara menggunakan kebenaran berasaskan peranan Spring Security untuk menyekat akses kepada bahagian tertentu aplikasi.

Fungsi Utama:

  • Titik akhir akses awam yang boleh diakses oleh sesiapa sahaja.
  • Titik akhir khusus peranan yang mengehadkan akses berdasarkan peranan pengguna (USER, MODERATOR, ADMIN).
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. Uji API

1. Daftar sebagai mod dan pengguna. (Log masuk)

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

2. Log masuk untuk mendapatkan token akses.

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

3. Dapatkan API Token Segar Semula.

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

4. Menguji akses pengguna dengan melepasi token akses.

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

5. Menguji akses mod dengan menghantar token akses.

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

6. Menguji akses pentadbir dengan menghantar token akses yang sama.

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

Tidak dibenarkan kerana pengguna ini tidak mempunyai akses pentadbir (pengguna hanya mempunyai mod dan peranan pengguna)

Selamat belajar! Jumpa lagi.?

Atas ialah kandungan terperinci Melaksanakan Pengesahan Berasaskan Token dalam But Spring Menggunakan Spring Security, JWT dan Templat JDBC. 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