浅析C#中单点登录的原理和使用

姓名:尤学强  学号:17101223374

转载自:http://mp.weixin.qq.com/s/roHnftkXh9E9i4V3Kf6fKQ

【嵌牛导读】:单点登录原理及代码的实现

【嵌牛鼻子】:单点登录

【嵌牛提问】:单点登录优缺点都有什么?

【嵌牛正文】:

什么是单点登录?

我想肯定有一部分人“望文生义”的认为单点登录就是一个用户只能在一处登录,其实这是错误的理解(我记得我第一次也是这么理解的)。

单点登录指的是多个子系统只需要登录一个,其他系统不需要登录了(一个浏览器内)。一个子系统退出,其他子系统也全部是退出状态。如果你还是不明白,我们举个实际的例子把。比如博客园首页:https://www.cnblogs.com,和博客园的找找看http://zzk.cnblogs.com。

这就是两个系统(不同的域名)。如果你登录其中一个,另一个也是登录状态。如果你退出一个,另一个也是退出状态了。

那么这是怎么实现的呢?这就是我们今天要分析的问题了。

单点登录(SSO)原理

首先我们需要一个认证中心(Service),和两个子系统(Client)。

当浏览器第一次访问Client1时,处于未登录状态 -> 302到认证中心(Service) -> 在Service的登录页面登录(写入Cookie记录登录信息) -> 302到Client1(写入Cookie记录登录信息)

第二次访问Client1 -> 读取Client1中Cookie登录信息 -> Client1为登录状态

第一次访问Client2 -> 读取Client2中Cookie中的登录信息 -> Client2为未登录状态 -> 302到在Service(读取Service中的Cookie为登录状态) -> 302到Client2(写入Cookie记录登录信息)

我们发现在访问Client2的时候,中间时间经过了几次302重定向,并没有输入用户名密码去登录。用户完全感觉不到,直接就是登录状态了。

图解:

手撸一个SSO

环境:.NET Framework 4.5.2

Service:

///

/// 登录

///

///

///

///

///

[HttpPost]

public string Login(string name, string passWord, string backUrl)

{

if (true)//TODO:验证用户名密码登录

{

//用Session标识会话是登录状态

Session["user"] = "XX已经登录";

//在认证中心 保存客户端Client的登录认证码

TokenIds.Add(Session.SessionID, Guid.NewGuid());

}

else//验证失败重新登录

{

return "/Home/Login";

}

return backUrl + "?tokenId=" + TokenIds[Session.SessionID];//生成一个tokenId 发放到客户端

}

Client:

public static List Tokens = new List();

public async Task Index()

{

var tokenId = Request.QueryString["tokenId"];

//如果tokenId不为空,则是由Service302过来的。

if (tokenId != null)

{

using (HttpClient http = new HttpClient())

{

//验证Tokend是否有效

var isValid = await http.GetStringAsync("http://localhost:8018/Home/TokenIdIsValid?tokenId=" + tokenId);

if (bool.Parse(isValid.ToString()))

{

if (!Tokens.Contains(tokenId))

{

//记录登录过的Client (主要是为了可以统一登出)

Tokens.Add(tokenId);

}

Session["token"] = tokenId;

}

}

}

//判断是否是登录状态

if (Session["token"] == null || !Tokens.Contains(Session["token"].ToString()))

{

return Redirect("http://localhost:8018/Home/Verification?backUrl=http://localhost:26756/Home");

}

else

{

if (Session["token"] != null)

Session["token"] = null;

}

return View();

}

效果图:

当然,这只是用较少的代码撸了一个较简单的SSO。仅用来理解,勿用于实际应用。

IdentityServer4实现SSO

环境:.NET Core 2.0

上面我们手撸了一个SSO,接下来我们看看.NET里的IdentityServer4怎么来使用SSO。

首先建一个IdentityServer4_SSO_Service(MVC项目),再建两个IdentityServer4_SSO_Client(MVC项目)

在Service项目中用nuget导入IdentityServer4 2.0.2、IdentityServer4.AspNetIdentity 2.0.0、IdentityServer4.EntityFramework 2.0.0

在Client项目中用nuget导入IdentityModel 2.14.0

然后分别设置Service和Client项目启动端口为 5001(Service)、5002(Client1)、5003(Client2)

在Service中新建一个类Config:

public class Config

{

public static IEnumerable GetIdentityResources()

{

return new List

{

new IdentityResources.OpenId(),

new IdentityResources.Profile(),

};

}

public static IEnumerable GetApiResources()

{

return new List

{

new ApiResource("api1", "My API")

};

}

// 可以访问的客户端

public static IEnumerable GetClients()

{

return new List

{

// OpenID Connect hybrid flow and client credentials client (MVC)

//Client1

new Client

{

ClientId = "mvc1",

ClientName = "MVC Client1",

AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

RequireConsent = true,

ClientSecrets =

{

new Secret("secret".Sha256())

},

RedirectUris = { "http://localhost:5002/signin-oidc" }, //注意端口5002 是我们修改的Client的端口

PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

AllowedScopes =

{

IdentityServerConstants.StandardScopes.OpenId,

IdentityServerConstants.StandardScopes.Profile,

"api1"

},

AllowOfflineAccess = true

},

//Client2

new Client

{

ClientId = "mvc2",

ClientName = "MVC Client2",

AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

RequireConsent = true,

ClientSecrets =

{

new Secret("secret".Sha256())

},

RedirectUris = { "http://localhost:5003/signin-oidc" },

PostLogoutRedirectUris = { "http://localhost:5003/signout-callback-oidc" },

AllowedScopes =

{

IdentityServerConstants.StandardScopes.OpenId,

IdentityServerConstants.StandardScopes.Profile,

"api1"

},

AllowOfflineAccess = true

}

};

}

}

新增一个ApplicationDbContext类继承于IdentityDbContext:

public class ApplicationDbContext : IdentityDbContext

{

public ApplicationDbContext(DbContextOptions options)

: base(options)

{

}

protected override void OnModelCreating(ModelBuilder builder)

{

base.OnModelCreating(builder);

}

}

在文件appsettings.json中配置数据库连接字符串:

"ConnectionStrings": {

"DefaultConnection": "Server=(local);Database=IdentityServer4_Demo;Trusted_Connection=True;MultipleActiveResultSets=true"

}

在文件Startup.cs的ConfigureServices方法中增加:

public void ConfigureServices(IServiceCollection services)

{

services.AddDbContext(options =>

options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); //数据库连接字符串

services.AddIdentity()

.AddEntityFrameworkStores()

.AddDefaultTokenProviders();

services.AddMvc();

string connectionString = Configuration.GetConnectionString("DefaultConnection");

var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

services.AddIdentityServer()

.AddDeveloperSigningCredential()

.AddAspNetIdentity()

.AddConfigurationStore(options =>

{

options.ConfigureDbContext = builder =>

builder.UseSqlServer(connectionString,

sql => sql.MigrationsAssembly(migrationsAssembly));

})

.AddOperationalStore(options =>

{

options.ConfigureDbContext = builder =>

builder.UseSqlServer(connectionString,

sql => sql.MigrationsAssembly(migrationsAssembly));

options.EnableTokenCleanup = true;

options.TokenCleanupInterval = 30;

});

}

并在Startup.cs文件里新增一个方法InitializeDatabase(初始化数据库):

///

/// 初始数据库

///

///

private void InitializeDatabase(IApplicationBuilder app)

{

using (var serviceScope = app.ApplicationServices.GetService().CreateScope())

{

serviceScope.ServiceProvider.GetRequiredService().Database.Migrate();//执行数据库迁移

serviceScope.ServiceProvider.GetRequiredService().Database.Migrate();

var context = serviceScope.ServiceProvider.GetRequiredService();

context.Database.Migrate();

if (!context.Clients.Any())

{

foreach (var client in Config.GetClients())//循环添加 我们直接添加的 5002、5003 客户端

{

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.GetApiResources())

{

context.ApiResources.Add(resource.ToEntity());

}

context.SaveChanges();

}

}

}

修改Configure方法:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)

{

//初始化数据

InitializeDatabase(app);

if (env.IsDevelopment())

{

app.UseDeveloperExceptionPage();

app.UseBrowserLink();

app.UseDatabaseErrorPage();

}

else

{

app.UseExceptionHandler("/Home/Error");

}

app.UseStaticFiles();

app.UseIdentityServer();

app.UseMvc(routes =>

{

routes.MapRoute(

name: "default",

template: "{controller=Home}/{action=Index}/{id?}");

});

}

然后新建一个AccountController控制器,分别实现注册、登录、登出等。

新建一个ConsentController控制器用于Client回调。

然后在Client的Startup.cs类里修改ConfigureServices方法:

public void ConfigureServices(IServiceCollection services)

{

services.AddMvc();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>

{

options.DefaultScheme = "Cookies";

options.DefaultChallengeScheme = "oidc";

}).AddCookie("Cookies").AddOpenIdConnect("oidc", options =>

{

options.SignInScheme = "Cookies";

options.Authority = "http://localhost:5001";

options.RequireHttpsMetadata = false;

options.ClientId = "mvc2";

options.ClientSecret = "secret";

options.ResponseType = "code id_token";

options.SaveTokens = true;

options.GetClaimsFromUserInfoEndpoint = true;

options.Scope.Add("api1");

options.Scope.Add("offline_access");

});

}

对于Client的身份认证就简单了:

[Authorize]//身份认证

public IActionResult Index()

{

return View();

}

///

/// 登出

///

///

public async Task Logout()

{

await HttpContext.SignOutAsync("Cookies");

await HttpContext.SignOutAsync("oidc");

return View("Index");

}

效果图:

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

推荐阅读更多精彩内容