Spring Security、JWT、および JDBC テンプレートを使用した Spring Boot でのトークンベースの認証の実装

Susan Sarandon
リリース: 2024-10-29 02:22:30
オリジナル
1016 人が閲覧しました

コードリファレンスの Github リンク

導入

最新の Web アプリケーションでは、安全なユーザー認証が非常に重要です。従来、セッションベースの認証が広く使用されてきましたが、アプリケーションの分散化と拡張性が高まるにつれ、トークンベースの認証にはいくつかの利点があります。

トークンベースの認証により、アプリケーションをステートレスにすることができます。つまり、サーバーはセッション データを保存する必要がなく、スケーラブルな RESTful API に最適です。

このチュートリアルでは、Spring Security と JDBC テンプレートを使用して Spring Boot アプリケーションに JWT (JSON Web Token) 認証を実装する方法を説明します。

JWT (JSON Web Token) は、2 者間で転送されるクレームを表現するコンパクトで URL セーフな方法です。これは一般に、各リクエストが署名付きトークンを使用して認証されるステートレス認証に使用されます。

JWT とトークンベースの認証を使用する理由

ステートレス認証
JWT トークンは自己完結型であり、ユーザーの認証情報をトークン ペイロード内で直接伝送するため、サーバーのメモリ使用量が削減され、スケーラビリティが向上します。

クロスプラットフォームのサポート
トークンはクライアントに安全に (ローカル ストレージや Cookie などに) 保存できるため、モバイル アプリケーションや Web アプリケーションで簡単に使用できます。

セキュリティ
各トークンはデジタル署名されているため、その整合性が保証され、サーバーはリクエストのたびにデータベースにクエリを実行することなくトークンを検証できます。

何を学ぶか

このチュートリアルでは、次の方法を学びます:

  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
  • メイブン

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. モデルを作成する

次のモデルを定義しましょう。
モデル パッケージで、以下の 4 つのファイルを作成します:

モデル/ERole.java

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

モデル/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
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

モデル/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
}

ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

モデル/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 クラスは、User エンティティとロール エンティティに関連するカスタム データベース操作を処理する Spring @Repository です。 JdbcTemplate を使用して、ユーザーの取得、ユーザー名または電子メールによるユーザーの存在の確認、新しいユーザーの作成、ロールの取得などのタスクの 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 クラスは、RefreshToken エンティティに関連するデータベース操作を処理する Spring @Repository です。 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 は、JWT ベースのトークン認証を使用するように Spring Security を構成します。

  • AuthTokenFilter (JWT トークンの処理用)、DaoAuthenticationProvider (ユーザー詳細の取得とパスワードの検証用)、BCryptPasswordEncoder (パスワードのハッシュ化と比較用) など、認証に必要なさまざまなコンポーネントの Bean を定義します。

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. この UserDetailsImpl オブジェクトは Spring Security によって使用され、 ユーザーを認証し、全体を通してユーザーの権限を確認します。 セッション。

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 リクエストを 1 回処理するフィルタにします。その主な役割は、リクエストから JWT (JSON Web Token) を抽出して検証し、トークンが有効であれば 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 クラスは、Spring Boot アプリケーションで認証を目的とした JWT (JSON Web Token) の作成、解析、検証を処理するユーティリティ クラスです。 JWT を操作するために jjwt ライブラリを使用します。

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 クラスは、不正アクセスの試みを遮断し、401 エラー コード、エラー メッセージ、リクエストの詳細を含む構造化された JSON 応答を返すカスタム エントリ ポイントです。
エラーをログに記録し、認証が失敗した場合にクライアントに明確でユーザーフレンドリーな応答を提供します。

14. コントローラーのペイロードクラスを作成する

以下は 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 のロールベースの承認を使用して、アプリケーションの特定の部分へのアクセスを制限する方法を示します。

主な機能:

  • 誰でもアクセスできるパブリック アクセス エンドポイント。
  • ユーザーの役割 (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. APIのテスト

1. MODとユーザーとして登録します。 (サインイン)

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 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート