IdentityServer4实战包含PHP客户端

一、前言

       实操使用IdentityServer4完成一个单点登录,不是网上千篇一律的按照官方文档弄的demo,在这记录下这个过程中踩的坑以及解决办法。

二、理论

       网上有很多IdentityServer4的理论,官网文档也有,这里就不赘述了。

三、认证服务器配置

1、配置证书,ids4需要一个证书,在测试的时候可以使用AddDeveloperSigningCredential 方法,他会自动生成。发布上线最好自己使用openssl生成,步骤如下:
下载地址Win64 OpenSSL v1.1.0k,安装后进行设置,

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两个文件
执行合并命令

openssl pkcs12 -export -in cas.clientservice.cer -inkey cas.clientservice.key -out IS4.pfx

IS4.pfx是证书名称,可以自己修改,中途会提示让你输入Export Password,这个password在IS4中会用到,需要记下来。

2、数据迁移
新建一个.net core mvc项目,引入ids4。平时我们练习都是把数据保存在内存中,实际在生产环境要持久化存储,所以我们使用SQL Server数据库。
Nuget安装

Install-Package IdentityServer4.EntityFramework

在Startup.cs的
ConfigureServices 进行配置

  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

初始化数据库 该方法执行一次即可。

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客户端,可以引入两种模式: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、引入必须的包,主要有两个一个是openid,一个是jwt。
composer require jumbojett/openid-connect-php
composer require lcobucci/jwt

4、php客户端进行配置登陆,按照ids4认证服务器中的客户端进行配置

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 文件中分配给该中间件一个 key,默认情况下,该类的 $routeMiddleware 属性包含了 Laravel 自带的中间件,要添加你自己的中间件,只需要将其追加到后面并为其分配一个 key,例如:

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

你可能感兴趣的:(IdentityServer4实战包含PHP客户端)