你说我一个学客户端的,怎么去研究前后端了呢?
这段时间因为手里项目的关系,要写点前端和后端的东西。需求上还挺奇怪,要实现登录,不允许有注册功能而是要求接入企业微信。对我这么个写客户端的来说,前后端说陌生也不陌生,说熟悉也算不上。那需求摆在这里了,总得写吧。
后端
Duende Identity Server 的搭建与配置
虽说之前在实习的时候,组里出过一次名为《给 Kotlin 客户端研发的后端入门教程》,用到的技术栈是 JetBrains 官方的 Ktor 框架。但因为之前稍微用过 .NET Core 那一套,并且我一直很喜欢 C# 里的 LINQ,就还是继续用 .NET Core 了。Ktor 也稍微了解过一点,但看了下社区感觉不算很活跃的样子,还是算了。
.NET Core 这边有一个官方+社区的认证服务器框架,叫 Identity Server。出到版本 4 之后调整了一下策略,保持开源的情况下有了新的商业化运作。不过这都不是我们要研究的重点。
在 Github 上另外有一个开源的项目叫 Duende.IdentityServer.Admin,对 IS 进行了一些包装,在业务上没有比较个性化的需求时,可以几乎做到开箱即用。根据 Readme 完成脚手架的搭建,并配置好数据库之后,基本上就可以直接使用了。
对于需要接入认证的应用,首先我们需要在 IS 的后台配置一个 Client(客户端)。根据不同类型的客户端应用,Open ID Connect 提供了不同的认证流程(Flow)。由于 React 的 OIDC 客户端只支持 Authentication Code Flow,因此选提供了该 Flow 的选项即可。
然后需要对客户端进行一定的配置。如下图所示,只勾选“允许通过浏览器访问令牌”。默认情况下“需要 Pkce”也是被选中的,但 React 的 OIDC 客户端我尝试了很久都没能在这个选项开启的时候成功认证,那就索性先关闭了。
接下来配置“重定向 Uri”,这个选项对应了认证页面 URL 中的 redirect_uri
字段,只有在这里配置了的重定向 Uri 才会被允许跳转。在本地开发的时候注意也需要加上 localhost
。
在“认证/注销”选项卡中有一个“注销重定向 Uri”,这个配置用于指定用户主动注销会话后的重定向 URL。实际上是否会被用到取决于 OIDC 客户端的实现。
在“令牌”选项卡中,需要配置“允许跨域来源”。这个配置是相当重要的一个选项,关系到浏览器跨域请求能否进行。注意协议和端口号一定要完全一致。
最后在“同意屏幕”中可以选择是否关掉同意屏幕。所谓同意屏幕,就是类似于 QQ 授权登录的是否确定要登录目标网站。如果浏览器存在着认证系统的有效会话,就会直接跳过这个步骤,直接重定向。
至此 IS 的配置部分就完成了。
IS 接入企业微信登录
在解决方案的 *.STS.Identity
项目中添加 AspNet.Security.OAuth.WorkWeixin
包。这个包封装了和企业微信登录的交互,我们只需要配置好申请到的密钥等信息即可。
然后打开 *.STS.Identity
下的 StartupHelper.cs
文件,找到 AddExternalProviders
方法,加入下面的代码:
if (externalProviderConfiguration.UseWeComProvider) { authenticationBuilder.AddWorkWeixin(options => { options.ClientId = externalProviderConfiguration.WeComCorpId; // corpid options.ClientSecret = externalProviderConfiguration.WeComCorpSecret; // secret options.AgentId = externalProviderConfiguration.WeComAgentId; // agentid }); }
ClientId
、ClientSecret
和 AgentId
三个参数分别对应了企业微信后台应用的 corpid
、secret
和 agentid
。
然后重新编译运行,就可以看到登录界面多了一个“外部登录”选项。
点击之后就可以正确跳转企业微信的扫码界面了。
前端
前端除了要配置 OIDC 客户端之外,还需要对原有的 AntD Pro 脚手架做比较大的一些改动。
我这里使用的 AntD Pro 是最新版本,语言选的 TypeScript。首先给项目中添加 oidc-client-ts
这个包。从名字就能看出来是 OIDC 的客户端。
首先新建一个文件用于存放认证服务器信息,这里将认证信息存放到了浏览器的 Local Storage 中:
export const oidcConfig = { client_id: 'your_client_id', redirect_uri: `${window.location.origin}/user/login`, scope: 'openid roles profile', authority: 'your_auth_server_uri', userStore: new WebStorageStateStore({ store: window.localStorage }), loadUserInfo: true } export const oidcLocalStorageKey = `oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`
找到 app.tsx
文件,将 getInitialState
修改为:
export async function getInitialState(): Promise<{ settings?: Partial<LayoutSettings>; currentUser?: User; loading?: boolean; fetchUserInfo?: () => Promise<User | undefined>; }> { const fetchUserInfo = async () => { const oidcStorage = localStorage.getItem(oidcLocalStorageKey) return oidcStorage ? User.fromStorageString(oidcStorage) : undefined }; if (history.location.pathname !== loginPath) { const currentUser = await fetchUserInfo(); return { fetchUserInfo, currentUser, settings: defaultSettings, }; } return { fetchUserInfo, settings: defaultSettings, }; }
注意这里的 User
是 oidc-client-ts
的一个类。fetchUserInfo
闭包主要用于将 Local Storage 里序列化存储的登录信息反序列化为 User
对象。
然后,在 RunTimeLayoutConfig
的返回值中,rightContentRender
、footerRender
和 childrenRender
分别用 <AuthProvider {...oidcConfig}></AuthProvider>
包裹一下,用于为组件开发提供认证信息 Context。注意导入一下上面提前定义好的 oidcConfig
。
onPageChange
也需要进行一些改动,这里给出一种参考实现。主要思路是在本地没有用户信息或者 Token 过期的时候能重定向到登录页面。
onPageChange: () => { const oidcStorage = localStorage.getItem(oidcLocalStorageKey) if (!oidcStorage && location.pathname !== loginPath) { history.push(loginPath) } else if (oidcStorage && location.pathname !== loginPath) { const currentUser = User.fromStorageString(oidcStorage!) if (currentUser.expired) { history.push(loginPath) } } }
在 src/components/RightContent/AvatarDropdown.tsx
进行修改,使得在头像下拉菜单中能显示用户名:
const auth = useAuth() // 用于提供退出登录等用户登录态管理功能 const { currentUser } = initialState // 从 initialState 中获得当前用户信息 return ( <HeaderDropdown overlay={menuHeaderDropdown}> <span className={`${styles.action} ${styles.account}`}> <Avatar size="small" style={{ background: backgroundColor, color: foregroundColor }} className={styles.avatar} alt="avatar"> {currentUser.profile.preferred_username[0].toLocaleUpperCase()} </Avatar> <span className={`${styles.name} anticon`}>{currentUser.profile.preferred_username}</span> </span> </HeaderDropdown> )
在 access.ts
中可以对接 AntD Pro 的权限系统,这里只简单区分一下是否为管理员用户。User
还是 oidc-client-ts
提供的类型。
export default function access(initialState: { currentUser?: User } | undefined) { const { currentUser } = initialState ?? {}; return { canAdmin: currentUser && currentUser.profile.role === 'Administrator', }; }
这样基本上就完成了 IS 和 AntD Pro 的对接。至于如何在 .NET Core 中完成和 IS 的对接,网络上已经有不少的资料,就不再赘述了。