iabaoqian0 | 🎮 Demo
最近,我必须在Increaser应用中更新和重构身份验证逻辑。完成任务后,我决定写一篇文章,以便其他人可以在他们的应用程序中添加社交验证,速度要比我快得多。
步骤1.获得凭据
我们将需要CLIENT_ID
和CLIENT_SECRET
对,让每个提供商在前端和后端实现身份验证。
要使用Google,我们需要在Google Developer Console中创建一个项目,请转到“凭据”选项卡并添加OAuth 2.0客户端。
在Increaser中,我有两个客户起源,一个用于Localhost开发,另一个用于生产版本。
重定向URI是您希望用户在授权后出现的地方。每个提供商都将查询参数附加到指定的URI,以便我们可以在前端处理它们并在后端处理它们。我们将为所有提供商使用相同的路线,并通过查询参数provider
进行区分。
要使用Facebook,我们需要在Facebook for Developers上创建一个应用程序。该过程看起来相似,这是我们在客户端OAuth设置上所拥有的。
要使用LinkedIn,我们也转到LinkedIn Developers并在那里创建一个应用程序。
步骤2.反应组件
在增加器中,我共享一个组件,其中包括登录模式和注册模式之间的授权按钮。您可能会看到唯一的区别是文本根据弹出窗口的目的而更改。
组件调用以获取身份验证链接的函数。然后,它可以浏览每个可用的提供商,并呈现一个可重复使用的按钮组件,该按钮组件充当链接。每个按钮都有不同的颜色,图标和文本。
import React from 'react'
import styled from 'styled-components'
import {
faGoogle,
faFacebookSquare,
faLinkedin
} from '@fortawesome/free-brands-svg-icons'
import Button from '../reusable/button'
import { PROVIDER, PROVIDER_NAME } from 'constants/auth'
import { useTranslation } from 'internalization'
import { getProvidersUrls } from 'utils/auth'
const PROVIDER_COLOR = {
}
const PROVIDER_ICON = {
}
const ButtonWrapper = styled.div`
margin: 10px 0;
`
const Providers = ({ purpose }) => {
const { t } = useTranslation()
const text = t('auth.modal')[purpose]
const providersUrls = getProvidersUrls()
return Object.keys(PROVIDER).map(provider =>
<ButtonWrapper key={provider}>
<Button
linkTo={providersUrls[provider]}
externalInSameTab
background={PROVIDER_COLOR[provider]}
text={`${text.button} ${PROVIDER_NAME[provider]}`}
icon={PROVIDER_ICON[provider]}
/>
</ButtonWrapper>
)
}
下面我们可以看到与授权相关的常数。我们可以在这里为每个提供商找到成对的URL和范围。
export const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
export const GOOGLE_SCOPE =
'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile'
export const FACEBOOK_AUTH_URL = 'https://www.facebook.com/v4.0/dialog/oauth'
export const FACEBOOK_SCOPE = 'public_profile,email'
export const LINKEDIN_AUTH_URL =
'https://www.linkedin.com/oauth/v2/authorization'
export const LINKEDIN_SCOPE = 'r_liteprofile r_emailaddress'
export const PROVIDER = {
GOOGLE: 'GOOGLE',
FACEBOOK: 'FACEBOOK',
LINKEDIN: 'LINKEDIN'
}
export const MODAL_PURPOSE = {
SIGN_UP: 'SIGN_UP',
SIGN_IN: 'SIGN_IN'
}
export const PROVIDER_NAME = {
}
现在,我们想将这些常数转换为授权URL。我们正在使用两个助手功能将查询参数附加到给定的URL。
const getURLWithQueryParams = (base, params) => {
const query = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')
return `${base}?${query}`
}
export const getRedirectUri = provider =>
`${window.location.origin}${PATH.OAUTH}?provider=${provider.toLowerCase()}`
export const getProvidersUrls = () => ({
[PROVIDER.GOOGLE]: getURLWithQueryParams(GOOGLE_AUTH_URL, {
client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
redirect_uri: getRedirectUri(PROVIDER.GOOGLE),
scope: GOOGLE_SCOPE,
response_type: 'code',
access_type: 'offline',
prompt: 'consent'
}),
[PROVIDER.FACEBOOK]: getURLWithQueryParams(FACEBOOK_AUTH_URL, {
client_id: process.env.REACT_APP_FACEBOOK_APP_ID,
redirect_uri: getRedirectUri(PROVIDER.FACEBOOK),
scope: FACEBOOK_SCOPE,
response_type: 'code',
auth_type: 'rerequest',
display: 'popup'
}),
[PROVIDER.LINKEDIN]: getURLWithQueryParams(LINKEDIN_AUTH_URL, {
response_type: 'code',
client_id: process.env.REACT_APP_LINKEDIN_CLIENT_ID,
redirect_uri: getRedirectUri(PROVIDER.LINKEDIN),
scope: LINKEDIN_SCOPE
})
})
用户与提供商签名后,他将出现在 /OAuth页面上。在Increaser中,此页面为空。
const OAuth = ({ processOAuthParams }) => {
useEffect(() => {
processOAuthParams()
}, [processOAuthParams])
return null
}
组件调用操作,该操作触发特定的传奇,该传奇将查询参数转换为对象。然后,将代码和提供商的名称命名为后端的标识查询。还有一些Increaser特定的代码。如果是第一个标识,则意味着用户只是注册,并且该应用将启动入职。另外,在标识后,将进行附加查询以同步应用程序。
const queryToObject = queryString => {
const pairsString =
queryString[0] === '?' ? queryString.slice(1) : queryString
const pairs = pairsString
.split('&')
.map(str => str.split('=').map(decodeURIComponent))
return pairs.reduce((acc, [key, value]) => {
if (key) {
acc[key] = value
}
return acc
}, {})
}
export function* processOAuthParams() {
const queryString = window.location.search
history.push(PATH.TIME_PICKER)
if (queryString) {
const { code, provider: lowProvider } = queryToObject(queryString)
const provider = lowProvider && PROVIDER[lowProvider.toUpperCase()]
if (provider) {
try {
const query = IDENTIFY_WITH_OAUTH_QUERY(
provider,
code,
getRedirectUri(provider),
offsetedUtils.getOffset()
)
const { identifyWithOAuth } = yield callApi(query)
yield put(receiveAuthData(identifyWithOAuth))
if (identifyWithOAuth.firstIdentification) {
yield put(showAfterSignUpModal())
getAnalytics().finishSignUp()
}
yield* synchronize()
} catch (err) {
yield put(failToProcessOAuthParams())
reportError('Fail to Authorize', err)
}
}
}
}
步骤3. nodejs后端
Increaser在后端使用GraphQl。它具有一个用于登录和注册,返回基本用户信息和JWT令牌的查询。
type User {
id: ID
email: String
name: String
token: String
tokenExpirationTime: Int
firstIdentification: Boolean
}
enum AuthProvider {
GOOGLE
FACEBOOK
LINKEDIN
}
type Query {
identifyWithOAuth(
provider: AuthProvider!
code: String!
redirectUri: String!
): User
}
我们不会研究查询处理程序的实现,因为它具有太多的Increaser特定代码。我们要注意的是我们如何从前端验证代码并从提供商那里获取电子邮件和名称等信息。
const { URLSearchParams } = require('url')
const jwt = require('jsonwebtoken')
const fetch = require('node-fetch')
const GOOGLE_TOKEN: 'https://oauth2.googleapis.com/token'
const GOOGLE_USER_INFO: 'https://www.googleapis.com/oauth2/v2/userinfo'
const FACEBOOK_TOKEN: 'https://graph.facebook.com/v4.0/oauth/access_token'
const FACEBOOK_USER_INFO: 'https://graph.facebook.com/me'
const LINKEDIN_TOKEN: `https://www.linkedin.com/oauth/v2/accessToken`
const LINKEDIN_NAME: 'https://api.linkedin.com/v2/me'
const LINKEDIN_EMAIL: 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))'
const fetchJSON = (...args) => fetch(...args).then(r => r.json())
module.exports = {
getValidatedWithGoogleUser: async (code, redirectUri) => {
const { access_token } = await fetchJSON(GOOGLE_TOKEN, {
method: 'POST',
body: JSON.stringify({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: redirectUri,
grant_type: 'authorization_code',
code
})
})
const userData = await fetchJSON(GOOGLE_USER_INFO, {
method: 'GET',
headers: {
Authorization: `Bearer ${access_token}`
}
})
return userData
},
getValidatedWithFacebookUser: async (code, redirectUri) => {
const tokenUrl = getURLWithQueryParams(FACEBOOK_TOKEN, {
client_id: process.env.FACEBOOK_CLIENT_ID,
client_secret: process.env.FACEBOOK_CLIENT_SECRET,
redirect_uri: redirectUri,
code
})
const { access_token } = await fetchJSON(tokenUrl)
const dataUrl = getURLWithQueryParams(FACEBOOK_USER_INFO, {
fields: ['email', 'name'].join(','),
access_token
})
const userData = await fetchJSON(dataUrl)
return userData
},
getValidatedWithLinkedinUser: async (code, redirectUri) => {
const body = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: process.env.LINKEDIN_CLIENT_ID,
client_secret: process.env.LINKEDIN_CLIENT_SECRET
})
const { access_token } = await fetchJSON(LINKEDIN_TOKEN, {
method: 'POST',
body
})
const payload = {
method: 'GET',
headers: { Authorization: `Bearer ${access_token}` }
}
const { localizedFirstName, localizedLastName } = await fetchJSON(
LINKEDIN_NAME,
payload
)
const userData = {
name: `${localizedFirstName} ${localizedLastName}`
}
const response = await fetchJSON(LINKEDIN_EMAIL, payload)
if (response.elements) {
userData.email = response.elements[0]['handle~'].emailAddress
}
return userData
}
}
正如我们所能看到的,我们为每个提供商遵循相同的过程。首先,我们获得access_token
并使用它来接收用户的信息。在Increaser中,一旦用户验证了,后端将通过jsonwebtoken库生成验证数据。
const jwt = require('jsonwebtoken')
const generateAuthData = id => {
const tokenExpirationTime =
Math.floor(Date.now() / 1000) + process.env.JWT_LIFESPAN_IN_SECONDS
return {
token: jwt.sign({ id, exp: tokenExpirationTime }, process.env.SECRET),
tokenExpirationTime
}
}
仅此而已。也许我没有涵盖所有内容,但我相信您可以填补空白:)