IdentityServer4 學習筆記[2]-用戶名密碼驗證


回顧

上一篇介紹了IdentityServer4客戶端授權的方式,今天來看看IdentityServer4的基於密碼驗證的方式,與客戶端驗證相比,主要是配置文件調整一下,讓我們來看一下

配置修改

public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    SubjectId = "1",
                    Username = "alice",
                    Password = "password"
                },
                new TestUser
                {
                    SubjectId = "2",
                    Username = "bob",
                    Password = "password"
                }
            };
        }

        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new IdentityResource[]
            {
                new IdentityResources.OpenId()
            };
        }

        public static IEnumerable<ApiResource> GetApis()
        {
            return new List<ApiResource>
            {
                new ApiResource("api1", "My API")
            };
        }

        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "client",

                    // no interactive user, use the clientid/secret for authentication
                    AllowedGrantTypes = GrantTypes.ClientCredentials,

                    // secret for authentication
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },

                    // scopes that client has access to
                    AllowedScopes = { "api1" }
                },
                // resource owner password grant client
                new Client
                {
                    ClientId = "ro.client",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes = { "api1" }
                }
            };
        }
    }

通過上面的代碼,與客戶端授權方式相比,多了兩個東西,一個是GetClients()方法中增加了一個Client,授權方式為資源擁有者密碼的模式,另一個是增加了一個方法GetUsers(),真實場景中TestUser一般使用Asp.NetCore.Identity的用戶,這里暫時使用TestUser來測試,IdentityServer4不是用戶管理系統,它是授權框架(發放令牌的)

注冊用戶

在Startup中,把TestUser也添加上

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServer()
       .AddDeveloperSigningCredential()
       .AddInMemoryClients(Config.GetClients())             
       .AddInMemoryApiResources(Config.GetApis())
       .AddInMemoryIdentityResources(Config.GetIdentityResources())
       .AddTestUsers(Config.GetUsers());
}

新建測試Api項目


可以選擇刪除IIS的設置,與前一篇文章的操作一致,並修改端口號為5001(Api資源服務地址)
並新增一個控制器IdentityController,繼承自ControllerBase

[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
    public IActionResult Get()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
}

注意到IdentityController控制器類上有個特性[Authorize],這個代表這個控制器需要驗證OK后才能訪問,如果沒有[Authorize]就說明訪問不需要授權,在Get方法中這么寫是為了獲取到用戶的身份信息,也就是Bearer Token中所包含的用戶信息,當然也可以從Cookie中獲取,我們使用Bearer Token的方式,以便照顧有移動客戶端的場景

配置Api項目

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvcCore()
            .AddAuthorization()
            .AddJsonFormatters();

            ///這里使用5000端口的授權服務端來驗證
            services.AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
            {
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;
                options.Audience = "api1";
            });
        }

        public void Configure(IApplicationBuilder app)
        {
            ///這句別忘了,啟用驗證
            app.UseAuthentication();
            app.UseMvc();
        }
    }

啟動

解決方案右鍵選擇屬性菜單並打開,設置啟動方式,選擇多個啟動項目,啟動,讓兩個服務都運行起來

驗證測試

打開Postman,填入參數,提交,獲取Token

拿到Token后,到5001地址去執行http://localhost:5001/identity,選擇Bearer Token,把剛才獲取到的Token填入,並點擊紅色按鈕

點擊Send 按鈕后,Api執行成功

如果不填寫Token,或者故意將Token填錯將返回401,未授權錯誤

打開網址(https://jwt.io/),把Token復制進去,解析一下看看,與客戶端授權方式相比,多了一個Sub

進一步思考

IdentityServer4應該有可以獲取到用戶信息的端口,我們從之前的發現端點里也能猜到一些,那我們拿着剛剛獲取到的Token去這個端點獲取下試試看

那我們就用Postman測試下,填寫http://localhost:5000/connect/userinfo,使用Get

從上圖可以看出,報403了,被拒絕訪問了,可能哪里出了問題,經過一番搜索,授權服務端Config里GetClients()方法里ro.client這個用戶,有個AllowedScopes = { "api1"},這里的權限可能不足
如下所示

 // resource owner password grant client
    new Client
    {
        ClientId = "ro.client",
        AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

        ClientSecrets =
        {
            new Secret("secret".Sha256())
        },                                     
        AllowedScopes = { "api1"}                    
    }

調整后,允許的權限如下
AllowedScopes = { "api1",IdentityServerConstants.StandardScopes.OpenId}
...期間重復的步驟省略,再次獲取一次看看

這次OK了,看到一個sub,這個也貌似對應着TestUser里的SubjectId,這個是用戶的唯一編號

 new TestUser
                {
                    SubjectId = "1",
                    Username = "alice",
                    Password = "password",                  
                },

但是如果我想返回更多的用戶信息怎么辦呢,比如返回用戶的電話號碼,Email,以及自定義的類似組織等信息,應該如何處理呢,那我們給用戶增加些身份(Claim)信息

public static List<TestUser> GetUsers()
{
    return new List<TestUser>
    {
        new TestUser
        {
            SubjectId = "1",
            Username = "alice",
            Password = "password",
            Claims = new Claim[]
            {
                new Claim(JwtClaimTypes.NickName,"Sarco"),
                new Claim(JwtClaimTypes.GivenName,"SarcoTest"),
                new Claim(JwtClaimTypes.PhoneNumber,"186221085730"),
                new Claim("org_code","3210")
            }
        },
        new TestUser
        {
            SubjectId = "2",
            Username = "bob",
            Password = "password"
        }
    };
}

我們增加了四項身份信息,NickName,GivenName,PhoneNumber和OrgCode,其中第四項是自定義的,
同時修改下Client的AllowedScopes

new Client
{
    ClientId = "ro.client",
    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

    ClientSecrets =
    {
        new Secret("secret".Sha256())
    },

    AllowedScopes = { "api1",IdentityServerConstants.StandardScopes.OpenId,
    IdentityServerConstants.StandardScopes.Profile},
}           

再次獲取Token

會發現報錯了,上面scope我沒有填寫,和之前的一樣,但是報錯了,如果填寫上,如api 或者 openid,可以成功,那看看不填有沒有辦法呢,經過一番研究,Config里GetIdentityResources()方法增加一項new IdentityResources.Profile()就可以了。

public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile()
            };
        }

再次獲取下Token,並且根據Token再次獲取用戶信息

會發現,現在nickname和given_name有了,但是phone_number和org_code還是沒有
報錯原因:GetIdentityResources()方法添加后,只是說現在授權資源里包含Profile了,但是在GetClients()方法里的AllowedScopes里並不包含,所以報錯
那么phone_number和org_code怎么沒有出現呢?

我們看下Profile的范圍,根據說明,它包含終端用戶的默認身份信息有name,family_name,given_name,family_name,middle_name,nick_name,preferred_username,profile,picture,website,gender,birthdate,zoneinfo,locate and updated_at,也就是可以這么說,這么多的身份信息都屬於Profile這個組里面,因為之前的用戶信息里,我只添加了四項

new Claim(JwtClaimTypes.NickName,"Sarco"),
new Claim(JwtClaimTypes.GivenName,"SarcoTest"),
new Claim(JwtClaimTypes.PhoneNumber,"186221085730"),
new Claim("org_code","3210")

其中NickName和GivenName是屬於Profile組的,所以,當客戶的AllowedScopes里包含IdentityServerConstants.StandardScopes.Profile時,nick_name和given_name會顯示出來,而PhoneNumber不屬於Profile里,所以不會返回顯示,自定義的組織信息肯定也無法獲取到,還記得我前面說的
經過一番研究,Config里GetIdentityResources()方法增加一項new IdentityResources.Profile()就可以了,這是為什么呢?
記得之前獲取Token的時候,Scope不填寫的時候會報錯,而填api或者openid就不會報錯,是因為如果不填寫,授權服務端就會從CliendId為"ro.client"的客戶端擁有的Scope里全找一次,而我們一開始AllowedScopes里面包含了
IdentityServerConstants.StandardScopes.Profile,但是在GetIdentityResources()方法里沒有添加new IdentityResources.Profile(),所以執行到獲取Scope為IdentityServerConstants.StandardScopes.Profile時,由於找不到這項的IdentityResources,所以失敗了,但是獲取Token的時候填寫api或者openid時,精確查找,由於AllowScopes和GetIdentityResources()都有,所以可以成功,這里可以這么說,AllowedScopes里的項必須在GetIdentityResources()或者GetApis()里里面要有
理解了這一點后,那我們調整下代碼讓自定義的org_code返回

public static IEnumerable<IdentityResource> GetIdentityResources()
{
    return new IdentityResource[]
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResources.Phone(),
        new IdentityResource("org","組織代碼",new string[]{"org_code" })             
    };
}

public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "client",

                    // no interactive user, use the clientid/secret for authentication
                    AllowedGrantTypes = GrantTypes.ClientCredentials,

                    // secret for authentication
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },

                    // scopes that client has access to
                    AllowedScopes = { "api1" }
                },
                // resource owner password grant client
                new Client
                {
                    ClientId = "ro.client",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                  
                    ///org代表GetIdentityResources()里的自定義new IdentityResource("org","組織代碼",new string[]{"org_code" }),而org_code代表用戶Claims里的new Claim("org_code","3210")
                    AllowedScopes = { "api1",IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,"org"},                 
                }               
            };
        }

調整后,再用postman獲取Token,然后從http://localhost:5000/connect/userinfo里獲取下用戶信息

順利的獲取到了phone_number,org_code等信息

總結

IdentityResource與TestUser中的Claims的關系
TestUser中的多項Claim可以成為一個IdentityResource的一個項,也就是可以創建一個IdentityResource,包含User中的一個和多個Claim信息,類似於教師證里面包含工號和職位等多個相關信息
Client的AllowedScopes與IdentityResource以及ApiResource的關系
Client的AllowedScopes中的項必須在IdentityResource或者ApiResource中能找到,否則也會報錯,ApiResource代表Api資源,IdentityResource項代表能訪問用戶身份信息包含哪些信息


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM