IdentityServer4-10 - 添加对外部认证的支持之QQ登录

IdentityServer4 添加对外部认证的支持之QQ登录

IdentityServer4 前言

前面我们提到过IdentityServer4是可以添加外部认证的,如果外部认证支持OAuth2,那么添加到IdentityServer4是非常简单的,在ASP.NET Core下提供了非常多的外部认证实现,比如Google,Facebook,Twitter,Microsoft帐户和OpenID Connect等,但是对于我们国内来说最常用的莫过于QQ登录。

申请QQ登录

访问QQ互联官方网站:https://connect.qq.com/

点击“应用管理”-> “创建应用”,填写你的网站信息,这里的信息请不要胡乱填写,这个会影响审核的,以后要是修改了这些信息需要重新审核。

填写完善资料的时候,唯一一个需要注意的就是回调地址,这里我们后面详细介绍。

等待审核结果,这里审核还是非常快的,一般一天左右就行了

注意:如果网站没有备案号我不知道是否能通过申请,我自己是拥有备案号的,然后网站LOGO必须上传,不然会申请不过的。

添加QQ登录

QQ登录是支持OAuth2,所以可以集成到IdentityServer4。本来是打算自己写一个的,但是在查找信息的过程中,发现已经有人实现了,组件名为:Microsoft.AspNetCore.Authentication.QQ,Nuget可以直接安装。

1.先将 Microsoft.AspNetCore.Authentication.QQ 组件添加到项目中

2.配置QQ登录信息

在Startup类的ConfigureServices方法里添加如下代码:

1
2
3
4
5
6
services.AddAuthentication()
.AddQQ(qqOptions =>
{
qqOptions.AppId = "";
qqOptions.AppKey = "";
})

3.在QQ互联后台配置回调地址

回调地址是随时可以在QQ互联后台配置的,因为这个回调地址已经在QQ登录组件里定义了的,所以此处配置为:

1
http://你的域名/signin-qq

比如:

1
2
http://localhost:2692/signin-qq
http://www.baidu.com/signin-qq

4.添加跳转的action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[HttpGet]
public async Task<IActionResult> ExternalLogin(string provider, string returnUrl)
{
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("ExternalLoginCallback"),
Items =
{
{ "returnUrl", returnUrl }
}
};

// start challenge and roundtrip the return URL
props.Items.Add("scheme", provider);
return Challenge(props, provider);
}

5.添加回调处理成功跳转的Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
HttpGet]
public async Task<IActionResult> ExternalLoginCallback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync("QQ");

if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}

// retrieve claims of the external user
var externalUser = result.Principal;
var claims = externalUser.Claims.ToList();

// try to determine the unique id of the external user (issued by the provider)
// the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
if (userIdClaim == null)
{
userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
}
if (userIdClaim == null)
{
throw new Exception("Unknown userid");
}

// remove the user id claim from the claims collection and move to the userId property
// also set the name of the external authentication provider
claims.Remove(userIdClaim);
var provider = result.Properties.Items["scheme"];
var userId = userIdClaim.Value;

// this is where custom logic would most likely be needed to match your users from the
// external provider's authentication result, and provision the user as you see fit.
//
// check if the external user is already provisioned
var user = _users.FindByExternalProvider(provider, userId);
if (user == null)
{
// this sample simply auto-provisions new external user
// another common approach is to start a registrations workflow first
user = _users.AutoProvisionUser(provider, userId, claims);
}

var additionalClaims = new List<Claim>();

// if the external system sent a session id claim, copy it over
// so we can use it for single sign-out
var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
if (sid != null)
{
additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
}

// if the external provider issued an id_token, we'll keep it for signout
AuthenticationProperties props = null;
var id_token = result.Properties.GetTokenValue("id_token");
if (id_token != null)
{
props = new AuthenticationProperties();
props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } });
}

// issue authentication cookie for user
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, userId, user.SubjectId, user.Username));
await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, props, additionalClaims.ToArray());

// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);

// validate return URL and redirect back to authorization endpoint or a local page
var returnUrl = result.Properties.Items["returnUrl"];
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}

return Redirect("~/");
}

画了一张图来表示这个流程:

运行测试

1.打开登录页面,点击“QQ”

2.从QQ登录

我们通过第一步,跳转到了QQ的登录页面:

登录之后,QQ也有相应的提醒

登录之后跳转回我们自己的程序:

这里显示的名称是根据QQ获取用户信息接口返回的QQ昵称

同时,我们也可以在QQ互联里面的授权管理查看我们刚刚授权登录的信息:

其他说明

如果遇到其他异常可以用抓包软件比如fiddler,抓一下与QQ通信的请求信息,看看是否有异常。