IdentityServer 4実戦PHPクライアントを含む

17626 ワード

一、前言
実操はIdentityServer 4を使って単一のログインを完了し、ネット上で公式ドキュメントに従って作ったdemoではなく、この過程で踏んだ穴と解決方法を記録します.
二、理論
ネット上にはIdentityServer 4の理論がたくさんあり、公式サイトのドキュメントもありますが、ここでは説明しません.
三、認証サーバーの構成
1、証明書の構成、ids 4には証明書が必要で、テスト時にAddDeveloperSigningCredentialメソッドを使用することができ、自動的に生成されます.公開オンラインは自分でopensslを使って生成することが望ましい.ステップは以下の通りである:アドレスWin 64 OpenSSL v 1をダウンロードする.1.0 k、インストール後に設定、
d:\openssl>set RANDFILE=d:\openssl\.rnd
d:\openssl>set OPENSSL_CONF=d:\openssl\OpenSSL-Win64\bin\openssl.cfg

生成コマンドの実行
openssl req -newkey rsa:2048 -nodes -keyout cas.clientservice.key -x509 -days 365 -out cas.clientservice.cer

対応するディレクトリの下にcasが生成されます.clientservice.cerとcas.clientservice.key 2つのファイルのマージコマンドの実行
openssl pkcs12 -export -in cas.clientservice.cer -inkey cas.clientservice.key -out IS4.pfx

IS4.pfxは証明書名で、自分で修正することができて、途中でExport Passwordを入力するようにヒントを与えて、このpasswordはIS 4で使うことができて、メモする必要があります.
2、データ移行の新規作成Netcore mvcプロジェクトはids 4を導入します.普段はメモリにデータを保存する練習をしていますが、実際には生産環境でストレージを永続化しなければならないので、SQL Serverデータベースを使用しています.Nugetインストール
Install-Package IdentityServer4.EntityFramework

スタータープでcsのコンフィギュレーションサービス
  string connectionString = AppConfig.GetSection("ConnectionString").Value;
            var migrationsAssembly = typeof(AuditLog).GetTypeInfo().Assembly.GetName().Name;
            //    
            services.AddIdentityServer(opts =>
            {
                //opts.PublicOrigin = "http://www.baidu.com"; //    
                opts.UserInteraction = new UserInteractionOptions
                {
                    LoginUrl = "/Account/Login",
                    ErrorUrl = "Home/Error"
                };

            }).AddSigningCredential(new X509Certificate2("IS4.pfx", "xxxx")).AddConfigurationStore(options =>
             {
                 options.ConfigureDbContext = b =>
                     b.UseSqlServer(connectionString,
                         sql => sql.MigrationsAssembly(migrationsAssembly));
             })
                // this adds the operational data from DB (codes, tokens, consents)
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = b =>
                        b.UseSqlServer(connectionString,
                            sql => sql.MigrationsAssembly(migrationsAssembly));

                    // this enables automatic token cleanup. this is optional.
                    options.EnableTokenCleanup = true;
                })
                //.AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources())
                //.AddInMemoryApiResources(IdentityConfig.GetApiResources())
                //.AddInMemoryClients(IdentityConfig.GetClients())
                .AddResourceOwnerValidator()
                .AddProfileService();

データベース移行ファイルの追加
dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb

データベースを初期化このメソッドを1回実行すればよい.
private void InitializeDatabase(IApplicationBuilder app)
{
    using (var serviceScope = app.ApplicationServices.GetService().CreateScope())
    {
        serviceScope.ServiceProvider.GetRequiredService().Database.Migrate();

        var context = serviceScope.ServiceProvider.GetRequiredService();
        context.Database.Migrate();
        if (!context.Clients.Any())
        {
            foreach (var client in Config.GetClients())
            {
                context.Clients.Add(client.ToEntity());
            }
            context.SaveChanges();
        }

        if (!context.IdentityResources.Any())
        {
            foreach (var resource in Config.GetIdentityResources())
            {
                context.IdentityResources.Add(resource.ToEntity());
            }
            context.SaveChanges();
        }

        if (!context.ApiResources.Any())
        {
            foreach (var resource in Config.GetApis())
            {
                context.ApiResources.Add(resource.ToEntity());
            }
            context.SaveChanges();
        }
    }
}

対応するクライアント構成
 "SSO": {
    "Resources": [     
      {
        "Name": "Photo",
        "DisplayName": "    ",
        "Claims": "Photo"
      },
      {
        "Name": "Position",
        "DisplayName": "    ",
        "Claims": "Position"
      }
    ],
    "Apis": [
      {
        "Name": "Ids",
        "DisplayName": "    ",
        "Description": "    ",
        "Claims": "",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7"
      },
      {
        "Name": "IdsAPI",
        "DisplayName": "      ",
        "Description": "      ",
        "Claims": "",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7"
      },
      {
        "Name": "PClient",
        "DisplayName": "php   ",
        "Description": "php   ",
        "Claims": "",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7"
      },
      {
        "Name": "PClientAPI",
        "DisplayName": "php     ",
        "Description": "php     ",
        "Claims": "",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7"
      },
      {
        "Name": "NetCoreAPI",
        "DisplayName": "php     ",
        "Description": "php     ",
        "Claims": "",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7"
      }

    ],
    "Clients": [
      {
        "Id": "IdentityServer",
        "Name": "    ",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7",
        "GrantTypes": "hybrid",
        "Urls": "http://localhost:5002,https://localhost:5002",
        "ApiNames": "Ids,Photo,Position"
      },
      {
        "Id": "IdentityServerAPI",
        "Name": "    xxx",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7",
        "GrantTypes": "password",
        "Urls": "",
        "ApiNames": "IdsAPI"
      },
      {
        "Id": "PHPClient",
        "Name": "php   ",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7",
        "GrantTypes": "implicit",
        "Urls": "http://localhost:8090/callback",
        "ApiNames": "PClient,Photo,Position"
      },
      {
        "Id": "PHPAPIClient",
        "Name": "php   API",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7",
        "GrantTypes": "password",
        //"AccessTokenLifetime": 180, //       ,  1800
        "Urls": "",
        "ApiNames": "PClientAPI,Photo,Position"
      },
      {
        "Id": "NetCoreAPIClient",
        "Name": ".NetCore   API",
        "Secret": "8c31656b4acb74f15bf0d2378d8be3e7",
        "GrantTypes": "password",
        "AccessTokenLifetime": 36000,
        "Urls": "",
        "ApiNames": "NetCoreAPI,Photo,Position"
      }
    ]
  },

最後に、これは単一のプログラムに対して、一部のプロジェクトがクラスライブラリのデータ層を使用してデータ操作を行う場合、直接クラスライブラリに移行ファイルを追加するのは間違っています.解決策は、新しいプロジェクトで操作が完了した後、移行ファイルを対応するクラスライブラリにコピーすることですが、認証サーバのStartupのmigrationsAssemblyでは、対応する変更が必要です.
四、Netクライアント
1、MVCクライアント、2種類のモードを導入することができる:password好hybird、mvcのwebジャンプを保護するだけでなく、対応するAPIインタフェースも保護し、以下のように配置する.
//    
            //oidc   

            services.AddAuthentication("Bearer")
             .AddJwtBearer("Bearer", options =>
             {
                 options.Authority = "http://localhost:5000";
                 options.RequireHttpsMetadata = false;
                 options.Audience = "IdsAPI";
             });
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                //options.DefaultChallengeScheme = "oidc";             
                options.DefaultChallengeScheme = "Cookies"; //        

            }).AddCookie("Cookies", options =>
            {
                options.LoginPath = new PathString("/Account/Login");

            })
                .AddOpenIdConnect("oidc", options =>
                {


                    options.SignInScheme = "Cookies";
                    options.Authority = "http://localhost:5000";
                    options.RequireHttpsMetadata = false;
                    options.ClientId = "IdentityServer";
                    ////                              
                    //options.Events.OnRemoteFailure = context =>
                    //{
                    //    context.Response.Redirect("/Account/Refuse");
                    //    context.HandleResponse();
                    //    return Task.FromResult(0);
                    //};
                    options.ClientSecret = "8c31656b4acb74f15bf0d2378d8be3e7";
                    options.ResponseType = "code id_token";
                    options.SaveTokens = true;

                    options.GetClaimsFromUserInfoEndpoint = true;
                    options.Scope.Add("Photo");
                    options.Scope.Add("Position");
                    options.Scope.Add("Ids");
                    options.Scope.Add("offline_access");
                    options.ClaimActions.MapUniqueJsonKey("Id", "Id");
                    options.ClaimActions.MapUniqueJsonKey("TrueName", "TrueName");
                    options.ClaimActions.MapUniqueJsonKey("NickName", "NickName");
                    options.ClaimActions.MapUniqueJsonKey("UserName", "UserName");
                    options.ClaimActions.MapUniqueJsonKey("Position", "Position");
                    options.ClaimActions.MapUniqueJsonKey("Photo", "Photo");
                });


対応するAPIに[Authorize(AuthenticationSchemes=JwtBearerDefaults.AuthenticationScheme)]を追加するとpassword認証モードになります.
五、PHPクライアント
1、UIログインモードは、まず認証サーバにクライアントを配置し、認証モードはImplicitであり、urlsはphpに配置されたコールバックリンクである.上にjsonが展示されています.
2、phpプロジェクトを新設します.ここではlaravelフレームワークで紹介します.他のフレームワークは大きく異なります.composer create-project--prefer-dist laravel/laravel PhpHybirdClient php artisan key:generate 3、導入に必要なパッケージは、主に2つあります.1つはopenidで、1つはjwtです.composer require jumbojett/openid-connect-php composer require lcobucci/jwt
4、phpクライアントは構成登録を行い、ids 4認証サーバのクライアントに従って構成する
namespace App\Http\Controllers;
use App\User;
use Illuminate\Support\Facades\Auth;
use Jumbojett\OpenIDConnectClient;
use Lcobucci\JWT\Parser;
class AuthController extends Controller
{
    public function __construct()
    {
        $this->middleware('guest');
    }
    public function Login()
    {
         $oidc = new OpenIDConnectClient('http://localhost:5000',
            'PHPClient', '8c31656b4acb74f15bf0d2378d8be3e7');
        $oidc->setResponseTypes(array('id_token'));
        $oidc->setRedirectURL('http://localhost:8090/callback'); //    callbackurl
        $oidc->addScope(array('openid profile Photo Position'));
        $oidc->setAllowImplicitFlow(true);
        $oidc->addAuthParam(array('response_mode' => 'form_post'));
        $oidc->authenticate();
    }

    public function Callback()
    {
        $token = (new Parser())->parse((string)$_POST['id_token']); // Parses from a string
        $user = new User();
        $user->name = $token->getClaim('TrueName');
        $user->id = $token->getClaim('Id');
       //          
        $user = User::where('openid',$token->getClaim('Id'))->first();
        if (!$user)
            $user = new User();
        $user->openid = $token->getClaim('Id');
        $user->name = $token->getClaim('TrueName');
        $user->photo = $token->getClaim('Photo');
        $user->position = $token->getClaim('Position');
        $user->save();
        Auth::login($user);
        return redirect('/');
    }
}

ルーティングの追加
Route::get('/', 'HomeController@Index');
Route::get('login', 'AuthController@Login')->name('login');
Route::post('callback', 'AuthController@Callback')->name('callback');

注意点はコールバックリンク、post、laravelはcrsf検証があり、VerifyCsrfTokenファイルに例外を追加する必要があります.
 protected $except = [
        'callback'
    ];

ユーザーを保存するには、ユーザー・テーブルを構成する必要があります.
class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];


    protected $dateFormat = 'U';
}


5、php APIインタフェースの保護
APIインタフェース保護、ミドルウェア使用、インタフェース検証によるミドルウェア生成
php artisan make:middleware CheckToken

ミドルウェア検証ロジック
       $token = (string)$request->id_token;

        //    
        //$token = (new Parser())->parse((String)$token);
        //$signer = new Sha256();
        //$publicKey = new Key('file://F:\...\cas.clientservice.cer'); //       
        //if (!$token->verify($signer, $publicKey)) {
        //    return '    ';
        //}
        //if ($token->getClaim('exp') < time()) {
        //    return '    ';
        //}
        //if ($token->getClaim('client_id')!='EM') {
        //    return '   ID  ';
        //}
        //    
        $post_data = array(
            'client_id' => 'PClientAPI',  // api  id   ids4  client id
            'client_secret' => '8c31656b4acb74f15bf0d2378d8be3e7',
            'token' => $token
        );
        $postdata = http_build_query($post_data);
        $options = array(
            'http' => array(
                'method' => 'POST',
                'header' => 'Content-type:application/x-www-form-urlencoded',
                'content' => $postdata,
                'timeout' => 15 * 60 //     (  :s)
            )
        );
        $context = stream_context_create($options);
        $result = file_get_contents('http://localhost:5000/connect/introspect', false, $context);
        $request->Cliams = json_decode($result);
        if ($result && json_decode($result)->active)
            return $next($request);
        else
            return response()->json(['error' => 'Unauthenticated.'], 401);

指定されたルーティングにミドルウェアを割り当てるには、まずapp/Http/Kernelにミドルウェアを割り当てる必要があります.phpファイルでミドルウェアに割り当てられているキーです.デフォルトでは、クラスの$routeMiddlewareプロパティにはLaravelが持つミドルウェアが含まれています.独自のミドルウェアを追加するには、後で追加してキーを割り当てる必要があります.たとえば、次のようにします.
protected $routeMiddleware = [
    'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'token' => CheckToken::class
];

ミドルウェアがHTTP Kernelで定義されると、middlewareメソッドを使用してルーティングに割り当てることができます.
Route::get('/', function () {
    //
})->middleware('token');
       
public function __construct()
{
   $this->middleware('token');
}