入门教程:创建简单的授权服务器,客户端和API

创建一个基础的授权服务器,客户端和API

通过这个练习,你将创建一个最小可用的IdentityServer做为OAuth2授权服务器。
我们从最基本的功能和配置项开始(完整代码可以从这里下载).
随着学习的深入,后面还有进一步的例程。当前这个练习包括:

  • 创建一个自托管的IdentityServer

  • 设置好客户端,使用服务账号和代理用户账号与认证服务通信

  • 注册一个API

  • 获取访问令牌

  • 调用 API

  • 验证访问令牌

搭建 IdentityServer

首先我们创建一个控制台程序,然后配置IdentityServer.
使用visual studio创建一个标准控制台程序,然后安装Nuget程序包

install-package identityserver3

登记API

所有需要访问令牌才能调用的APIs,可以归类到一系列作用域。这些作用域必须提前在IdentityServer中注册。我们创建一个叫做Scope的类来管理所有的作用域。

译者注:比如处理用户信息的可以成为用户作用域.订单相关的可以放在订单作用域下面.

using IdentityServer3.Core.Models;

static class Scopes
{
    public static List<Scope> Get()
    {
        return new List<Scope>
        {
            new Scope
            {
                Name = "api1"
            }
        };
    }
}

登记客户端

我们先注册一个单一客户端,这个客户端可以请求 api1作用域的令牌。第一个迭代我们我们只使用服务账号获取令牌(可以认为这是机器到机器的通信),在后面的迭代中,我们会加入用户概念。

对于客户端,我们需要配置:

  • 显示名和ID (ID不能重复)

  • 客户端密钥 (用于令牌服务认证当前客户端)

  • 处理方式(客户端凭据流动方向)

  • 引用令牌,引用令牌不需要签名证书

  • 访问api1作用域

using IdentityServer3.Core.Models;

static class Clients
{
    public static List<Client> Get()
    {
        return new List<Client>
        {
           // no human involved
            new Client
            {
                ClientName = "Silicon-only Client",
                ClientId = "silicon",
                Enabled = true,
                AccessTokenType = AccessTokenType.Reference,

                Flow = Flows.ClientCredentials,

                ClientSecrets = new List<Secret>
                {
                    new Secret("F621F470-9731-4A25-80EF-67A6F7C5F4B8".Sha256())
                },

                AllowedScopes = new List<string>
                {
                    "api1"
                }
            }
        };
    }
}

配置IdentityServer

IdentityServer 其实是一个OWIN的中间件. 所以我们在Startup类中用UseIdentityServer 扩展方法.
下面的方法使用前面的作用域和客户端配置好了一个基础的授权服务。我们同样配置了一个空的用户列表,后面我们会添加用户。

using Owin;
using System.Collections.Generic;
using IdentityServer3.Core.Configuration;
using IdentityServer3.Core.Services.InMemory;

namespace IdSrv
{
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var options = new IdentityServerOptions
            {
                Factory = new IdentityServerServiceFactory()
                            .UseInMemoryClients(Clients.Get())
                            .UseInMemoryScopes(Scopes.Get())
                            .UseInMemoryUsers(new List<InMemoryUser>()),
                            
                RequireSsl = false
            };

            app.UseIdentityServer(options);
        }
    }
}

添加日志功能

因为我们的托管程序是一个控制台程序,所以我们希望日志可以直接在控制台上输出。
Serilog 是一个完美的选择:

install-package serilog
install-package serilog.sinks.literate

托管 IdentityServer

最后一步是托管IdentityServer,所以我们在控制台程序中添加Katana自托管程序包。

install-package Microsoft.Owin.SelfHost

Program.cs中添加如下代码:

// logging
Log.Logger = new LoggerConfiguration()
    .WriteTo
    .LiterateConsole(outputTemplate: "{Timestamp:HH:mm} [{Level}] ({Name:l}){NewLine} {Message}{NewLine}{Exception}")
    .CreateLogger();

// hosting identityserver
using (WebApp.Start<Startup>("http://localhost:5000"))
{
    Console.WriteLine("server running...");
    Console.ReadLine();
}

这个控制台程序运行的时候,会在输出一系列信息,最后打印Server running...

添加API

现在我们来添加一个简单的web API,这个API必须获得上面IdentityServer的访问令牌才可以调用。

创建一个web应用

在解决方案中增加一个 ASP.NET Web Application 并选择 Empty 选项 (不引用任何framework).

安装必要的程序包:

install-package Microsoft.Owin.Host.SystemWeb
install-package Microsoft.AspNet.WebApi.Owin
install-package IdentityServer3.AccessTokenValidation

增加一个控制器

添加一个最简单的控制器:

[Route("test")]
public class TestController : ApiController
{
    public IHttpActionResult Get()
    {
        var caller = User as ClaimsPrincipal;

        return Json(new
        {
            message = "OK computer",
            client =  caller.FindFirst("client_id").Value
        });
    }
}

控制的User属性可以取到访问令牌中的声明信息。

添加Startup

Add the following Startup class for both setting up web api and configuring trust with IdentityServer
在web API项目中,增加Startup类,配置号webAPI,并信任上面建立的IdentityServer

using Microsoft.Owin;
using Owin;
using System.Web.Http;
using IdentityServer3.AccessTokenValidation;

[assembly: OwinStartup(typeof(Apis.Startup))]

namespace Apis
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // accept access tokens from identityserver and require a scope of 'api1'
            app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
                {
                    Authority = "http://localhost:5000",
                    ValidationMode = ValidationMode.ValidationEndpoint,

                    RequiredScopes = new[] { "api1" }
                });

            // configure web api
            var config = new HttpConfiguration();
            config.MapHttpAttributeRoutes();
            
            // require authentication for all controllers
            config.Filters.Add(new AuthorizeAttribute());

            app.UseWebApi(config);
        }
    }
}

用浏览器访问上面的API--因为没有访问令牌,浏览器会显示401错误。

增加控制台客户端

接下来我们增加一个控制台客户端,它向IdentityServer请求访问令牌,并用这个令牌访问API。
首先在解决方案里面增加一个控制台应用程序,然后装上OAuth2的nuget包:

install-package IdentityModel

首先用客户端凭据得到访问令牌:

using IdentityModel.Client;

static TokenResponse GetClientToken()
{
    var client = new TokenClient(
        "http://localhost:5000/connect/token",
        "silicon",
        "F621F470-9731-4A25-80EF-67A6F7C5F4B8");

    return client.RequestClientCredentialsAsync("api1").Result;
}

然后用这个令牌调用API

static void CallApi(TokenResponse response)
{
    var client = new HttpClient();
    client.SetBearerToken(response.AccessToken);

    Console.WriteLine(client.GetStringAsync("http://localhost:14869/test").Result);
}

在控制台客户端的Main函数里,依次调用上面两个方法,一切okay的话,控制台会显示{"message":"OK computer","client":"silicon"}

增加一个用户

目前为止,客户端使用服务账号得到访问令牌的,下面我们要引入用户--人!

添加一个用户服务

The user service manages users - for this sample we will use the simple in-memory user service.
First we need to define some users:
用户服务管理用户--为了减少不必要的复杂度, 这里我们用内存用户服务。
首先我们硬编码一些用户:

using IdentityServer3.Core.Services.InMemory;

static class Users
{
    public static List<InMemoryUser> Get()
    {
        return new List<InMemoryUser>
        {
            new InMemoryUser
            {
                Username = "bob",
                Password = "secret",
                Subject = "1"
            },
            new InMemoryUser
            {
                Username = "alice",
                Password = "secret",
                Subject = "2"
            }
        };
    }
}

UsernamePassword 用来认证一个用户,
Subject是一个标识符,在系统中唯一标识一个用, 它会被内嵌在访问令牌中。

Startup 中,用上面的Get方法替换空的用户列表。

添加一个新的客户端

接下来,我们添加一个客户端的定义,使用所谓的资源所有者密码凭据授权,这个流允许客户端发送用户名密码给令牌服务,然后得到一个访问令牌。

客户端Clients代码如下:

using IdentityServer3.Core.Models;
using System.Collections.Generic;

namespace IdSrv
{
    static class Clients
    {
        public static List<Client> Get()
        {
            return new List<Client>
            {
                // no human involved
                new Client
                {
                    ClientName = "Silicon-only Client",
                    ClientId = "silicon",
                    Enabled = true,
                    AccessTokenType = AccessTokenType.Reference,

                    Flow = Flows.ClientCredentials,

                    ClientSecrets = new List<Secret>
                    {
                        new Secret("F621F470-9731-4A25-80EF-67A6F7C5F4B8".Sha256())
                    },

                    AllowedScopes = new List<string>
                    {
                        "api1"
                    }
                },

                // human is involved
                new Client
                {
                    ClientName = "Silicon on behalf of Carbon Client",
                    ClientId = "carbon",
                    Enabled = true,
                    AccessTokenType = AccessTokenType.Reference,

                    Flow = Flows.ResourceOwner,

                    ClientSecrets = new List<Secret>
                    {
                        new Secret("21B5F798-BE55-42BC-8AA8-0025B903DC3B".Sha256())
                    },

                    AllowedScopes = new List<string>
                    {
                        "api1"
                    }
                }
            };
        }
    }
}

更新API

当具体的人引入权限系统的时候,访问令牌包括一个sub声明来唯一标识用户。

[Route("test")]
public class TestController : ApiController
{
    public IHttpActionResult Get()
    {
        var caller = User as ClaimsPrincipal;

        var subjectClaim = caller.FindFirst("sub");
        if (subjectClaim != null)
        {
            return Json(new
            {
                message = "OK user",
                client = caller.FindFirst("client_id").Value,
                subject = subjectClaim.Value
            });
        }
        else
        {
            return Json(new
            {
                message = "OK computer",
                client = caller.FindFirst("client_id").Value
            });
        }
    }
}

更新客户端

Next add a new method to the client to request an access token on behalf of a user:
接下来,我们在控制台客户端增加一个模拟用户名密码获取访问令牌的方法:

static TokenResponse GetUserToken()
{
    var client = new TokenClient(
        "http://localhost:5000/connect/token",
        "carbon",
        "21B5F798-BE55-42BC-8AA8-0025B903DC3B");

    return client.RequestResourceOwnerPasswordAsync("bob", "secret", "api1").Result;
}

在客户端Main代码中,调用用户名密码来获取令牌,然后查看获取的令牌信息及API响应信息。

下一步:

经过这个代码演练,你已经会编写简单的OAuth2服务啦,接下来,你将尝试:

  • 其它处理流程 - 如:隐式流,代码流和混合流。他们实现其它复杂场景(邦联认证,外部认证) 的基础。

  • 使用用户数据库 - 或者编写自己的用户服务或者使用ASP.net标识服务和成员服务。

  • 保存客户端和作用域配置到数据库中。我们有现成的EF支持。

  • 使用OpenID Connect 和标识符作用域来认证用户。

上面提到的 大部分知识点,会在下一个代码演练MVC walkthrough 提及.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容