赞
踩
用户认证(Authentication) 是应用中非常重要的一个功能模块。它通常由密码验证、二步验证等方式组成,用于保证用户的账户安全。目前市面上大多数公司都有自己的认证服务,但是对于小型互联网企业而言,自己搭建认证服务也不太现实,所以很多时候需要选择第三方提供商进行认证。比如腾讯QQ邮箱、微信支付、支付宝等,这些都是第三方认证服务提供商。如果开发者不想自己搭建认证服务器,又想用某种形式的用户认证,那么可以选择这些认证服务提供商。
前端应用一般采用单页面模式,也就是所有的页面都在同一个页面内实现,每一次页面跳转都会刷新整个页面,这种情况下用户登录信息会丢失,用户需要重新登录。为了解决这个问题,现在许多公司都在研发基于 OAuth 的方案,OAuth 是 Open Authorization 的简称,是一种授权机制,通过 OAuth,第三方应用可以使用自身账号代替用户给予的授权申请,从而让用户无感知地获取资源访问权限。
采用 OAuth 的前端应用流程如下所示:
实际项目中,前端应用可能存在多个 OAuth 服务供用户选择。比如 QQ、微信登录等。我们可以根据需求选择适合的服务,然后把令牌存储到前端应用本地,并在每次请求时自动添加到请求头中。这样就可以实现用户免登录状态下正常访问应用。
JSON Web Token (JWT) 是一种基于 JSON 格式的数据结构,可以用来表示令牌,用于身份验证或者信息交换。它的主要特征包括签名、验证、加密、压缩等。我们可以利用 JWT 来建立单点登录 (Single Sign-On) 的功能。
JWT 可以在请求头中传递,以 Bearer 开头,例如:
Authorization: Bearer <token>
前端应用可以存储 JWT 令牌,并且在每个请求头中添加该令牌,后端可以通过解析该令牌获取用户相关的信息。
本文通过对用户认证在 React 中的实践,一步步的讲解如何用 OAuth 和 JWT 在 React 中实现用户认证。 首先,我们先来看一下整体认证流程:
从上面的流程图中,我们看到 OAuth 和 JWT 的配合可以帮助我们完成单点登录的功能。下面我们再来逐步讲解在 React 中如何实现用户认证。
OAuth 是 Open Authorization 的缩写,是一个关于授权的标准。它描述了如何让第三方应用获得有限的访问权限,相当于提供了一个 API 来帮你实现用户认证。它最初是 Facebook 为其应用程序开放 API 的时候提出的,但由于商业原因和版权问题,近年来开始逐渐流行。随着互联网的发展,越来越多的公司和组织开始选择使用 OAuth 来实现用户认证。
OAuth 定义了四种授权类型,分别为:
一般来说,前两种授权类型都要求用户手动授予授权,后两种授权类型不需要用户手动授权,只需向 OAuth 服务器提供 Client ID 和 Client Secret 即可。下面我们就来分析一下 Authorization Code Grant Type 。
Authorization Code Grant Type 是 OAuth 协议中的授权类型,这种授权类型通常用于第三方网站或应用的用户认证。用户访问第三方应用时,应用会先请求用户给予相应权限。当用户同意后,应用会重定向到指定的回调地址,并附带一个授权码。
授权码只能使用一次,且只能通过 HTTPS 连接。应用将得到授权码后,需要向 OAuth 服务器请求访问令牌。请求中需要提供以下参数:
OAuth 服务器将根据客户端提供的授权码和其他参数生成访问令牌,并将访问令牌返回给应用。
访问令牌是应用通过 OAuth 访问受保护资源时的凭据,包含应用的用户信息、权限范围等。访问令牌一般包含三部分:Header、Payload、Signature。其中 Header 包含令牌的类型、Token 使用的 Hash 函数和签名算法;Payload 则包含关于用户的信息、权限范围、过期时间、其他一些信息;Signature 则是使用 HMAC - SHA256 或 RSA 算法计算得到的结果。
应用收到访问令牌后,可以缓存它,并在后续的 API 请求中将它放在请求头中,向 OAuth 服务器请求相应资源。访问令牌的有效期很短,通常是几分钟甚至几秒钟,过期时需要重新获取。
JSON Web Tokens (JWT) 是一种基于 JSON 格式的数据结构,可以用来表示令牌,用于身份验证或者信息交换。它的主要特征包括签名、验证、加密、压缩等。我们可以利用 JWT 来建立单点登录 (Single Sign-On) 的功能。
JWT 可以在请求头中传递,以 Bearer 开头,例如:
Authorization: Bearer <token>
前端应用可以存储 JWT 令牌,并且在每个请求头中添加该令牌,后端可以通过解析该令牌获取用户相关的信息。
首先,我们需要创建自己的 OAuth 应用,并获取 client_id 和 client_secret。这个可以在 OAuth 服务提供商那里找到,也可以直接向开源平台如 Auth0 申请。Auth0 是全球领先的云原生身份管理提供商,提供了多种安全性、可扩展性和集成度,可满足各种身份和访问控制的需求。本文使用的 Auth0 平台没有强制要求你注册,直接登录即可。
Auth0 提供的配置界面让我们很容易地设置好 OAuth 应用。我们只需要按照提示填写一些必要的参数,就可以创建出一个完整的 OAuth 应用。
配置完毕后,我们就得到了 client_id 和 client_secret。
用户需要访问我们的应用时,需要首先请求用户登录,并同意授权。在 OAuth 协议中,用户需要经历两个阶段的交互:
下面我们来模拟用户登录过程。假设用户想要访问应用,则需要先访问我们的 OAuth 应用授权接口。我们可以编写一个按钮,用户点击按钮后,程序会打开浏览器并请求用户登录,登录成功后,浏览器会跳转回应用并附带一个授权码。
const clientId = 'your_client_id';
function login() {
const url = `https://${domain}/authorize?
audience=${clientId}&
response_type=code&
scope=openid%20email%20profile& // 可选参数
redirect_uri=${redirectUri}`;
window.location = url;
}
其中 domain 表示 OAuth 服务提供商的域名,redirectUri 表示用户登录成功后的跳转地址。注意,redirect_uri 需要和 OAuth 应用中设置的 Callback URLs 一致。
授权码获取成功后,我们就可以开始获取访问令牌。
我们需要向 OAuth 服务提供商请求访问令牌。访问令牌会包含用户的身份信息,在 OAuth 授权的过程中,我们需要发送请求到授权服务器,并附带授权码、client_id 和 client_secret。
async function getAccessToken(authCode) { try { const response = await fetch(`https://${domain}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, grant_type: 'authorization_code', code: authCode, redirect_uri: redirectUri }) }); if (!response.ok) { throw new Error('Could not retrieve access token'); } return await response.json(); } catch (error) { console.error(error); } }
其中 domain 表示 OAuth 服务提供商的域名,redirectUri 表示用户登录成功后的跳转地址。注意,redirect_uri 需要和 OAuth 应用中设置的 Callback URLs 一致。
请求成功后,服务端会返回访问令牌,包括 access_token、expires_in、refresh_token 等属性。
{
"access_token": "<KEY>",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "openid profile email offline_access",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE2MTMzNjMwOTksImV4cCI6MTYxMzU0MDMxOSwiaWF0IjoxNjEzMzYzMDk5LCJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIiLCJhdWQiOiJyZXNldCJ9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1uYmljbyJ9.eyJhY3IiOiIxIiwiYW1yIjpbInBsYWluLnJlYWQiXX0.YsNpZzb_qyJaDcn3L-hfw_tT0W2NtSOfQjOX1qnoLiJpSwcEZCVxRmGzfhEotyjVB7eamZEmowrKGUDIpYw9uxTtQ"
}
此处省略了 refresh_token 和 id_token 属性,它们是 OAuth 协议中的可选属性,用于扩展 OAuth 功能。refresh_token 可以用来获取新的访问令牌,id_token 可以用来校验用户身份。
应用收到访问令牌后,需要检查其有效性。我们需要向 OAuth 服务提供商发送检查访问令牌的请求。
async function verifyAccessToken(accessToken) { try { const response = await fetch(`https://${domain}/userinfo`, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) { throw new Error('Could not verify access token'); } return await response.json(); } catch (error) { console.error(error); } }
其中 domain 表示 OAuth 服务提供商的域名,请求成功后,服务端会返回关于用户的详细信息,如用户 ID、昵称、邮箱等。
如果访问令牌有效,我们就可以认为用户已登录成功。如果访问令牌已经失效,我们需要向 OAuth 服务提供商请求更新访问令牌。
如果访问令牌已经失效,则需要向 OAuth 服务提供商请求更新访问令牌。更新访问令牌的步骤如下:
async function updateAccessToken(refreshToken) { try { const response = await fetch(`https://${domain}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, grant_type:'refresh_token', refresh_token: refreshToken }) }); if (!response.ok) { throw new Error('Could not retrieve updated access token'); } return await response.json(); } catch (error) { console.error(error); } }
其中 domain 表示 OAuth 服务提供商的域名,refreshToken 就是之前获取到的 refresh_token。请求成功后,服务端会返回新的访问令牌。
至此,我们完成了用户登录和认证的流程。用户第一次登录的时候,我们需要请求用户授权,获得授权码,再向 OAuth 服务提供商请求访问令牌。用户访问下次需要访问我们的应用时,我们就不需要再去请求用户授权,而是直接使用已有的访问令牌,并校验其有效性。如果访问令牌已经失效,则需要更新访问令牌。
下面,我们以一个简单的 React 应用来演示如何实现基于 OAuth 的用户认证。本例仅演示基本功能,但足够说明问题。
首先,我们需要安装几个依赖包:react、react-dom、@auth0/auth0-spa-js。
npm install react react-dom @auth0/auth0-spa-js
然后,我们需要引用 Auth0 的 JavaScript SDK 文件,该文件是我们用来与 Auth0 服务交互的主要文件。
<script src="https://cdn.auth0.com/auth0-spa-js/1.14/auth0-spa-js.production.js"></script>
接着,我们创建一个 Auth0 实例。
// authConfig.js
export default {
domain: 'your_domain', // 替换为你的 Auth0 域名前缀
client_id: 'your_client_id', // 替换为你的 Auth0 应用 ID
redirect_uri: `${window.location.origin}/callback` // 回调页路径,通常应该和您在 Auth0 控制台中配置的保持一致
};
// index.js
import authConfig from './authConfig';
const auth0 = createAuth0Client({
...authConfig,
});
redirect_uri 是应用登录成功后会重定向回您的应用的路径。通常应该设置为和您在 Auth0 控制台中配置的一致。
用户需要登录才能访问我们的应用,因此我们需要提供登录功能。下面是一个登录组件的例子。
import React, { useState } from'react'; const Login = () => { const [isLoading, setIsLoading] = useState(false); const handleLogin = async event => { event.preventDefault(); setIsLoading(true); await auth0.loginWithRedirect({}); setIsLoading(false); }; return isLoading? ( <div className="loader">loading...</div> ) : ( <button onClick={handleLogin}>Log in</button> ); }; export default Login;
这里的 handleLogin 方法会触发登录操作。
用户登录后,需要提供注销功能。下面是一个注销组件的例子。
import React, { useState } from'react'; const Logout = () => { const [isLoading, setIsLoading] = useState(false); const handleLogout = async event => { event.preventDefault(); setIsLoading(true); await auth0.logout({ returnTo: `${window.location.origin}`, client_id: process.env.REACT_APP_AUTH0_CLIENT_ID || '' // optional parameter to logout from all devices }); setIsLoading(false); }; return isLoading? ( <div className="loader">loading...</div> ) : ( <button onClick={handleLogout}>Log out</button> ); }; export default Logout;
这里的 handleLogout 方法会触发注销操作。returnTo 参数可以设置为登出后返回的页面。
我们需要封装一个 AuthProvider 组件,它负责管理用户登录状态,包括登录和退出。下面是一个示例。
import React, { useEffect, useState } from'react'; import auth0 from '@auth0/auth0-spa-js'; const AuthProvider = ({ children }) => { const [isAuthenticated, setIsAuthenticated] = useState(); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(); useEffect(() => { let isMounted = true; const initAuth = async () => { try { const authenticated = await auth0.isAuthenticated(); if (authenticated && isMounted) { setIsAuthenticated(true); } else if (isMounted) { setIsAuthenticated(false); } } catch (e) { setError(e); } setIsLoading(false); }; initAuth(); return () => { isMounted = false; }; }, []); const login = async () => { try { await auth0.loginWithRedirect({}); } catch (e) { setError(e); } }; const logout = () => { auth0.logout({ returnTo: `${window.location.origin}`, client_id: process.env.REACT_APP_AUTH0_CLIENT_ID || '', // optional parameter to logout from all devices }); setIsAuthenticated(false); }; return!isLoading && error === undefined? ( <AuthContext.Provider value={{ isAuthenticated, login, logout }}> {children} </AuthContext.Provider> ) : null; }; export default AuthProvider;
这里的 useEffect 会在组件渲染时执行,判断当前用户是否已登录,并初始化组件状态。setIsAuthenticated 会设为 true 或 false。
login 方法会触发登录操作,logout 方法会触发注销操作,并清空用户登录状态。
AuthContext 会存放用户登录状态,包括 isAuthenticated、login 和 logout 方法。
ProtectedRoute 组件是一个高阶组件,用于保护特定的路由。下面是一个示例。
import React from'react'; import { useLocation } from'react-router-dom'; import { useSelector } from'react-redux'; import { Redirect } from'react-router-dom'; const ProtectedRoute = ({ component: Component,...rest }) => { const location = useLocation(); const user = useSelector(state => state.userReducer.user); const renderComponent = props => { if (!user?.sub) { return <Redirect to="/login" />; } return <Component {...props} />; }; return ( <Route {...rest} render={renderComponent} /> ); }; export default ProtectedRoute;
ProtectedRoute 接收 component 属性,它代表受保护的组件。useLocation hook 会获取当前路由位置, useSelector 会获取 Redux store 中的用户数据。
如果当前用户未登录,则渲染 Redirect 组件,重定向到登录页面。如果用户已登录,则渲染受保护的组件。
登录成功后,Auth0 会重定向到登录回调页面,我们需要监听回调页面的跳转,并解析访问令牌。下面是一个示例。
import React, { useEffect } from'react'; import { useDispatch } from'react-redux'; import auth0 from '@auth0/auth0-spa-js'; import history from '../history'; import { setUser } from '../../store/actions/userActions'; const Callback = () => { const dispatch = useDispatch(); useEffect(() => { const fn = async () => { const query = window.location.search; const urlParams = new URLSearchParams(query); const accessToken = urlParams.get('access_token'); if (!accessToken) { return history.push('/'); } try { const decodedToken = jwtDecode(accessToken); localStorage.setItem('access_token', accessToken); if (decodedToken['https://example.com/roles']!== 'admin') { alert('You do not have permission to access this page.'); return history.push('/'); } const userInfo = await auth0.getUserInfo(); dispatch(setUser(userInfo)); history.push('/home'); } catch (e) { console.log(e); return history.push('/'); } }; fn(); }, [dispatch]); return <div>Loading...</div>; }; export default Callback;
这里的 useEffect 会在组件渲染时执行,并解析查询字符串中的 access_token。如果 access_token 不存在,则渲染首页。如果 access_token 存在,则尝试解码 access_token,并存储在本地缓存中。如果 decodedToken 没有 admin 角色,则渲染首页。如果 decodedToken 有 admin 角色,则获取用户信息,并设置 Redux 的 userReducer 状态,最后重定向到主页。
虽然 OAuth 和 JWT 在用户认证领域扮演着举足轻重的角色,但基于 OAuth 和 JWT 的用户认证仍然是不够的。当前仍然有很多潜在的攻击面,比如 CSRF 漏洞、跨站请求伪造、Session 劫持等等。为了更加安全地保障用户数据的安全,我们需要进一步提升用户隐私和用户数据安全。
另外,基于 OAuth 和 JWT 的用户认证还不能完全解决物理单点登录的问题。当用户把设备分离出网络时,即使他们同属一个网络,他们也无法共享登录状态。这就需要我们考虑更加复杂的单点登录解决方案,比如双因子认证、多因子认证等。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。