Spring Securityフレームワークの詳細説明

安全

Springセキュリティフレームワーク

Spring Security フレームワークは、主に認証認可関連を解決します。問題。

依存関係を追加する

Spring Boot プロジェクトで Spring Security を使用する必要がある場合は、spring-boot-starter-security依存関係を追加する必要があります。

この依存関係がプロジェクトに追加されると、次のような一連の自動構成がデフォルトで実行されます。

  • 現在のプロジェクト内のすべてのアクセスは、許可される前にログインする必要があります。
    • ログインしていない場合は、 /login に自動的にリダイレクトされます。ログインに成功すると、以前にアクセスしたページまたはホームページに自動的にリダイレクトされます。 /span>
    • アクセス /logout ログアウトできます
    • ログインせずに特定のパスにアクセスできるようにしたい場合は、Spring Security の構成クラスをカスタマイズし、これらのパスを「ホワイトリスト」として構成できます。
  • プロジェクトの開始プロセス中に、ユーザー名を使用してランダムな一時パスワードが生成されます。user

パスワードの暗号化

開発現場では、すべてのユーザー パスワードはデータベースに保存する前に暗号化する必要があります。

ユーザーの元のパスワード (例:1234) は、オリジナル または と呼ばれることがよくあります。 < a i=4>平文、暗号化後に得られる結果 ( など) は、通常暗号文と呼ばれます。 >。 lkjfadshfdslafndshdsfaj

暗号化を処理する場合、通常はメッセージ ダイジェスト アルゴリズムを選択してユーザーのパスワードを処理する必要があります。

注: 暗号化アルゴリズムを使用してパスワードを暗号化して保存することはできません。通常、暗号化アルゴリズムは、送信プロセスのセキュリティを確保するために使用されます。

メッセージ ダイジェスト アルゴリズムは不可逆的であり、パスワードの暗号化に適しています。

メッセージ ダイジェスト アルゴリズムの主な機能は次のとおりです。

  • 同じアルゴリズムの場合、メッセージの長さに関係なく、ダイジェストの長さは固定されます。
  • メッセージが同じ場合、ダイジェストも同じでなければなりません
  • メッセージが異なる場合、ダイジェストは理論的には同じではありません(同じである可能性があります)。
    • メッセージの長さは無制限ですが、ダイジェストの長さは制限されており、固定されています

注: 理論的には、同じ概要に対応する n 個の異なるメッセージが存在しますが、そのような現象が発生する可能性は非常に低いです。

一般的なメッセージ ダイジェスト アルゴリズムは次のとおりです。

  • MD シリーズ(メッセージ ダイジェスト):MD2 / MD4 / MD5
    • すべての MD シリーズは 128 ビット アルゴリズムです
  • SHA ファミリー(安全なハッシュ アルゴリズム):SHA-1 / SHA-256 / SHA-384 / SHA-512
    • SHA-1 は 160 ビット アルゴリズムであり、その他はアルゴリズム名に対応します。たとえばSHA-256 は 256 ビット アルゴリズムです。
  • SM3 (国家暗号化アルゴリズム)
    • SM3 は 256 ビットのアルゴリズムです

Spring Boot では、spring-boot-starter 依存関係には、MD5 アルゴリズム処理を簡単に実装できるDigestUtils ツール クラスが含まれます。次に例を示します。

package cn.tedu.csmall.passport;

import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;

public class MessageDigestTests {

    @Test
    public void testMd5() {
        String rawPassword = "123456";
        String encodedPassword = DigestUtils.md5DigestAsHex(rawPassword.getBytes());
        System.out.println("rawPassword = " + rawPassword);
        System.out.println("encodedPassword = " + encodedPassword);
        // 123456 >>> e10adc3949ba59abbe56e057f20f883e
    }

}

他のメッセージ ダイジェスト アルゴリズムを使用したい場合は、commons-codec 依存関係を自分でプロジェクトに追加できます。DigestUtils という名前のツールもあります。この依存関係クラスは、さまざまなアルゴリズムの API を提供します。

メッセージアルゴリズムの特性として「同じメッセージであれば、要約も同じでなければならない」という特徴があるため、インターネット上にはメッセージと要約の対応関係を記録し、データベースに記録するプラットフォームがいくつか存在します。概要情報間の対応関係を知るために、概要に基づいて実行されます。ただし、これらのプラットフォームが記録できる対応関係は非常に限られているため、より複雑なメッセージを使用できますが、これらのメッセージはこれらのプラットフォームに含まれない可能性が高く、元のメッセージがこれらのプラットフォームによって逆クエリされることはありません。

言い換えると、元のパスワードが十分に複雑である限り、これらのプラットフォームによって「解読」されることはありません。

ただし、一部のシナリオでは複雑なメッセージ (パスワード) の使用がサポートされておらず、複雑な元のパスワードの使用を望まないユーザーもいます。そのため、対応するメッセージとダイジェストを網羅的にリストするのは簡単です。この問題を解決するには、暗号化を「」にする必要があります。このプロセスでは「Salt」が使用されます。Salt の本質は文字列です。その機能は、計算されるデータをより複雑にすることです。次に例を示します。

@Test
public void testMd5() {
    String salt = "kjkhglkjjg";
    String rawPassword = "123456";
    //                                                   123456kjkhglkjjg
    String encodedPassword = DigestUtils.md5DigestAsHex((rawPassword + salt).getBytes());
    System.out.println("rawPassword = " + rawPassword);
    System.out.println("encodedPassword = " + encodedPassword);
}

塩の使用方法を含め、塩の特定の値について明確な要件はありません。

さらに、複数の暗号化、つまり上記のアルゴリズムを周期的に呼び出すこともできます。

したがって、パスワードのセキュリティを向上するには、次のようにします。

  • より強力なパスワードを強制する
  • 塩で
  • 複数の暗号化
  • より安全なアルゴリズムを使用する
  • 上記の方法を併用してください

ソルトの追加について: 通常、ランダムなソルト値を使用できます。元のパスワードがまったく同じであっても、得られる暗号化結果はまったく異なります。例:

@Test
public void testMd5() {
    for (int i = 0; i < 5; i++) {
        String salt = UUID.randomUUID().toString();
        String rawPassword = "123456";
        String encodedPassword = DigestUtils.md5DigestAsHex((salt + rawPassword).getBytes());
        System.out.println("rawPassword = " + rawPassword);
        System.out.println("encodedPassword = " + encodedPassword);
        System.out.println();
    }
}

たとえば、上記の実行結果 (毎回異なります):

rawPassword = 123456
encodedPassword = 678408c66bef83edf72b11ad5b505161

rawPassword = 123456
encodedPassword = 99c3da1ef1d1e9ea976c91a00af0b4c0

rawPassword = 123456
encodedPassword = 52c809ab1ef18607c0f357d1caa4082f

rawPassword = 123456
encodedPassword = faf506f5d7a8d5109fc24d4c700fb136

rawPassword = 123456
encodedPassword = e89b5401bfbd233e24cb3862425ccdb8

ランダム ソルト値を使用した後は、そのランダム ソルト値を記録する必要があることに注意してください。 (データ フィールドを追加するときに、データ テーブルで特別なソルト値を使用できます)ソルト アドレスと暗号化結果を記録するか、記録されたパスワードとして 1 つの文字列に結合します)。そうしないと、その後のパスワードの検証中に一致する結果を取得できなくなります。

使用例:

rawPassword = 123456
salt = 4da1ba18-e9c5-4adc-bc0e-3768aca841ad
encodedPassword = ef3bcab34967ab87d9a3002366439898

得到最终密码(盐值拼接密文):
4da1ba18-e9c5-4adc-bc0e-3768aca841adef3bcab34967ab87d9a3002366439898

Spring Security フレームワークを使用する場合、このフレームワークには BCryptPasswordEncoder クラスも含まれます。このクラスは、BCrypt アルゴリズムを使用してパスワードを処理し、encode() メソッドで暗号化を実現でき、matches() メソッドを呼び出すことで元のテキストと暗号文を比較できます。 (これら 2 つのメソッドは PasswordEncoder インターフェイスで定義されています)

Spring Security で特定のリクエストを解放する

デフォルトでは、Spring Security では、アクセスする前にすべてのリクエストがログインする必要があるため、特定のリクエストを解放する必要がある場合、これらのリクエスト パスを「ホワイトリスト」として構成できます。

は、 WebSecurityConfigurerAdapter クラスを継承し、 configurer(HttpSecurity) メソッドをオーバーライドする構成クラスをカスタマイズする必要があります。

package cn.tedu.csmall.passport.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        log.debug("创建密码编码器组件:BCryptPasswordEncoder");
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 在配置路径时,可以使用星号作为通配符
        // 使用 /* 只能匹配1层级路径,例如 /user 或 /brand,不可以匹配多层级,例如不可以匹配到 /user/list
        // 使用 /** 可以匹配若干层级路径

        http.csrf().disable(); // 禁用防止跨域访问,如果无此配置,白名单路径的异步访问也会出现403错误

        http.authorizeRequests() // 请求需要被授权才可以访问
            .antMatchers("/**") // 匹配某些路径
            .permitAll(); // 允许直接访问(不需要经过认证和授权)
    }
}

データベース内のアカウントを使用してログインします

Spring Security では、デフォルトのログイン (デフォルトで存在する /login ページ) を使用し、デフォルトでユーザー名として user を使用し、スタートアップを使用します。一時パスワードが生成されました。処理中に、データベース内のアカウントを使用してログインすることもできます。Spring Security は、入力ボックスに入力されたユーザー名とパスワードを自動的に取得し、インターフェイス タイプ オブジェクトを自動的に呼び出します。UserDetailsService UserDetails loadUserByUsername(String username) メソッドを実行し、返された UserDetails オブジェクトを取得します。このオブジェクトにはパスワードの暗号文値が含まれている必要があります。次に、Spring Security はログイン インターフェイスからそれを自動的に取得します。元のパスワード テキストは暗号化され、UserDetails の暗号テキストと比較されて、ログインが成功するかどうかが判断されます。

テストする前に、Spring Security の configurer(HttpSecurity) メソッドを無効にする (関連するコードをコメントアウトする) 必要があります。そうしないと、 /login ページが表示されません。

次に、プロジェクトのルート パッケージのクラスsecurity.UserDetailsServiceImplをカスタマイズし、UserDetailsService インターフェイスを実装して、インターフェイスの を書き換えます。 loadUserByUsername()方法:

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("Spring Security自动根据用户名【{}】查询用户详情", s);
        return null;
    }
    
}

UserDetailsService インターフェースの実装クラスが記述され、このクラスのオブジェクトが Spring によって作成されると、Spring Security はこのクラスのオブジェクトを自動的にアセンブルします。後でプロジェクトを開始するときに使用されます。デフォルトのランダムなパスワードが生成され、デフォルトのユーザー名userは使用できなくなります。

このメソッドで特定のアカウント情報を返すことをテストできます。次に例を示します。

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    log.debug("Spring Security自动根据用户名【{}】查询用户详情", s);

    // 以下是临时使用的代码
    if ("wangkejing".equals(s)) {
        UserDetails userDetails = User.builder()
                .username("wangkejing") // 用户名
                .password("$2a$10$XzUcx6Oag7n0tNKhBAGQEe5sFv9Jow9Fa0020UiWkfajUue7bmjz6") // 密码,此密文的原文是123456
                .disabled(false) // 账号是否禁用
                .accountLocked(false) // 账号是否锁定
                .accountExpired(false) // 账号是否过期
                .credentialsExpired(false) // 认证是否过期
                .authorities("临时给出任意字符串") // 【必须】此账号的权限信息
                .build();
        return userDetails;
    }

    return null;
}

この時点でプロジェクトを再起動し、 /login を通じてログインをテストすると:

  • ユーザー名がそうでない場合wangkejing、ブラウザはプロンプトを表示しUserDetailsService returned null、IntelliJ IDEA コンソールに例外メッセージが表示されます。
  • ユーザー名がwangkejingであり、パスワードが123456ではない場合、ブラウザは「ユーザー名またはパスワードが間違っています」というプロンプトを表示します
  • ユーザー名がwangkejing、パスワードが123456の場合、ログインは成功します(ログイン後のジャンプは404になる場合があります)

データベース内のアカウントを使用してログインする場合は、上記のコードを「ユーザー名に基づいて管理者情報を照会します。このユーザー名に対応するデータがある場合は、管理者情報を照会します。カプセル化」に置き換えます。会員情報をUserDetailsに入れて返します。」

したがって、次に実行するタスクは次のとおりです。

  • ルート パッケージの下にクラスを作成しますpojo.vo.AdminLoginVO。これには少なくともユーザー名、パスワードが含まれます

  • AdminMapperインターフェースに抽象メソッドを追加しますAdminLoginVO getByUsername(String username);

  • 上記の抽象メソッドによってマップされた SQL をAdminMapper.xmlで設定します。

    • select username, password from ams_admin where username=?
      
  • でテスト済みAdminMapperTests

  • UserDetailsServiceImpl ではAdminMapper オブジェクトを自動的にアセンブルし、上記のクエリ関数を呼び出して、クエリ結果の情報を返された にカプセル化します。 a>UserDetails、有効なレコードが見つからない場合は、直接戻ることができますnull

ログインプロセスをカスタマイズする

デフォルトでは、Spring Security にはデフォルトのログイン ページがあり、ユーザー名とパスワードを入力すると、Spring Security は自動的にログイン要求を受信し、処理します。ログインが成功すると、以前にアクセスしたページに自動的にジャンプします。失敗すると、デフォルトのログイン ページにエラー メッセージが表示されます。

上記のプロセスは、次の理由から開発の実践には適していません。

  • これはフロントエンドとバックエンドの分離ではありません (サーバーはログイン処理後に JSON 結果に応答しません)。
  • Validation フレームワークを使用してリクエスト パラメーターの形式を検証するなど、詳細を処理するのは不便です。

この問題を解決するには、カスタム サービスの実装プロセス中に Spring Security メカニズムを使用してユーザー名とパスワードを検証することを除いて、他のデータ処理プロセスの開発と同じことを行う必要があります。

まず、コントローラーはクライアントによって送信されたログイン要求を受信できる必要があります。

  • ルート パッケージの下にクラスを作成しpojo.dto.AdminLoginDTO、ユーザー名やパスワードなどのログイン リクエストの関連パラメータをこのクラスにカプセル化します

  • ログイン リクエストを処理するメソッドをAdminControllerに追加します:

    • @PostMapping("/login")
      public JsonResult login(AdminLoginDTO adminLoginDTO) {
          // 通过日志简单的输出
          return JsonResult.ok();
      }
      
  • 完了後、Knife4j でアクセスをテストできます。応答結果は常に成功です (実際のログイン実装はまだありません)。

次に、サービスでログインを処理する準備をする必要があります。

  • IAdminService中声明:void login(AdminLoginDTO adminLoginDTO);
  • 上記のメソッドを書き直しますAdminServiceImpl。実装の詳細は今のところ空白のままで大丈夫です
  • ログインを処理するAdminControllerときに、サービス コンポーネントのこのメソッドを呼び出します

Service でのログイン処理の詳細については、Spring Security の AuthenticationManager オブジェクトを使用して Spring Security の認証処理 (その後のユーザー情報の保存、アクセス許可に必要) を実行する必要があります。 、など)。

取得する必要があるAuthenticationManager場合は、Spring Security 構成クラス (カスタマイズされたSecurityConfigurationクラス) で書き直す必要がありますauthenticationManager()メソッド。このメソッドは AuthenticationManager オブジェクトを返し、オーバーライドするときにメソッドに @Bean アノテーションを追加します。これにより、Spring は自動的にこのメソッドを呼び出して、結果は Spring コンテナに保存されます。

@Bean // 必须添加此注解
@Override
protected AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}

次に、ビジネス実装クラスに戻り、AuthenticationManager オブジェクトを自動的にアセンブルします。特定の実装中に、このオブジェクトの authenticate() メソッドを呼び出して、目的を達成します。 Spring Security 認証では、このメソッドのパラメータを使用してUsernamePasswordAuthenticationTokenユーザー名とパスワードをカプセル化できます。

@Override
public void login(AdminLoginDTO adminLoginDTO) {
    log.debug("开始处理管理员登录的业务,参数:{}", adminLoginDTO);

    // 调用AuthenticationManager执行Spring Security的认证
    Authentication authentication
            = new UsernamePasswordAuthenticationToken(
                    adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
    authenticationManager.authenticate(authentication);
    log.debug("登录成功!");
}

上記のコードの実行フローは大まかに次のとおりです。

请求 ==> Controller ==> Service ==> AuthenticationManager ==> UserDetailsServiceImpl ==> Mapper

セッションについて

HTTP プロトコルはステートレスです。プロトコル自体の観点から見ると、通信プロセス中にユーザー関連の情報は記録されません。ユーザーが初めてサーバーにアクセスした後、2 回目に再度アクセスしたとき、サーバーはこれが最初の訪問者と同じユーザーであることを認識しません。

開発の現場では、ユーザーの身元を明確にする必要があります。そのため、各プログラミング言語は、セッションに基づいた処理メカニズムを提供します。セッションは、Map によって維持されるデータと同様のデータです。サーバー側プログラム。各クライアントは、ここで特定の値に対応する一意のキーを持っています。したがって、各クライアントがアクセスするときに、対応するセッション データにデータを保存することができます。また、後で以前に保存したデータを取り出すこともできます。たとえば、ログインに成功した後、ユーザーの ID をセッションに保存することができます。 「セッションにこのIDが存在するかどうか」に基づいてユーザーがログインしているかどうかを判断し、保存されているユーザーIDに基づいてユーザーの身元を識別します。

セッションの具体的な使用法では、クライアントが初めてサーバーにリクエストを行うときに、いわゆるMapキーはなく、サーバーが自動的にキーを生成します。キーがクライアントに応答されると、クライアントは自動的にキーを保存し、後続の各リクエストで自動的にそのキーを運びます。このプロセス中、クライアントは Cookie テクノロジーを使用してキーをクライアントに保存します。

Session の Key 自体は UUID 値であり、特定の情報の意味はありませんが、各クライアントがサーバー側の Session にアクセスする際に競合が起こらないように一意です。

現時点では、クラスター アーキテクチャでは同じユーザーからの複数のリクエストがクラスター内の異なるサーバーで処理される可能性があり、セッションはサーバー メモリに保存されているデータであるため、ユーザーを識別するためにセッション テクノロジを使用することは推奨されていません。ユーザーの身元は認識されません。

トークンについて

トークンは「チケット」「トークン」とも呼ばれますが、最大の特徴はセッションキーのようなデータが情報の意味を体現していることです! 「乗車券」に相当します 一部のデータを乗車券に反映させることができます サーバーは「駅」に相当します 異なる駅でも同様の乗車券照合機構を備えており、「乗車券」の真贋を識別することができ、そこから特定の情報を取得します。

JWTについて

JWT = Json Web トークン。JSON を使用して通常のトークンよりも多くの情報をカプセル化します。

ネットワーク上で送信される JWT データのセキュリティを確保するために、JWT 自体は暗号化されたデータのセットであり、通常、JWT の生成と JWT 内のデータの解析を担当する関連ツールキットがあります。

これを実現するには、jjwt ツールキットを使用することを選択できます。プロジェクトに依存関係を追加します。

<!-- JJWT(Java JWT) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

テスト クラスを作成して、JWT の生成とJWT の解析

public class JwtTests {

    // 密钥
    String secretKey = "jfdsakjdsfk%&JFDsfFDFADSFhj875421dsafhjafdsfdsalkjafdsafds";

    @Test
    public void testGenerateJwt() {
        // 准备Claims
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 9527);
        claims.put("name", "刘老师");

        // 准备过期时间:1分钟
        Date expirationDate = new Date(System.currentTimeMillis() + 1 * 60 * 1000);

        // JWT的组成部分:Header(头)、Payload(载荷)、Signature(签名)
        String jwt = Jwts.builder()
                // Header:用于配置算法与此结果数据的类型
                // 通常配置2个属性:typ(类型)、alg(算法)
                .setHeaderParam("typ", "jwt")
                .setHeaderParam("alg", "HS256")
                // Payload:用于配置需要封装到JWT中的数据
                .setClaims(claims)
                .setExpiration(expirationDate)
                // Signature:用于指定算法与密钥(盐)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        System.out.println(jwt);
        // eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9
        // .
        // eyJuYW1lIjoi5YiY6ICB5biIIiwiaWQiOjk1MjcsImV4cCI6MTY1NzYxOTY2Nn0
        // .
        // kDW_hgQKbBb01WA5kQeMaxY8Fc_H2Yao2DdFABlbuiw
    }

    @Test
    public void testParseJwt() {
        String jwt = "eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5YiY6ICB5biIIiwiaWQiOjk1MjcsImV4cCI6MTY1NzY3NTExNX0.yMG3xL4b2SCNjaqwrIHB3tfA9HHmkiiLzpuYzJCSlog";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Object id = claims.get("id");
        Object name = claims.get("name");
        System.out.println("id=" + id);
        System.out.println("name=" + name);
    }

}

JWT を解析するときに、次のような例外が発生する場合があります。

  • JWT データの有効期限が切れると、次のようになります。

    io.jsonwebtoken.ExpiredJwtException: 
    
    JWT expired at 2022-07-13T09:18:35Z. Current time: 2022-07-13T09:27:35Z, a difference of 540694 milliseconds.  Allowed clock skew: 0 milliseconds.
    
  • 生成と解析に使用されるキーが矛盾している場合、または JWT データの最後の部分が悪意を持って改ざんされている場合:

    io.jsonwebtoken.SignatureException: 
    
    JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
    
  • JWT データのパート 1 が悪意を持って改ざんされた場合:

    io.jsonwebtoken.MalformedJwtException: 
    
    Unable to read JSON value: {"t{�:"jwt","alg":"HS256"}
    

プロジェクトでの JWT の使用

プロジェクトで JWT を使用する場合、通常は次の問題に注意する必要があります。

  • JWT を生成するタイミング: 通常、ログインに成功すると、JWT が生成され、JWT がクライアントに応答されます。
  • クライアントがサーバーにアクセスするために JWT を運ぶのはいつですか: サーバーは気にしません
  • JWT をいつチェックするか:??????

JWTをクライアントに応答する

クライアントに JWT に応答する必要がある場合は、次のことを行う必要があります。

  • AdminServiceImpllogin() で、authenticate() によって返された結果を取得し、この結果を < a i=4> 型であれば、この 型から元々 に格納されていたデータを取得し、必要な部分を(一時的に < a i=7>)、JWT データとして生成します (今のところカプセル化ツール クラスは考慮せず、テスト クラスを参照してください)UserUserUserDetailsServiceusername
  • IAdminService インターフェースの login() の戻り値を に変更します。String
  • AdminServiceImpl クラスで、login() の戻り値もString に変更し、JWT データを返します。 >
  • ログインを処理するメソッドAdminControllerでは、Service コンポーネントのメソッド呼び出し時に戻り値を取得し、この戻り値を応答結果にカプセル化します

AdminServiceImpl の実装コードの概要

@Override
public String login(AdminLoginDTO adminLoginDTO) {
    log.debug("开始处理管理员登录的业务,参数:{}", adminLoginDTO);

    // 调用AuthenticationManager执行Spring Security的认证
    Authentication authentication
            = new UsernamePasswordAuthenticationToken(
                    adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
    Authentication loginResult = authenticationManager.authenticate(authentication);

    // 以上调用的authenticate()方法是会抛出异常的方法,如果还能执行到此处,则表示用户名与密码是匹配的
    log.debug("登录成功!认证方法返回:{} >>> {}", loginResult.getClass().getName(), loginResult);
    // 从认证结果中获取Principal,本质上是User类型,且是UserDetailsService中loadUserByUsername()返回的结果
    log.debug("尝试获取Principal:{} >>> {}", loginResult.getPrincipal().getClass().getName(), loginResult.getPrincipal());
    User user = (User) loginResult.getPrincipal();
    String username = user.getUsername();
    log.debug("登录成功的用户名:{}", username);

    // 应该在此处生成JWT数据,向JWT中存入:id(暂无), username, 权限(暂无)
    Map<String, Object> claims = new HashMap<>();
    claims.put("username", user.getUsername());
    Date expiration = new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000);
    String jwt = Jwts.builder()
            .setHeaderParam("typ", "jwt")
            .setHeaderParam("alg", "HS256")
            .setClaims(claims)
            .setExpiration(expiration)
            .signWith(SignatureAlgorithm.HS256, "lkjfdslkjafds8iufnmdsfadsa")
            .compact();
    log.debug("生成JWT数据:{}", jwt);
    return jwt;
}

JWTデータを運ぶクライアントについて

クライアントが認証を必要とするリソースにアクセスしようとする場合、クライアントは JWT データを伝送する必要があり、サーバーは JWT データを取得、確認、解析する必要があります。

クライアントが JWT を運ぶ場合、通常、リクエスト ヘッダー (リクエスト ヘッダー) の Authorization 属性に JWT データを配置します。通常、サーバー側プログラムは次のように設計されています。リクエスト ヘッダーの Authorization 属性から JWT データを取得します。

サーバー側でJWTをチェックする

JWT はさまざまなリクエストに対してチェックする必要があるため、コントローラーでは JWT が処理されません。

通常、JWT はフィルター コンポーネントでチェックする必要があります。

  • フィルターは、クライアント要求を受信するための Java サーバー側プログラム (使用するフレームワークに関係なく) の最初のコンポーネントであり、すべての要求はコントローラーに実行される前にフィルターを通過します。
  • XXXX

カスタムフィルタークラスに必要なもの:

/**
 * <p>处理JWT的过滤器</p>
 *
 * <p>此过滤器将尝试获取请求中的JWT数据,如果存在有效数据,将尝试解析,</p>
 * <p>然后,将解析得到的结果存入到Spring Security的上下文中,</p>
 * <p>以至于Spring Security框架中的其它组件能够从上下文中获取到用户的信息,</p>
 * <p>从而完成后续的授权访问。</p>
 */
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    // 最终,过滤器可以选择“阻止”或“放行”
    // 如果选择“阻止”,则后续的所有组件都不会被执行
    // 如果选择“放行”,会执行“过滤器链”中剩余的部分,甚至继续向后执行到控制器等组件
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        // 此方法是任何请求都会执行的方法
        log.debug("执行JwtAuthorizationFilter");

        // 清除Security的上下文
        // 如果不清除,只要此前存入过信息,即使后续不携带JWT,上下文中的登录信息依然存在
        SecurityContextHolder.clearContext();

        // 从请求头中获取JWT
        String jwt = request.getHeader("Authorization");
        log.debug("从请求头中获取的JWT数据:{}", jwt);

        // 先判断是否获取到了有效的JWT数据,如果无JWT数据,直接放行
        if (!StringUtils.hasText(jwt)) {
            log.debug("请求头中的JWT数据是无效的,直接放行");
            filterChain.doFilter(request, response);
            return;
        }

        // 如果获取到了有效的JWT值,则尝试进行解析
        Claims claims = Jwts.parser().setSigningKey("lkjfdslkjafds8iufnmdsfadsa")
                .parseClaimsJws(jwt).getBody();
        Object username = claims.get("username");
        log.debug("从JWT中解析得到用户名:{}", username);

        // TODO 临时:准备用户权限
        GrantedAuthority authority = new SimpleGrantedAuthority("1");
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(authority);

        // 当解析成功后,应该将相关数据存入到Spring Security的上下文中
        Authentication authentication
                = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);

        // 以下代码将执行“放行”
        filterChain.doFilter(request, response);
    }

}

そして、構成クラスに構成を追加します。

package cn.tedu.csmall.passport.config;

import cn.tedu.csmall.passport.filter.JwtAuthorizationFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthorizationFilter jwtAuthorizationFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        log.debug("创建密码编码器组件:BCryptPasswordEncoder");
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 在配置路径时,可以使用星号作为通配符
        // 使用 /* 只能匹配1层级路径,例如 /user 或 /brand,不可以匹配多层级,例如不可以匹配到 /user/list
        // 使用 /** 可以匹配若干层级路径

        // 白名单,不需要登录就可以访问
        String[] urls = {
                "/admins/login",
                "/doc.html",
                "/**/*.css",
                "/**/*.js",
                "/favicon.ico",
                "/v2/api-docs",
                "/swagger-resources"
        };

        http.csrf().disable(); // 禁用防止跨域访问,如果无此配置,白名单路径的异步访问也会出现403错误

        http.authorizeRequests() // 请求需要被授权才可以访问
                .antMatchers(urls) // 匹配某些路径
                .permitAll() // 允许直接访问(不需要经过认证和授权)
                .anyRequest() // 除了以上配置过的其它任何请求
                .authenticated(); // 已经通过认证,即已经登录过才可以访问

        // 添加处理JWT的过滤器,必须执行在处理用户名、密码的过滤器(Spring Security内置)之前
        http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
    }

}

大まかな実行プロセス

 

完全なコーディング プロセス: セキュリティ + JWT に基づく管理者ログイン

  • 相关依赖:spring-boot-starter-securityjjwt
  • 管理者ログイン用の VO クラスを作成します。例:AdminLoginVO
  • AdminMapper インターフェースと AdminMapper.xml ファイルに実装: ユーザー名に基づいて管理者情報をクエリします。これには少なくともユーザー名、パスワード、権限が含まれている必要があります。 a>
  • カスタム クラス、UserDetailsService インターフェイスの実装、loadUserByUsername() メソッドのオーバーライド、このクラスの AdminMapper クエリの渡し 対応する管理者を見つける情報を取得し、 UserDetails 型のオブジェクトにカプセル化して返します
  • セキュリティ構成クラスを作成し、WebSecurityConfigurerAdapter クラスから継承し、このクラスの @Bean メソッドを使用して AuthenticationManager オブジェクト。@Bean メソッドを使用して BCryptPasswordEncoder オブジェクト を取得します。
  • IAdminService インターフェースにログイン抽象メソッドを追加し、AdminServiceImpl でこのメソッドをオーバーライドします。メソッド本体で は認証を実行します。認証が成功すると、JWT データが生成されて返されます。この JWT データには、ユーザー名と必要な情報が含まれている必要がありますAuthenticationManagerauthenticate()
  • AdminController でログイン リクエストを処理し、IAdminService タイプ コンポーネントを呼び出して実装し、呼び出しによって取得した JWT をクライアントに応答します< /span> a>

完全なコーディングプロセス: ログイン後にアクセス

  • セキュリティ構成クラスで、いくつかのホワイトリストを指定します。これらは、ログインせずに直接アクセスできます。他のリクエスト パスは、アクセスする前にログインする必要があります。注意: ログイン、登録、およびその他のリクエスト パスはホワイトリストに含まれている必要があります。そうでない場合は、アクセスできない。
  • このフィルターで JWT フィルターを作成します。
    • セキュリティコンテキストのクリア
    • リクエストヘッダーからJWTを取得
    • JWTデータに対して基本的な判断(値があるかどうか)を行い、有効な値がない場合はそのまま渡します。
    • 有効な JWT が取得された場合は、それを解析してユーザー情報を取得し、コンテキストにユーザー情報を保存します。
  • セキュリティ構成クラスで、上記のフィルタを追加し、UsernamePasswordAuthenticationFilter の前に追加します。

CORSについて

CORS: デフォルトでは、クロスドメインの非同期アクセスは許可されていません。

Spring MVC フレームワークを使用し、クロスドメイン アクセスを許可する必要がある場合、構成クラスをカスタマイズし、WebMvcConfigure インターフェイスを実装し、addCorsMappings() 方法:

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
    
}

プロジェクトで Spring Security フレームワークがさらに使用される場合、クライアントが複雑なリクエスト (Authorization 属性の追加など、リクエスト ヘッダーにカスタマイズされた型破りな属性) を送信すると、 Spring Security 構成クラスで複雑なリクエストに対するクロスドメイン アクセスを許可するには、次のような解決策が考えられます。

http.cors();

または:

http.antMatchers(HttpMethod.OPTIONS, "/**").permitAll();

このような処理が必要な理由は、複雑なリクエスト自体にプリフライト メカニズムがあるためです。リクエストを送信すると、クライアントは最初に OPTIONS タイプのリクエストを自動的に送信します。 、サーバーが通過しない可能性があり、 403 エラーが表示され、実際のリクエストが送信されます (例: GETPOST)。 ) 複雑なリクエストヘッダー情報は送信されません。

ブラウザ側では、複雑なリクエストが正常に送信されると、プリフライト検査を実行するための後続のリクエストは自動的に送信されませんOPTIONS

許可されたアクセスを実現する

許可されたアクセスを実現する手順:

  • ユーザーがログインしようとすると、ユーザー名に基づいてデータベースから管理者の権限情報を照会する必要があります。

  • UserDetailsServiceImpl では (認証のためにログインすると、Spring Security フレームワークはこのクラスの loadUserByUesrname() メソッドを自動的に呼び出します)、ユーザー名 有効な管理者情報の後に、権限情報を UserDetails に保存します。

    • List<String> 形式の権限セットを String... 形式に変換します。例:

      admin.getPermissions().toArray(new String[] {})
      
  • AdminServiceImpllogin() では、認証が成功した後、返された Authentication から権限情報を取得してマージします。 JWT に生成

    • JWT から権限を取得して通常の形式に復元できるようにするには、権限リスト (Collection<? extend GrandtedAuthority>) を JSON 形式の文字列に変換してから、書かれた
  • JwtAuthorizationFilter で、JWT からの権限を解析し、セキュリティ コンテキストに保存します

    • JWT から解析された権限は JSON 形式の文字列であり、セキュリティ コンテキストに保存する前に Collection<? extend GrandtedAuthority> タイプに復元する必要があります。 List<SimpleGrantedAuthority>
  • セキュリティ構成クラスにアノテーションを追加して、グローバル認証アクセス チェックを有効にしますSecurityConfiguration@EnableGlobalMethodSecurity(prePostEnabled = true)

    • この構成は 1 回限りの構成です
  • コントローラで、リクエストの処理メソッドに @PreAuthorize アノテーションを使用し、そのリクエストで hasAuthority 属性を構成して、このリクエストが何らかの権限を持っている

    • 例えば:@PreAuthorize("hasAuthority('/ams/admin/read')")

ユーザー名に基づいて管理者の権限を照会します

まず、AdminLoginVO に必要な属性を追加します。

package cn.tedu.csmall.passport.pojo.vo;

import lombok.Data;

import java.io.Serializable;
import java.util.List;

@Data
public class AdminLoginVO implements Serializable {

    /**
     * 管理员的id
     */
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码(密文)
     */
    private String password;

    /**
     * 账号是否启用,0=禁用,1=启用
     */
    private Integer enable;

    /**
     * 此账号的权限列表
     */
    private List<String> permissions;

}

次に、AdminMapper.xmlでクエリを構成します。

<!-- AdminLoginVO getByUsername(String username); -->
<select id="getByUsername" resultMap="LoginResultMap">
    SELECT
        ams_admin.id,
        ams_admin.username,
        ams_admin.password,
        ams_admin.enable,
        ams_permission.value
    FROM ams_admin
    LEFT JOIN ams_admin_role ON ams_admin.id=ams_admin_role.admin_id
    LEFT JOIN ams_role_permission ON ams_admin_role.role_id=ams_role_permission.role_id
    LEFT JOIN ams_permission ON ams_role_permission.permission_id=ams_permission.id
    WHERE
        ams_admin.username=#{username}
</select>

<resultMap id="LoginResultMap" type="cn.tedu.csmall.passport.pojo.vo.AdminLoginVO">
    <id column="id" property="id" />
    <result column="username" property="username" />
    <result column="password" property="password" />
    <result column="enable" property="enable" />
    <collection property="permissions" ofType="java.lang.String">
        <constructor>
            <arg column="value" />
        </constructor>
    </collection>
</resultMap>

JWTフィルターの処理詳細

JWT データの有効期限切れ、署名エラー、不正なデータなどにより、JWT の解析が失敗する場合があります。これらのエラーは処理する必要があります。処理しないと、処理されない例外が発生し、最終的に 500 エラーが発生します。

上記のエラーは、JWT データの有効期限が切れている場合、JWT データが悪意を持って改ざんされている場合、その他のエラーの可能性の 3 つに大別されます。

まず、いくつかのエラーに対応する新しいビジネス ステータス コードをServiceCodeに追加します。

/**
 * 错误:JWT数据错误,可能被恶意篡改
 */
public static final int ERR_JWT_INVALID = 40001;
/**
 * 错误:JWT过期
 */
public static final int ERR_JWT_EXPIRED = 40300;

その後、try...catch を JWT フィルタで使用して、例外をキャッチして処理する必要があります。

ログインユーザーIDにカスタム情報を追加する

Spring Security フレームワークは、ユーザー ID およびその他の関連情報を使用またはカプセル化しません。使用中にさらに多くの情報を自分でカプセル化し、それをユーザー ID に追加する必要がある場合は、以下を行う必要があります。

  • カスタム クラスの実装UserDetailsインターフェース
  • またはカスタム クラス継承Userクラス

そして、ID などの必要な属性をカスタム クラスに追加し、UserDetailsService の実装クラスで loadUserByUsername() を返します。メソッド カスタム クラスのオブジェクト。

それではAdminDetailsクラスを作成します:

@Setter
@Getter
@EqualsAndHashCode
@ToString(callSuper = true)
public class AdminDetails extends User {

    private Long id;

    public AdminDetails(String username, String password, boolean enabled,
                        Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled,
                true, true, true,
                authorities);
    }

}

UserDetailsServiceImpl に戻る必要があるとき:

List<String> permissions = admin.getPermissions();
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String permission : permissions) {
    authorities.add(new SimpleGrantedAuthority(permission));
}

AdminDetails adminDetails = new AdminDetails(
        admin.getUsername(),
        admin.getPassword(),
        admin.getEnable() == 1,
        authorities
);
adminDetails.setId(admin.getId());
log.debug("即将向Spring Security返回AdminDetails:{}", adminDetails);

次に、AdminServiceImpllogin() メソッドに AuthenticationManagerauthenticate()認証を実行し、認証を通過した場合の戻り結果は上記のAdminDetailsオブジェクトです。したがって、そこから管理者のIDを取得し、JWTデータの生成に使用できます。ユーザーが成功した後に取得されるJWTデータログインにはID情報が含まれます。

その後、クライアントがリクエストを送信すると、送信される JWT には ID 情報も含まれます。この ID は JwtAuthenticationFilter で解析することで取得できます。最終的に、この ID 値はセキュリティ コンテキスト。UsernamePasswordAuthenticationToken クラスの principal 属性 (Object タイプ) を使用できるため、カスタム クラスは次の目的で使用されます。その後の可能性をカプセル化する 必要な管理者情報:

package cn.tedu.csmall.passport.security;

import lombok.Data;

import java.io.Serializable;

/**
 * 当前登录的当事人
 *
 * @author [email protected]
 * @version 0.0.1
 */
@Data
public class LoginPrincipal implements Serializable {

    /**
     * 当前登录的用户id
     */
    private Long id;

    /**
     * 当前登录的用户名
     */
    private String username;

}

次に、フィルターに次のように保存します。

// 准备当前登录用户的当事人信息
LoginPrincipal loginPrincipal = new LoginPrincipal();
loginPrincipal.setId(Long.parseLong(id.toString()));
loginPrincipal.setUsername(username.toString());

// 当解析成功后,应该将Authentication存入到Spring Security的上下文中
Authentication authentication
        = new UsernamePasswordAuthenticationToken(loginPrincipal, null, authorities);
//                                             == 以上封装了当事人信息 ==
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
log.debug("已经向Security的上下文中写入:{}", authentication);

この時点で、クライアントが JWT を使用してサーバーにアクセスする場合、サーバー側のセキュリティ コンテキストには管理者の ID、ユーザー名、権限が含まれます。このうち権限は単独で使用する必要はなく、管理者の ID とユーザー名を取得する必要がある場合は、次のようにすることができます。リクエストを処理するコントローラのメソッドにパラメータ リストを追加します。 を追加するだけです。このパラメータは、セキュリティ コンテキスト (フィルタに保存されているオブジェクト) の認証情報です。例: @PreAuthorizeAuthentication

// http://localhost:9081/admins
@ApiOperation("查询管理员列表")
@ApiOperationSupport(order = 401)
@PreAuthorize("hasAuthority('/ams/admin/read')")
@GetMapping("")
public JsonResult list(Authentication authentication) {
    log.debug("接收到查询管理员列表的请求");
    log.debug("当前认证信息:{}", authentication);
    LoginPrincipal principal = (LoginPrincipal) authentication.getPrincipal();
    Long id = principal.getId();
    log.debug("从认证信息中获取当前登录的管理员的id:{}", id);
    String username = principal.getUsername();
    log.debug("从认证信息中获取当前登录的管理员的用户名:{}", username);
    List<AdminListItemVO> admins = adminService.list();
    return JsonResult.ok(admins);
}

Authentication から 注釈を追加します。 、直接使用できます: 属性にカプセル化されたオブジェクト) を使用し、このパラメータの前に を宣言できます。パラメータ (フィルタ内の LoginPrincipalするのは面倒です。型を自分で取得して変換する必要があります。取得LoginPricipalUsernamePasswordAuthenticationTokenpricipal@AuthenticationPrincipal

// http://localhost:9081/admins
@ApiOperation("查询管理员列表")
@ApiOperationSupport(order = 401)
@PreAuthorize("hasAuthority('/ams/admin/read')")
@GetMapping("")
public JsonResult list(@AuthenticationPrincipal LoginPrincipal loginPrincipal) {
    log.debug("接收到查询管理员列表的请求");
    log.debug("当前认证信息中的当事人信息:{}", loginPrincipal);
    Long id = loginPrincipal.getId();
    log.debug("从认证信息中获取当前登录的管理员的id:{}", id);
    String username = loginPrincipal.getUsername();
    log.debug("从认证信息中获取当前登录的管理员的用户名:{}", username);
    List<AdminListItemVO> admins = adminService.list();
    return JsonResult.ok(admins);
}

Spring Security フレームワークの関連概念

認証

認証情報。プロジェクトでは、これはインターフェイスです。一般的に使用される実装クラスはUsernamePasswordAuthenticationTokenです。その意味はアプリケーションのシナリオによって異なります。たとえば、 メソッドのパラメータでは、主にユーザー名とパスワードをカプセル化するために使用されますが、このメソッドの戻り結果では、成功したユーザーの情報を表します。ログイン。 AuthenticationManagerauthenticat()

認可

プロジェクトでは、認証は主に JWT を含むリクエスト ヘッダーの属性名によって表されます。これは推奨される属性名です。

権限

権限は通常、いくつかの文字列で表され、一意で読みやすい特性を持つ必要があります。フレームワークは、ログイン ユーザー情報とコントローラに設定された権限に基づいてチェックし、ユーザーがこの操作を実行する権限を持っているかどうかを判断します。 . .

主要

パーティは Authentication の属性の一部です。UsernamePasswordAuthenticationToken を例に取ると、プリンシパル、資格情報、権限という 3 つの主要な部分が含まれます。プロジェクトで Authentication が認証に使用されている場合、プリンシパルはユーザー名です。 Authentication がユーザー認証後の情報である場合、他の意味を含むことができます。例: ID、ユーザー名など。

トークン

チケットとトークンは、意味のあるデータの一部を運ぶ情報を指します。

ユーザーの詳細

ユーザーの詳細は、認証プロセス中にユーザー情報をカプセル化するために使用されます。たとえば、UserDetailsService インターフェイスの実装クラス、loadUserByUsername() メソッド内で使用されます。このタイプのオブジェクトが返される必要があり、Spring Security はこのメソッドを自動的に呼び出して、タイプ UserDetails の結果を取得します。この結果にはパスワードが含まれている必要があり、Spring Security は自動的に < a i=4> は、ユーザーがログインを要求するときに入力されたパスワードを検証します。このタイプは、認証が成功した後、 のプリンシパルにもなります。 PasswordEncoderAuthentication

Spring Security フレームワークを使用するときに関連するファイル

pom.xml

関連する依存関係を追加する必要があります。Spring Security を使用する必要がある場合はspring-boot-starter-securityを追加します。JWT を使用する必要がある場合はjjwt を追加します (生成およびJWT データ解析ツールキット) と fastjson (オブジェクトを JSON 文字列に、またはその逆に変換するためのツールキット)。

UserDetailsS​​erviceImpl

UserDetailsService インターフェースの実装クラスであり、UserDetails loadUserByUsername(String s) メソッドを書き換える必要がありますが、Spring Security は認証時にこのメソッドを自動的に呼び出します。このメソッドの返される結果には、パスワード、権限、およびその他の必要な情報 (API に従って決定) が少なくとも含まれている必要があります。

返される UserDetails については、通常 User 型を使用できますが、この型には < などの属性は含まれません。 a i=3> なので、カスタム クラスが インターフェイスを実装したり、カスタム クラスが から継承してそれを使用したりすることも可能です。返された オブジェクト。 idUserDetailsUserUserDetails

セキュリティ構成

は Spring Security フレームワークの構成クラスであり、WebSecurityConfigurerAdapter から継承する必要があります。

このクラスに@EnableGlabalMethodSecurity(prePostEnabled = true) アノテーションを追加すると、グローバル メソッドの認可チェックが有効になります(@PreAuthorize を使用して、リクエストを処理するメソッドの認可をチェックできるようになります) )。

通常、認証を実行する場合、PasswordEncoder 対応する@Bean メソッドはこのクラスで設定されます (このメソッドは他の設定クラスでも可能です)。 Spring Security は、この PasswordEncoder オブジェクトの メソッドを自動的に使用してパスワードを検証します。 matches()

  • 暗号文が BCrypt アルゴリズムによって生成された場合は、@Bean メソッドBCryptPasswordEncoderで返される必要があります。暗号文がない場合 (パスワードは暗号化されていない)、このメソッドはNoOpPasswordEncoderなどを返す必要があります

このクラスでは、 に対応するAuthenticationManager を設定することもできます。このメソッドは通常、 を返すために使用されるオーバーライドされたメソッドです。 オブジェクトは、サービスでこのタイプのプロパティを自動的に組み立てたり、 メソッドを呼び出して認証を実行したりするなど、他のコンポーネントで認証を実行するために使用されます。 @BeanAuthenticationManagerauthenticate()

このカテゴリでは、void configure(HttpSecurity http) メソッドをオーバーライドし、このメソッド内でリクエストを処理する方法を構成することがより重要です。通常、次の構成を構成する必要があります。< /span>

  • http.csrf().disable(): クロスドメイン偽造攻撃の防止を無効にします。これは固定構成です。
  • http.cors(): Spring Security のフィルタ チェーンにCorsFilterを追加して、複雑な非同期リクエストの事前チェックを実装します。

さらに、パラメータ オブジェクトと対応するチェーン メソッドを呼び出してhttp、何らかの構成を実行する必要もあります。

  • authroizeRequests(): リクエストを認証します
  • antMatchers(): 特定のパスに一致します。このメソッドは、これらのパスがどのように処理されるかを決定しません。
  • permitAll(): 以前に構成されたパスのすべてのメソッドへの直接アクセスを許可しますantMatchers()
  • anyRequest(): 他のリクエスト (リクエスト パス) と一致します。つまり、以前に呼び出された antMatchers() 以外のすべてのリクエストです。このメソッドは、これらのリクエストの処理方法を決定するものではありません。
  • authenticated(): 認証済み

管理者の詳細

UserDetails インターフェイスの実装クラス、または User のサブクラスです。このクラスの主な機能は を実装することです。 =3> クラスが拡張されるのは、開発現場では通常、必要な認証情報にはユーザーの ID やその他の情報が含まれますが、これらの属性が Spring Security の で定義されていないため、開発に適合しないためです。が必要な場合は、展開されます。 UserUser

UserDetailsServiceImplloadUserByUsername() を書き込む場合、このメソッドは AdminDetails 型のオブジェクトを返す必要があります。

AuthenticationManagerauthenticate() メソッドを呼び出すと、返される結果のプリンシパルは AdminDetails オブジェクトになります。

JwtUtils

これは主に JWT の生成と JWT 解析のメソッドを定義しているため、JWT の生成と解析の詳細を気にすることなく他のコンポーネントで直接呼び出すことができます。

JwtAuthorizationFilter

これは JWT を処理するためのフィルターです。その主な機能は、クライアントのリクエスト ヘッダー内の有効な JWT を解析し、解析結果を認証情報にカプセル化し、その認証情報を Spring Security コンテキストに入力することです。

  • Spring Securityはコンテキストから認証情報のパーミッション部分を自動的に取り出してパーミッションを自動決定するため、コントローラでのリクエスト処理メソッドでは@PreAuthroize 注釈を使用して、承認されたアクセスを確認できます
  • コントローラでリクエストを処理するメソッドのパラメータ リストにAuthenticationパラメータを追加すると、を使用しなくてもコントローラで認証情報を取得できます。 a> Authentication パラメータではなく、カスタム パーティ タイプを使用し、@AuthenticationPrincipal 注釈を追加して、カスタマイズされたパーティ情報を直接取得します。

このフィルターの実装中は、次の点に注意する必要があります。

  • 明らかに無効な JWT (null、空の文字列など) は直接許可されるべきです。これは、ログイン、登録など、一部のリクエストで JWT データを送信する必要がないためです。
  • JWT の解析は失敗する可能性があり、特に JWT の有効期限が切れる可能性がある場合は、関連する例外を直接処理する必要があります。現在のコンポーネントはフィルターであり、他のすべてのコンポーネントの前に実行されます。したがって、例外をスローすることはできず、Spring MVC を使用して例外を均一に処理できます。 . メカニズム
  • JWT から関連データを解析した後、UsernamePasswordAuthenticationToken にカプセル化する必要があります。このうち、許可情報は authorities 属性にカプセル化する必要があります。このタイプのユーザーのログイン情報 (パーティ情報) は、このタイプの principal 属性にカプセル化する必要があります。さらに、他のシステム (他のプロジェクト) に権限関連の概念がない場合は、 here a>authorities を空にすることはできません。空にしないと、Spring Security によって「有効な認証情報がありません」と見なされます
  • 認証情報は必ず Spring Security コンテキストに保存してください
  • 以降の使用時に発生する可能性のある特定の問題 (たとえば、JWT を含む最初の訪問では最終的に情報がコンテキストに保存され、JWT を含まなくなった後続の訪問ではログインしているとみなされます) を回避するために、Spring をクリアする必要があります。フィルタが実行されたときの情報 セキュリティに関する情報

ログインプリンシパル

は主に、ID とユーザー名を UsernamePasswordAuthenticationToken に同時に格納するなど、パーティの複数の属性をカプセル化するために使用されます。コントローラでリクエストを行うと、 @AuthenticationPrincipal LoginPrincipal loginPrincipal パラメータを使用して、現在ログインしているパーティの情報を取得できます。

その他の関連クラスまたは実装: AdminMapper および関連

「ユーザー名に基づいて管理者情報を照会する」機能を実装する必要があり、返される結果にはこの管理者の権限リストが含まれている必要があります。

その他の関連クラスまたは実装: AdminServiceImpl

ログインの処理プロセスでは、AuthenticationManagerauthenticate() を呼び出して認証を実行し、戻り結果を取得する必要があります。その後、必要なデータが結果が返されます ビジネスメソッドの戻り値としてJWTを生成するために使用されます。

その他の関連クラスまたは実装: AdminController

ログインを処理するときは、サービス コンポーネントの呼び出し時に返された JWT データに応答する必要があります。

認証情報を取得する必要がある他のメソッドでは、@AuthenticationPrincipal LoginPrincipal loginPrincipal をパラメータ リストに追加して、現在ログインしているパーティの情報を取得します。

リクエストにアクセスする前に特定の権限が必要な場合は、@PreAuthorize アノテーションをメソッドに追加して権限を構成します。

課題 1: 認証されたアクセスをcsmall-serverに実装する

目標:csmall-serverプロジェクト内のすべての機能は、ログイン後にのみアクセスできます。

開発手順:

  • csmall-serverプロジェクトに関連する依存関係を追加します。

  • プロジェクトのスタートアップ クラスで除外する@SpringBootApplication アノテーションUserDetailsServiceAutoConfiguration

    • @SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
      
    • 一時的なuserユーザー名やランダムなパスワードは不要

  • 【手書き】JWTツールクラス

  • 创建LoginPrincipal

  • [手書き] JwtAuthenticationFilter を使用して JWT を取得および解析し、パーティ情報を Authentication にカプセル化し、Spring Security のコンテキストに保存します。

  • 创建SecurityConfigration

    • 必要http.cors()
    • 必要http.csrf().disable()
    • アクセスするにはすべてのリクエストにログインする必要があります

注: 元のクライアント リクエストはすべて JWT を運ぶ必要があります。

課題 2: csmall-server

次の機能が完了していることを確認してください。

  • ブランドを高める
  • ブランドリストの問い合わせ
  • IDに基づいてブランドを削除します
  • カテゴリを追加
  • クエリカテゴリリスト
  • IDに基づいてカテゴリを削除します
  • 属性の追加
  • クエリ属性リスト
  • IDに基づいて属性を削除します
  • フォトアルバムを追加
  • アルバムリストのクエリ
  • IDに基づいてアルバムを削除する
  • 属性テンプレートを追加
  • クエリ属性テンプレートのリスト
  • IDに基づいて属性テンプレートを削除します

上記の機能は、永続層、ビジネス ロジック層、コントローラー、インターフェイスを完成させる必要があります。

おすすめ

転載: blog.csdn.net/m0_71202849/article/details/126524955