DUICUO

マイクロサービスはシングルサインオン(SSO)認証サーバーを実装します

I. シングルサインオン(SSO)の概要

現在、各企業やプラットフォームは複数のシステムを運用しています。歴史的経緯により、これらのシステムは異なるベンダーから購入されているため、互いに独立しており、それぞれが独自のユーザー認証システムを備えています。ユーザーはログイン時に、各システムのユーザー名とパスワードを覚えておく必要があります。同時に、管理者は異なるシステムに同じユーザーに対して複数のログインアカウントを設定する必要があり、ユーザーにとって明らかに不便です。理想的なシナリオは、複数のシステムが存在する場合、ユーザーが一度ログインするだけですべてのシステムにアクセスできることです。1つのシステムからログアウトすると、すべてのシステムから自動的にログアウトされるため、繰り返し操作する必要がなくなります。これがシングルサインオン(SSO)システムによって実現される機能です。

シングルサインオンはシステム機能の定義です。現在、オープンソースで普及しているシングルサインオンの実装方法として、CASとOAuth2の2つがあります。以前はCASが最も多く使用されていましたが、現在ではSpring Cloudの普及に伴い、Spring Securityが提供するOAuth2認証・認可サーバーを使用してシングルサインオンを実装する人が増えています。

OAuth2は標準的な認可プロトコルであり、誰でもOAuth2認可サーバーを開発できます。Baidu Open PlatformやTencent Open Platformなど、ほとんどのオープンプラットフォームは現在、OAuth2プロトコルに基づいて実装されています。OAuth2.0では4つの認可タイプが定義されており、最新バージョンのOAuth2.1では7つの認可タイプが定義されています。これらのうち2つのタイプは、セキュリティ上の懸念から推奨されなくなりました。

【OAuth2.1推奨の5つの認可方式】

  • 承認コード:ユーザーが承認サーバーの URL 経由でクライアントにリダイレクトされた後、アプリケーションは URL から承認コードを取得し、その承認コードを使用してアクセス トークンを要求します。
  • PKCE [Proof Key for Code Exchange]: CSRF および認証コードインジェクション攻撃を防ぐために使用される認証コードタイプの拡張。
  • クライアント認証情報:クライアントは、ユーザー認証を必要とせずに、クライアントIDとクライアントキーを使用して認可サーバーに直接アクセストークンを要求します。これは通常、システム間の認可に使用されます。
  • デバイスコード:ブラウザを搭載していないデバイスや入力機能が制限されているデバイスで使用します。事前に取得したデバイスコードを使用してアクセストークンを取得します。
  • リフレッシュ トークン:アクセス トークンの有効期限が切れると、ユーザーの操作を必要とせずにリフレッシュ トークンを通じてアクセス トークンを取得できます。

【OAuth2.1で非推奨/禁止されている2つの認可種別】

  • 暗黙的フロー:暗黙的認可は、ネイティブアプリケーションやJavaScriptアプリケーションにおけるOAuthフローを簡素化するために、以前は推奨されていました。この認可フローでは、追加の認可コード交換ステップなしでアクセストークンが即座に返されます。しかし、この認可フローではHTTPリダイレクトを介してアクセストークンが直接返されるため、大きなリスクを伴うため推奨されません。一部の認可サーバーでは、このタイプの認可は直接禁止されています。
  • パスワードグラント:クライアントはユーザー名とパスワードを使用して認可サーバーからアクセストークンを取得します。クライアントはユーザー名とパスワードの両方を取得する必要があるため、この方法は推奨されません。最新のOAuth 2セキュリティのベストプラクティスでは、パスワードグラントは完全に禁止されています。

Spring Security の OAuth2 プロトコルのサポート:

Spring Securityの公式サイトによると、OAuth2の長期的なサポートと実際のビジネスシナリオの考慮に基づき、ほとんどのシステムでは認可サーバーは不要です。そのため、Springはspring-security-oauth2の使用を推奨していません。Spring Securityは、spring-security-oauth2からOAuth2のログイン、クライアント、リソースサーバーの機能を段階的に抽出し、Spring Securityに統合しました。また、認可サーバー機能を実装するために、別途spring-authorization-serverプロジェクトを作成しました。

現在、私たちが最もよく理解しているのは、Spring Security OAuth の OAuth2 プロトコルの実装とサポートです。Spring Security OAuth と Spring Security は別々のプロジェクトであるため、これらを区別することが重要です。以前は、OAuth2 関連の機能は Spring Security OAuth プロジェクト内で実装されていました。しかし、Spring Security 5.x 以降、Spring Security プロジェクトは Spring Security OAuth の機能を徐々に取り入れてきました。Spring Security 5.2 以降では、OAuth 2.0 のログイン、クライアント、リソースサーバー機能が追加されました。ただし、認可サーバー機能は Spring Security プロジェクトに統合されることを意図したものではなく、代わりに「spring-authorization-server」という別のプロジェクトが作成されました: [詳細は省略]。「spring-security」は OAuth2.1 プロトコルを実装し、「spring-security-oauth2」は OAuth2.0 プロトコルを実装します。

Springの将来計画では、Spring Security OAuthの現在の機能をすべてSpring Security 5.xに組み込む予定です。Spring SecurityがSpring Security OAuthと機能的に同等になった後も、少なくとも今後1年間はバグ修正とセキュリティアップデートを提供し続ける予定です。

[GitEggフレームワークシングルサインオン実装計画]:

spring-authorization-server の最新バージョンは 0.2.3 であり、一部の機能はまだ修正・改良中であるため、実運用環境への適用には不十分です。そのため、現在は認可サーバーとして spring-security-oauth2 を使用しており、spring-authorization-server の安定版がリリースされた後に移行・アップグレードする予定です。

[spring-security-oauth2 によって実装されるデフォルトの認可タイプ]:

  • 暗黙的フロー [このタイプは spring-authorization-server ではサポートされなくなりました]。
  • 承認コード。
  • パスワード付与 [このタイプは spring-authorization-server ではサポートされなくなりました]。
  • クライアント資格情報。
  • リフレッシュトークンの承認。

GitEggマイクロサービスフレームワークのgitegg-oauthにspring-security-oauth2が導入されました。このコードはOAuth2のパスワード認証とリフレッシュトークン認証を利用し、「SMS認証コード型」と「グラフィカル認証コード型」向けにカスタマイズされた拡張機能を備えています。これらは実際にはパスワード認証の拡張認証タイプです。

現在、ほぼすべてのSpring Cloudマイクロサービスは、トークンの取得にOAuth2パスワード認証を使用しています。最新のOAuth2プロトコルではパスワード認証が推奨されていない、あるいは禁止されているにもかかわらず、GitEggフレームワークのシステム管理インターフェースではなぜパスワード認証が依然として使用されているのか疑問に思うかもしれません。推奨されていない理由は、サードパーティのクライアントがユーザー名とパスワードを収集し、セキュリティリスクが生じる可能性があるためです。しかし、私たちの場合、クライアントはサードパーティのクライアントではなく、私たち自身のシステム管理インターフェースです。すべてのユーザー名とパスワードは私たち自身のシステムから取得されます。堅牢なシステムセキュリティ対策を実装することで、ユーザー名とパスワードが第三者に漏洩するリスクを最小限に抑えることができます。

spring-security-oauth2 を使用してシングル サインオンを実装する前に、まず SSO、OAuth2、spring-security-oauth2 の違いと関係を理解する必要があります。

  • シングルサインオン(SSO)とは、システムログインソリューションの定義です。社内システムへのログインだけでなく、QQ、WeChat、GitHubなどのインターネット上のサードパーティサービスへのログインも含まれます。
  • OAuth2は、様々な認証タイプを含むシステム認証プロトコルです。シングルサインオン機能を実装するには、認証コード認証とリフレッシュトークン認証という2種類の認証タイプを使用できます。
  • spring-security-oauth2 は、OAuth2 プロトコルの認証タイプの具体的な実装であり、シングル サインオン機能を実装するために実際に使用するコードでもあります。

II. Spring Securityシングルサインオンサーバーとクライアントの実装プロセス分析

シングル サインオン ビジネス プロセス シーケンス図:

システム A (シングル サインオン クライアント) が保護されたリソースに初めてアクセスしたときにトリガーされるシングル サインオン プロセスの説明。

1. ユーザーはブラウザを通じてシステム A の保護されたリソース リンクにアクセスします。

2. システム A は、現在のセッションがログインされているかどうかを判断します。ログインされていない場合は、システム A のログイン アドレス /login にリダイレクトします。

3. システムAが初めて/loginリクエストを受信すると、状態パラメータとコードパラメータは取得されません。このとき、システムAはシステムに設定されているシングルサインオンサーバーの認証URLを連結し、認証リンクにリダイレクトします。

4. シングル サインオン サーバーは、セッションがログインされているかどうかを判断します。ログインされていない場合は、シングル サインオン サーバーのログイン ページを返します。

5. ユーザーはログイン ページでユーザー名、パスワードなどの情報を入力して、ログイン操作を実行します。

6. シングル サインオン サーバーはユーザー名とパスワードを確認し、ログイン情報をコンテキスト セッションに設定します。

7. シングル サインオン サーバーはシステム A の /login リンクにリダイレクトし、リンクにコードと状態のパラメーターが含まれるようになります。

8. システムAは/loginリクエストを再度受信します。このリクエストには、状態パラメータとコードパラメータが含まれています。システムAは、OAuth2RestTemplateを介してシングルサインオンサーバーの/oauth/tokenインターフェースにリクエストを送信し、トークンを取得します。

9. トークンを取得した後、システムAはまずトークンを解析し、設定された公開鍵(非対称暗号化)を使用して検証します。検証に合格すると、トークンはコンテキストに設定され、次回のアクセス要求ではコンテキストから直接取得されます。

10. 前のセッションと次のセッションを処理した後、システム A はログイン前に要求された保護されたリソース リンクにリダイレクトします。

システムB(シングルサインオンクライアント)保護されたリソースへのアクセスプロセスの説明

1. ユーザーはブラウザを通じてシステム B の保護されたリソース リンクにアクセスします。

2. システム B は、現在のセッションがログインされているかどうかを判断します。ログインされていない場合は、システム B のログイン アドレス /login にリダイレクトします。

3. システムBが初めて/loginリクエストを受信すると、状態パラメータとコードパラメータは取得されません。このとき、システムBはシステムに設定されているシングルサインオンサーバーの認証URLを連結し、認証リンクにリダイレクトします。

4. シングル サインオン サーバーは、このセッションがログインされているかどうかを判断します。ユーザーは上記のシステム A にアクセスしたときにログインしているため、この時点ではログイン ページに戻りません。

5. シングル サインオン サーバーはシステム B の /login リンクにリダイレクトし、リンクにコードと状態のパラメーターが含まれるようになります。

6. システムBは/loginリクエストを再度受信します。このリクエストには、状態パラメータとコードパラメータが含まれます。システムBは、OAuth2RestTemplateを介してシングルサインオンサーバーの/oauth/tokenインターフェースにリクエストを送信し、トークンを取得します。

7. トークンを取得した後、システムBはまずトークンを解析し、設定された公開鍵(非対称暗号化)を使用して検証します。検証に合格すると、トークンはコンテキストに設定され、次回のアクセス要求ではコンテキストから直接取得されます。

8. 前のセッションと次のセッションを処理した後、システム B はログイン前に要求された保護されたリソース リンクにリダイレクトします。

Spring Security OAuth2 シングルサインオン実装プロセス:

1. ユーザーはブラウザを通じてシングル サインオンで保護されたリソース リンクにアクセスします。

2. Spring Securityはコンテキストに基づいてログイン状態を判定します(Spring Securityシングルサインオンサーバーとクライアントはデフォルトでセッションベースで動作します)。ログインしていない場合は、シングルサインオンクライアントのアドレス/loginにリダイレクトされます。

3. シングルサインオンクライアントのOAuth2ClientAuthenticationProcessingFilterインターセプターは、コンテキストを介してトークンを取得します。シングルサインオンクライアント/loginへの初回アクセス時には、codeパラメータとstateパラメータが利用できないため、UserRedirectRequiredException例外がスローされます。

4. シングルサインオンクライアントはUserRedirectRequiredException例外をキャッチし、設定ファイルの設定に基づいて、シングルサインオンサーバー上の認可リンク/oauth/authorizeにリダイレクトします。このリンクとリクエストには、関連する設定パラメータが含まれます。

5. シングルサインオンサーバーは認可リクエストを受信すると、セッション情報に基づいて現在のセッションがログイン済みかどうかを判断します。ログイン済みでない場合は、ユーザーをシングルサインオンサーバーの統合ログインインターフェースにリダイレクトします(シングルサインオンサーバーは、セッション情報に基づいてユーザーがログイン済みかどうかも判断します。ここで、マイクロサービスにおけるセッションクラスター共有の問題を解決するために、spring-session-data-redis が導入されています)。

6. ユーザーがログイン操作を完了すると、シングルサインオンサーバーはシングルサインオンクライアントの/loginリンクにリダイレクトします。このとき、リンクにはコードパラメータと状態パラメータが含まれます。

7. ステップ3のOAuth2ClientAuthenticationProcessingFilterインターセプターを再度使用して、コンテキストからトークンを取得します。この時点ではコンテキストにトークンが存在しないため、OAuth2RestTemplateはシングルサインオンサーバーの/oauth/tokenインターフェースにリクエストを送信し、リダイレクトによって取得したコードと状態をトークンと交換します。

8. トークンを取得した後、シングルサインオンクライアントはまずトークンを解析し、設定された公開鍵(非対称暗号化)を使用して検証します。検証に合格すると、トークンはコンテキストに設定され、次回のアクセス要求時にコンテキストから直接取得できるようになります。

9. シングル サインオン クライアントは、前のセッション要求と次のセッション要求の処理を完了すると、ログイン前に要求された保護されたリソース リンクにリダイレクトします。

III. [認可コード認証]と[リフレッシュトークン認証]を使用したシングルサインオンサーバーの実装

1. シングルサインオンサーバーページをカスタマイズする

gitegg-oauth を認可サーバーとして使用する場合、ログインページなどの情報をカスタマイズする必要があります。以下では、ログインページ、ホームページ、エラーメッセージページ、パスワード復旧ページをカスタマイズします。認可確認ページなど、その他の必要なページは必要に応じて定義できますが、今回のビジネスロジックでは二次的なユーザー確認は必要ないため、このページはカスタマイズしません。

gitegg-oauth プロジェクトの pom.xml ファイルに Thymeleaf の依存関係を追加します。Spring が公式に推奨するテンプレートエンジンである Thymeleaf を使用して、フロントエンドページのレンダリングと表示を実装します。

 < ! --Thymeleafテンプレートエンジンはシングルサインオンサーバーのページをレンダリングします-- >
<依存関係>
< グループ ID > org .springframework .boot </ グループ ID >
<artifactId> spring-boot-starter-thymeleaf </artifactId>
</依存関係>

GitEggOAuthController にページ リダイレクト パスを追加します。

 /**
シングルサインオン - ログインページ
@戻る
/
@GetMapping ( "/login" ) パブリック文字列ログイン() {
「ログイン」 を返します
}

/**
* シングルサインオン - ホームページ:シングルサインオンシステムに直接アクセスした後に表示されるページです。クライアントシステムからアクセスした場合は、クライアントページにリダイレクトされます。
@戻る
/
@GetMapping ( "/index" ) パブリック文字列インデックス() {
「インデックス」 を返します
}

/**
シングルサインオン - エラーページ
@戻る
/
@GetMapping ( "/error" ) パブリック文字列エラー() {
「エラー」 を返します
}

/**
シングルサインオン - パスワードを忘れた場合のページ
@戻る
/
@GetMapping ( "/find/pwd " ) パブリック文字列findPwd () {
"findpwd" を返します
}

resources ディレクトリの下に static (静的リソース) ディレクトリと templates (ページ コード) ディレクトリを作成し、favicon.ico ファイルを追加します。

カスタム ログイン ページ login.html コード:

 < !DOCTYPE html >
< html xmlns : th = "http://www.thymeleaf.org" >
<ヘッド>
< メタ文字セット= "UTF-8" >
< meta http-equiv = "X-UA-Compatible" content = "IE=edge,chrome=1" >
< meta name = "description" content = "統合ID認証プラットフォーム" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
統合ID認証プラットフォーム
< link rel = "ショートカットアイコン" th : href = "@{/gitegg-oauth/favicon.ico}" />
< link rel = "ブックマーク" th : href = "@{/gitegg-oauth/favicon.ico}" />
< リンクタイプ= "text/css" rel = "スタイルシート" th : href = "@{/gitegg-oauth/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css}" >
< リンクタイプ= "text/css" rel = "stylesheet" th : href = "@{/gitegg-oauth/assets/bootstrap-validator-0.5.3/css/bootstrapValidator.css}" >
< リンクタイプ= "text/css" rel = "スタイルシート" th : href = "@{/gitegg-oauth/assets/css/font-awesome.min.css}" >
< リンクタイプ= "text/css" rel = "stylesheet" th : href = "@{/gitegg-oauth/assets/css/login.css}" >
< ! -- [ IE の場合]>
< スクリプトタイプ= "text/javascript" th : src = "@{/gitegg-oauth/assets/js/html5shiv.min.js}" ></ スクリプト>
< ! [ endif ] -- >
</head>
<本文>
< div クラス= "htmleaf-container" >
< div クラス= "form-b​​g" >
< div クラス= "コンテナ" >
< div クラス= "row login_wrap" >
< div クラス= "login_left" >
< スパンクラス= "circle" >
< ! -- <span> </span>
<span> </span> -- >
< img th : src = "@{/gitegg-oauth/assets/images/logo.svg}" class = "logo" alt = "logo" >
</ スパン>
< スパンクラス= "スター" >
<span> </span>
<span> </span>
<span> </span>
<span> </span>
<span> </span>
<span> </span>
<span> </span>
<span> </span>
</ スパン>
< スパンクラス= "fly_star" >
<span> </span>
<span> </span>
</ スパン>
< p id = "タイトル" >
GitEgg クラウド統合ID認証プラットフォーム
</p>
</div>
< div クラス= "login_right" >
< div クラス= "title cf" >
< ul class = "title-list fr cf " >
< li class = "on" >アカウントとパスワードのログイン</li>
<li>確認コードログイン</li>
<p> </p>
</ul>
</div>
< div クラス= "ログインフォームコンテナ アカウントログイン" >
< フォームクラス= "form-horizo​​ntal account-form" th : アクション= "@{/gitegg-oauth/login}" メソッド= "post" >
< 入力タイプ= "hidden" クラス= "form-control" 名前= "client_id" = "gitegg-admin" >
< 入力ID = "user_type" タイプ= "hidden" クラス= "form-control" 名前= "type" = "user" >
< 入力ID = "user_mobileType" タイプ= "hidden" クラス= "form-control" 名前= "mobile" = "0" >
< div クラス= "入力ラッパー 入力アカウントラッパー フォームグループ" >
< div クラス= "入力アイコンラッパー" >
< i クラス= "入力アイコン" >
< svg t = "1646301169630" class = "icon" viewBox = "64 64 896 896" version = "1.1" xmlns = "http://www.w3.org/2000/svg" p-id = "8796" width = "1.2em" height = "1.2em" fill = "currentColor" >< path d = "M858.5 763.6c-18.9-44.8-46.1-85-80.6-119.5-34.5-34.5-74.7-61.6-119.5-80.6-0.4-0.2-0.8-0.3-1.2-0.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-0.4 0.2-0.8 0.3-1.2 0.5-44.8 18.9-85 46-119.5 80.6-34.5 34.5-61.6 74.7-80.6 119.5C146.9 807.5 137 854 136 901.8c-0.1 4.5 3.5 8.2 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c0.1 4.4 3.6 7.8 8 7.8h60c4.5 0 8.1-3.7 8-8.2-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z" p-id = "8797" ></ パス></ svg >
</ i >
</div>
< input type = "text" class = "input" name = "username" placeholder = "アカウントを入力してください" >
</div>
< div クラス= "入力ラッパー input-psw-wrapper フォームグループ" >
< div クラス= "入力アイコンラッパー" >
< i クラス= "入力アイコン" >
< svg t = "1646302713220" class = "icon" viewBox = "64 64 896 896" version = "1.1" xmlns = "http://www.w3.org/2000/svg" p-id = "8931" width = "1.2em" height = "1.2em" fill = "currentColor" >< path d = "M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240z m460 600H232V536h560v304z" p-id = "8932" ></ path >< path d = "M484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53c12.1-8.7 20-22.9 20-39 0-26.5-21.5-48-48-48s-48 21.5-48 48c0 16.1 7.9 30.3 20 39z" p-id = "8933" ></ パス></ svg >
</ i >
</div>
< input id = "password" type = "password" class = "input" name = "password" placeholder = "パスワードを入力してください" >
</div>
< div id = "account-err" クラス= "err-msg" スタイル= "width: 100%; text-align: center;" ></ div >
< button type = "submit" class = "login-btn" id = "loginSubmit" >今すぐログイン</button>
< div class = "forget" id = "forget" >パスワードを忘れましたか? </div>
</フォーム>
</div>
< div クラス= "ログインフォームコンテナ モバイルログイン" スタイル= "表示: なし;" >
< フォームクラス= "form-horizo​​ntal mobile-form" th : アクション= "@{/gitegg-oauth/phoneLogin}" メソッド= "post" >
< 入力ID = "tenantId" タイプ= "hidden" クラス= "form-control" 名前= "tenant_id" = "0" >
< 入力ID = "type" タイプ= "hidden" クラス= "form-control" 名前= "type" = "phone" >
< 入力ID = "mobileType" タイプ= "hidden" クラス= "form-control" 名前= "mobile" = "0" >
< 入力ID = "smsId" タイプ= "hidden" クラス= "form-control" 名前= "smsId" >
< div クラス= "入力ラッパー 入力アカウントラッパー フォームグループ 入力電話ラッパー" >
< div クラス= "入力アイコンラッパー" >
< i クラス= "入力アイコン" >
< svg t = "1646302822533" class = "icon" viewBox = "0 0 1024 1024" version = "1.1" xmlns = "http://www.w3.org/2000/svg" p-id = "9067" width = "1.2em" height = "1.2em" fill = "currentColor" >< path d = "M744 62H280c-35.3 0-64 28.7-64 64v768c0 35.3 28.7 64 64 64h464c35.3 0 64-28.7 64-64V126c0-35.3-28.7-64-64-64z m-8 824H288V134h448v752z" p-id = "9068" ></ パス>< パスd = "M512 784m-40 0a40 40 0 ​​1 0 80 0 40 40 0 ​​1 0-80 0Z" p-id = "9069" ></ パス></ svg >
</ i >
</div>
< input id = "phone" type = "text" class = "input" name = "phone" maxlength = "11" placeholder = "電話番号を入力してください" >
</div>
< div クラス= "code-form form-group sms-code-wrapper" >
< div クラス= "入力ラッパー 入力SMSラッパー" >
< div クラス= "入力アイコンラッパー" >
< i クラス= "入力アイコン" >
< svg t = "1646302879723" class = "icon" viewBox = "0 0 1024 1024" version = "1.1" xmlns = "http://www.w3.org/2000/svg" p-id = "9203" width = "1.2em" height = "1.2em" fill = "currentColor" >< path d = "M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5z" p-id = "9204" ></ path >< path d = "M833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6c20.2 15.7 48.5 15.7 68.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z" p-id = "9205" ></ パス></ svg >
</ i >
</div>
< input id = "code" type = "text" class = "input-code" name = "code" maxlength = "6" placeholder = "確認コードを入力してください" >
</div>
< div クラス= "入力コードラッパー" >
<a id="sendBtn" href="javascript:sendCode();"> 確認コードを取得</a>
</div>
</div>
< div id = "mobile-err" クラス= "err-msg" スタイル= "width: 100%; text-align: center;" ></ div >
< button type = "submit" class = "login-btn" id = "loginSubmitByCode" >今すぐログイン</button>
</フォーム>
</div>
</div>
</div>
</div>
</div>
< div クラス= "related" >
著作権© 2021 GitEgg All Rights Reserved .
</div>
</div>
< スクリプトタイプ= "text/javascript" th : src = "@{/gitegg-oauth/assets/js/jquery-2.1.4.min.js}" ></ スクリプト>
< スクリプトタイプ= "text/javascript" th : src = "@{/gitegg-oauth/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js}" ></ スクリプト>
< スクリプトタイプ= "text/javascript" th : src = "@{/gitegg-oauth/assets/bootstrap-validator-0.5.3/js/bootstrapValidator.js}" ></ スクリプト>
< スクリプトタイプ= "text/javascript" th : src = "@{/gitegg-oauth/assets/js/md5.js}" ></ スクリプト>
< スクリプトタイプ= "text/javascript" th : src = "@{/gitegg-oauth/assets/js/jquery.form.js}" ></ スクリプト>
< スクリプトタイプ= "text/javascript" th : src = "@{/gitegg-oauth/assets/js/login.js}" ></ スクリプト>
</本文>
</html>

カスタム login.js コード:

 var カウントダウン= 60 ;
jQuery ( 関数($) {
カウントダウン= 60 ;
$( '.account-form' ) .bootstrapValidator ({
メッセージ: 「入力内容が正しくありません」
フィードバックアイコン: {
有効: 'glyphicon glyphicon-ok'
無効: 'glyphicon glyphicon-remove'
検証中: 'glyphicon glyphicon-refresh'
},
フィールド: {
ユーザー名: {
コンテナ: '.input-account-wrapper'
メッセージ: 「入力内容が正しくありません」
バリデータ: {
空ではありません: {
メッセージ: 「ユーザーアカウントは空にできません」
},
文字列の長さ: {
: 2
最大: 32
メッセージ: 「アカウントの長さの範囲: 2〜32 文字。」
},
正規表現: {
正規表現: /^[ a-zA-Z0-9_\ .]+$/,
メッセージ: 「ユーザー名には文字、数字、ピリオド、アンダースコアのみ使用できます」
}
}
},
パスワード: {
コンテナ: '.input-psw-wrapper'
バリデータ: {
空ではありません: {
メッセージ: 「パスワードは空欄にできません」
},
文字列の長さ: {
: 5
最大: 32
メッセージ: 「パスワードの長さの範囲: 6〜32 文字。」
}
}
}
}
});
$( '.mobile-form' ) .bootstrapValidator ({
メッセージ: 「入力内容が正しくありません」
フィードバックアイコン: {
有効: 'glyphicon glyphicon-ok'
無効: 'glyphicon glyphicon-remove'
検証中: 'glyphicon glyphicon-refresh'
},
フィールド: {
電話: {
メッセージ: 「入力内容が正しくありません」
コンテナ: '.input-phone-wrapper'
バリデータ: {
空ではありません: {
メッセージ: 「電話番号は空欄にできません」
},
正規表現: {
正規表現: /^ 1 \d { 10 }$/,
メッセージ: 「電話番号の形式が正しくありません」
}
}
},
コード: {
コンテナ: '.input-sms-wrapper'
バリデータ: {
空ではありません: {
メッセージ: 「確認コードは空欄にできません」
},
文字列の長さ: {
: 6
最大: 6
メッセージ: 「確認コードは 6 文字です。」
}
}
}
}
});

var オプション={
beforeSerialize : beforeFormSerialize
success : formSuccess , // 送信成功後に実行されるコールバック関数
error : formError 、// 送信失敗後に実行されるコールバック関数。
ヘッダー: { "TenantId" : 0 },
clearForm : true , // 送信が成功した後にフォーム内のフィールド値をクリアするかどうか
restForm : true , // 送信が成功した後にフォーム内のフィールド値をリセットするかどうか、つまり、ページが読み込まれたときにフォームを初期状態に戻すかどうか。
timeout : 6000 // リクエストのタイムアウト時間を設定します。タイムアウト時間(ミリ秒単位)が経過すると、リクエストは自動的に終了します。
}
var mobileOptions ={
success : mobileFormSuccess , // 送信成功後に実行されるコールバック関数
error : mobileFormError 、// 送信失敗後に実行されるコールバック関数。
ヘッダー: { "TenantId" : 0 },
clearForm : true , // 送信が成功した後にフォーム内のフィールド値をクリアするかどうか
restForm : true , // 送信が成功した後にフォーム内のフィールド値をリセットするかどうか、つまり、ページが読み込まれたときにフォームを初期状態に戻すかどうか。
timeout : 6000 // リクエストのタイムアウト時間を設定します。タイムアウト時間(ミリ秒単位)が経過すると、リクエストは自動的に終了します。
}
beforeFormSerialize 関数(){
$( "#account-err" ) .html ( "" );
$( "#username" ) .val ( $.trim ($( "#username" ) .val ()));
$( "#password" ) .val ( $.md5 ( $.trim ($( "#password" ) .val ())));
}
関数formSuccess ( レスポンス){
$( ".account-form" ) .data ( 'bootstrapValidator' ) .resetForm ();
if ( レスポンス.成功)
{
window .location .href = response .targetUrl ;
}
それ以外
{
$( "#account-err" ) .html ( 応答.message );
}
}
関数formError ( レスポンス){
$( "#account-err" ) .html ( 応答);
}
関数mobileFormSuccess ( レスポンス){
$( ".mobile-form" ) .data ( 'bootstrapValidator' ) .resetForm ();
if ( レスポンス.成功)
{
window .location .href = response .targetUrl ;
}
それ以外
{
$( "#mobile-err" ) .html ( 応答.message );
}
}
関数mobileFormError ( レスポンス){
$( "#mobile-err" ) .html ( レスポンス);
}
$( ".account-form" ) .ajaxForm ( オプション);

$( ".mobile-form" ) .ajaxForm ( mobileOptions );

$( ".nav-left a" ) .click ( 関数( e ){
$( ".account-login" ) .show ();
$( ".mobile-login" ) .hide ();
});
$( ".nav-right a" ) .click ( 関数( e ){
$( ".account-login" ) .hide ();
$( ".mobile-login" ) .show ();
});

$( "#forget" ) .click ( 関数( e ){
window .location .href = "/find/pwd" ;
});

$( '.title-list li' ) .click ( 関数(){
var liindex = $( '.title-list li' ) .index ( this );
$( this ) .addClass ( 'on' ) .siblings ( ) .removeClass ( 'on' );
$( '.login_right div.login-form-container' ) .eq ( liindex ) .fadeIn ( 150 ) .siblings ( 'div.login-form-container' ) .hide ();
var liWidth = $( '.title-list li' ) .width ();

if ( liindex == 0 )
{
$( '.login_right .title-list p' ) .css ( "transform" , "translate3d(0px, 0px, 0px)" );
}
それ以外{
$( '.login_right .title-list p' ) .css ( "transform""translate3d(" + liWidth + "px, 0px, 0px)" );
}

});

});
関数sendCode (){
$( ".mobile-form" ) .data ( 'bootstrapValidator' ) .validateField ( 'phone' );
if ( ! $( ".mobile-form" ) .data ( 'bootstrapValidator' ) .isValidField ( "phone" ))
{
戻る;
}

if ( カウントダウン! = 60 )
{
戻る;
}
送信メッセージ();
varphone = $.trim ($( "#phone" ) .val ( ));
var tenantId = $( "#tenantId" ) .val ();
$.ajax ({
//リクエストメソッド
タイプ: "POST"
// 要求されたメディアタイプ
コンテンツタイプ: "application/x-www-form-urlencoded;charset=UTF-8"
データ型: 'json'
//リクエストアドレス
URL : "/code/sms/login"
//データ、 JSON文字列
データ: {
テナントID : テナントID
電話番号: 電話番号
コード: "aliValidateLogin"
},
// リクエスト成功
成功: 関数( 結果) {
$( "#smsId" ) .val ( 結果.data );
},
// リクエストが失敗しました。詳細なエラーメッセージが含まれます。
エラー: 関数( e ){
コンソールログ( e );
}
});
};
関数sendmsg (){
if ( カウントダウン== 0 ){
$( "#sendBtn" ) .css ( "color" , "#181818" );
$( "#sendBtn" ) .html ( "確認コードを取得" );
カウントダウン= 60 ;
false を返します
}
それ以外{
$( "#sendBtn" ) .css ( "color" , "#74777b" );
$( "#sendBtn" ) .html ( "Resend(" + countdown + ")" );
カウントダウン--;
}
setTimeout ( 関数(){
送信メッセージ();
}, 1000 );
}

2. 認可サーバーの設定

Web セキュリティ構成 WebSecurityConfig を変更して、許可なしでアクセスできる静的ファイルを追加します。

 @オーバーライド
public void configure ( WebSecurity web ) 例外をスローします{
web.ignoring () .antMatchers ( "/assets/**""/css/**""/images/**" );
}

Nacos 設定を変更し、新しいページアクセスパスをアクセスホワイトリストに追加します。これにより、ResourceServerConfig のリソースサーバー設定に認証なしでアクセスできるようになります。また、tokenUrls 設定を追加します。この設定はゲートウェイでの認証は行いませんが、OAuth2 ベーシック認証を必要とします。この認証は認可コードモードで必須です。

 #以下の構成は新しく追加されたものです。
ホワイトURL :
- "/gitegg-oauth/oauth/ログイン"
- "/gitegg-oauth/oauth/find/pwd"
- "/gitegg-oauth/oauth/エラー"
認証URL :
- 「/gitegg-oauth/oauth/index」
ホワイトURL :
- "/*/v2/api-docs
- "/gitegg-oauth/oauth/public_key"
- "/gitegg-oauth/oauth/token_key"
- "/gitegg-oauth/find/pwd
- "/gitegg-oauth/code/sms/ログイン"
- "/gitegg-oauth/change/password"
- "/gitegg-oauth/エラー
- 「/gitegg-oauth/oauth/sms/captcha/send」
# OAuth2認証インターフェースを追加しました。ゲートウェイはこれを許可し、認証は認証センターによって実行されます。
トークンURL :
- "/gitegg-oauth/oauth/token"

GitEggフレームワークはユーザー名とパスワードを使用し、保存されたパスワードを暗号化するため、これを処理するにはカスタムログインフィルターが必要です。同様に、携帯電話認証コードログインやQRコードログインなどの機能を追加することもできます。

 パッケージcom .gitegg .oauth .filter ;
cn .hutool .core .bean .BeanUtil をインポートします
com .gitegg .oauth .token .PhoneAuthenticationToken をインポートします
com .gitegg .platform .base .constant .AuthConstant をインポートします
com .gitegg .platform .base .domain .GitEggUser をインポートします
com .gitegg .platform .base .result .Result をインポートします
com .gitegg .service .system .client .feign .IUserFeign をインポートします
org.springframework.beans.factory.annotation.Autowired インポートます
org.springframework.security.authentication.AbstractAuthenticationToken インポートます
import org .springframework .security .authentication .AuthenticationServiceException ;
import org .springframework .security .authentication .UsernamePasswordAuthenticationToken ;
import org .springframework .security .core .Authentication ;
import org .springframework .security .core .AuthenticationException ;
import org .springframework .security .web .authentication .UsernamePasswordAuthenticationFilter ;
import org .springframework .util .StringUtils ;
import javax .servlet .http .HttpServletRequest ;
import javax .servlet .http .HttpServletResponse ;

/**
* 自定义登陆
* @author GitEgg
/
public class GitEggLoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_RESTFUL_TYPE_PHONE = "phone" ;
public static final String SPRING_SECURITY_RESTFUL_TYPE_QR = "qr" ;
public static final String SPRING_SECURITY_RESTFUL_TYPE_DEFAULT = "user" ;
// 登陆类型: user :用户密码登陆; phone :手机验证码登陆; qr :二维码扫码登陆
private static final String SPRING_SECURITY_RESTFUL_TYPE_KEY = "type" ;
// 登陆终端: 1 :移动端登陆,包括微信公众号、小程序等; 0PC后台登陆
private static final String SPRING_SECURITY_RESTFUL_MOBILE_KEY = "mobile" ;
private static final String SPRING_SECURITY_RESTFUL_USERNAME_KEY = "username" ;
private static final String SPRING_SECURITY_RESTFUL_PASSWORD_KEY = "password" ;
private static final String SPRING_SECURITY_RESTFUL_PHONE_KEY = "phone" ;
private static final String SPRING_SECURITY_RESTFUL_VERIFY_CODE_KEY = "code" ;
private static final String SPRING_SECURITY_RESTFUL_QR_CODE_KEY = "qrCode" ;
@オートワイヤード
private IUserFeign userFeign ;
private boolean postOnly = true ;
@オーバーライド
public Authentication attemptAuthentication ( HttpServletRequest request , HttpServletResponse response ) throws AuthenticationException {

if ( postOnly && ! "POST" .equals ( request.getMethod ())) {
throw new AuthenticationServiceException (
"Authentication method not supported: " + request.getMethod ());
}
String type = obtainParameter ( request , SPRING_SECURITY_RESTFUL_TYPE_KEY );
String mobile = obtainParameter ( request , SPRING_SECURITY_RESTFUL_MOBILE_KEY );
AbstractAuthenticationToken authRequest ;
String principal ;
String credentials ;
// 手机验证码登陆
if ( SPRING_SECURITY_RESTFUL_TYPE_PHONE.equals ( type )){
principal = obtainParameter ( request , SPRING_SECURITY_RESTFUL_PHONE_KEY );
credentials = obtainParameter ( request , SPRING_SECURITY_RESTFUL_VERIFY_CODE_KEY );

principal = principal.trim ();
authRequest = new PhoneAuthenticationToken ( principal , credentials );
}
// 账号密码登陆
それ以外{
principal = obtainParameter ( request , SPRING_SECURITY_RESTFUL_USERNAME_KEY );
credentials = obtainParameter ( request , SPRING_SECURITY_RESTFUL_PASSWORD_KEY );

Result < Object > result = userFeign.queryUserByAccount ( principal );
if ( null ! = result && result.isSuccess ()) {
GitEggUser gitEggUser = new GitEggUser ();
BeanUtil.copyProperties ( result.getData (), gitEggUser , false );
if ( !StringUtils .isEmpty ( gitEggUser.getAccount ())) {
principal = gitEggUser.getAccount ();
credentials = AuthConstant .BCRYPT + gitEggUser.getAccount () + credentials ;
}
}
authRequest = new UsernamePasswordAuthenticationToken ( principal , credentials );
}

// Allow subclasses to set the "details" property
setDetails ( request , authRequest );
return this.getAuthenticationManager () .authenticate ( authRequest );
}
private void setDetails ( HttpServletRequest request ,
AbstractAuthenticationToken authRequest ) {
authRequest.setDetails ( authenticationDetailsSource.buildDetails ( request ));
}
private String obtainParameter ( HttpServletRequest request , String parameter ) {
String result = request.getParameter ( parameter );
return result == null ? "" : result ;
}
}

四、实现单点登录客户端

spring-security-oauth2提供OAuth2授权服务器的同时也提供了单点登录客户端的实现,通用通过几行注解即可实现单点登录功能。

1、新建单点登录客户端工程,引入oauth2客户端相关jar包。

 < dependency >
< groupId > org .springframework .boot </ groupId >
< artifactId > spring-boot-starter-oauth2-client </ artifactId >
</ dependency >
< dependency >
< groupId > org .springframework .boot </ groupId >
< artifactId > spring-boot-starter-security </ artifactId >
</ dependency >
< dependency >
< groupId > org .springframework .security .oauth .boot </ groupId >
< artifactId > spring-security-oauth2-autoconfigure </ artifactId >
</ dependency >

2、新建WebSecurityConfig类,添加@EnableOAuth2Sso注解。

 @EnableOAuth2Sso
@構成
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@オーバーライド
protected void configure ( HttpSecurity http ) throws Exception {
http.authorizeRequests ()
.anyRequest () .authenticated ()
.and ()
.csrf () .disable ();
}
}

3、配置单点登录服务端相关信息。

 サーバー:
ポート: 8080
servlet :
context-path : / ssoclient1
security :
oauth2 :
クライアント
# 配置在授权服务器配置的客户端idsecret
client-id : ssoclient
client-secret : 123456
# 获取tokenurl
access-token-uri : http :// 127.0.0.1 / gitegg-oauth / oauth / token
# 授权服务器的授权地址
user-authorization-uri : http :// 127.0.0.1 / gitegg-oauth / oauth / authorize
resource :
jwt :
# 获取公钥的地址,验证token需使用,系统启动时会初始化,不会每次验证都请求
key-uri : http :// 127.0.0.1 / gitegg-oauth / oauth / token_key

述べる:

1、GitEgg框架中自定义了token返回格式,SpringSecurity获取token的/oauth/token默认返回的是ResponseEntity,自有系统登录和单点登录时需要做转换处理。

2、Gateway网关鉴权需要的公钥地址是gitegg-oauth/oauth/public_key,单点登录客户端需要公钥地址/oauth/token_key,两者返回的格式不一样,需注意区分。

3、请求/oauth/tonen和/oauth/token_key时,默认都需要使用Basic认证,也就是请求时需添加client_id和client_security参数。

源码地址:

GitEgg: GitEgg 是一款开源免费的企业级微服务应用开发框架,旨在整合目前主流稳定的开源技术框架,集成常用的最佳项目解决方案,实现可直接使用的微服务快速开发框架。

GitHub - wmz1930/GitEgg: GitEgg 是一款开源免费的企业级微服务应用开发框架,旨在整合目前主流稳定的开源技术框架,集成常用的最佳项目解决方案,实现可直接使用的微服务快速开发框架。