admin管理员组

文章数量:1653585

react手机验证码验证码

问题 (Problem)

Authorization is one of the first problems developers face upon starting a new project. And one of the most common types of authorization (from my experience) is the token-based authorization (usually using JWT).

授权是开发人员在开始新项目时面临的第一个问题。 基于我的经验,最常见的授权类型之一是基于令牌的授权(通常使用JWT)。

From my perspective, this article looks like "what I wanted to read two weeks ago". My goal was to write minimalistic and reusable code with a clean and straightforward interface. I had the next requirements for my implementation of the auth management:

从我的角度来看,这篇文章看起来像是“两周前我想阅读的内容”。 我的目标是使用简洁明了的界面编写极简且可重用的代码。 我对身份验证管理的实施有以下要求:

  • Tokens should be stored in local storage

    令牌应存储在本地存储中
  • Tokens should be restored on page reload

    令牌应在页面重新加载时恢复
  • Access token should be passed in the network requests

    访问令牌应在网络请求中传递
  • After expiration access token should be updated by refresh token if the last one is presented

    到期后,如果提供了最后一个令牌,则应使用刷新令牌更新访问令牌
  • React components should have access to the auth information to render appropriate UI

    React组件应该有权访问auth信息以呈现适当的UI
  • The solution should be made with pure React (without Redux, thunk, etc..)

    解决方案应使用纯React(没有Redux,thunk等)。

For me one of the most challenging questions were:

对我而言,最具挑战性的问题之一是:

  • How to keep in sync React components state and local storage data?

    如何使React组件状态和本地存储数据保持同步?
  • How to get the token inside fetch without passing it through the whole elements tree (especially if we want to use this fetch in thunk actions later for example)

    如何在不通过整个元素树的情况下获取令牌中的令牌(特别是例如,如果我们稍后要在重击操作中使用此令牌)

But let's solve the problems step by step. Firstly we will create a token provider to store tokens and provide possibility to listen to changes. After that, we will create an auth provider, actually wrapper around token provider to create hooks for React components, fetch on steroids and some additional methods. And in the end, we will look at how to use this solution in the project.

但是,让我们逐步解决问题。 首先,我们将创建一个token provider来存储令牌并提供侦听更改的可能性。 之后,我们将创建一个auth provider ,实际上是将token provider包装起来,以创建React组件的钩子,获取类固醇和一些其他方法。 最后,我们将研究如何在项目中使用此解决方案。

我只是想npm install ...并开始生产 (I just wanna npm install ... and go production)

I already gathered the package that contains all described below (and a bit more). You just need to install it by the command:

我已经收集了包含以下所有内容的软件包(还有更多内容)。 您只需要通过以下命令进行安装:

npm install react-token-auth

And follow examples in the react-token-auth GitHub repository.

并遵循react-token-auth GitHub存储库中的示例。

(Solution)

Before solving the problem I will make an assumption that we have a backend that returns an object with access and refresh tokens. Each token has a JWT format. Such an object may look like:

在解决问题之前,我将假设我们有一个后端,该后端返回带有访问和刷新令牌的对象。 每个令牌都有JWT格式。 这样的对象可能看起来像:

{
  "accessToken": "...",
  "refreshToken": "..."
}

Actually, the structure of the tokens object is not critical for us. In the simplest case, it might be a string with an infinite access token. But we want to look at how to manage a situation when we have two tokens, one of them may expire, and the second one might be used to update the first one.

实际上,令牌对象的结构对我们而言并不重要。 在最简单的情况下,它可能是带有无限访问令牌的字符串。 但是我们想看看当我们有两个令牌时如何处理情况,其中一个令牌可能会过期,而第二个令牌可能会用于更新第一个令牌。

智威汤逊 (JWT)

If you don't know what is the JWT token the best option is to go to jwt.io and look at how does it work. Now it is important that JWT token contains encoded (in Base64 format) information about the user that allows authenticate him on the server.

如果您不知道什么是JWT令牌,最好的选择是转到jwt.io并查看其工作方式。 现在,重要的是,JWT令牌包含有关用户的编码(以Base64格式)的信息,以允许在服务器上对他进行身份验证。

Usually JWT token contains 3 parts divided by dots and looks like:

通常,JWT令牌包含3个由点分隔的部分,看起来像:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.yOZC0rjfSopcpJ-d3BWE8-BkoLR_SCqPdJpq8Wn-1Mc

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.yOZC0rjfSopcpJ-d3BWE8-BkoLR_SCqPdJpq8Wn-1Mc

If we decode the middle part (eyJu...Mn0) of this token, we will get the next JSON:

如果我们解码此令牌的中间部分( eyJu...Mn0 ),我们将获得下一个JSON:

{
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516239022
}

With this information, we will be able to get the expiration date of the token.

有了这些信息,我们将能够获得令牌的到期日期。

代币提供者 (Token provider)

As I mentioned before, our first step is creating the token provider. The token provider will work directly with local storage and all changes of token we will do through it. It will allow us to listen to changes from anywhere and immediately notify the listeners about changes (but about it a bit later). The interface of the provider will have the next methods:

如前所述,我们的第一步是创建令牌提供程序。 令牌提供者将直接与本地存储一起使用,并且我们将通过令牌进行所有令牌更改。 这将使我们能够从任何地方收听更改,并立即将其更改通知听众(但稍后再通知)。 提供者的接口将具有以下方法:

  • getToken() to get the current token (it will be used in fetch)

    getToken()获取当前令牌(将在获取中使用)

  • setToken() to set token after login, logout or registration

    setToken()在登录,注销或注册后设置令牌

  • isLoggedIn() to check is the user logged in

    isLoggedIn()检查用户是否登录

  • subscribe() to give the provider a function that should be called after any token change

    subscribe()为提供程序提供一个在更改任何令牌后应调用的函数

  • unsubscribe() to remove subscriber

    unsubscribe()删除订户

Function createTokenProvider() will create an instance of the token provider with the described interface:

函数createTokenProvider()将使用所描述的接口创建令牌提供者的实例:

const createTokenProvider = () => {

    /* Implementation */

    return {
        getToken,
        isLoggedIn,
        setToken,
        subscribe,
        unsubscribe,
    };
};

All the next code should be inside the createTokenProvider function.

接下来的所有代码都应位于createTokenProvider函数内。

Let's start by creating a variable for storing tokens and restoring the data from local storage (to be sure that the session will not be lost after page reload):

让我们首先创建一个变量来存储令牌并从本地存储中还原数据(以确保会话在页面重载后不会丢失):

let _token: { accessToken: string, refreshToken: string } = 
    JSON.parse(localStorage.getItem('REACT_TOKEN_AUTH') || '') || null;

Now we need to create some additional functions to work with JWT tokens. At the current moment, the JWT token looks like a magic string, but it is not a big deal to parse it and try to extract the expiration date. The function getExpirationDate() will take a JWT token as a parameter and return expiration date timestamp on success (or null on failure):

现在,我们需要创建一些其他功能来使用JWT令牌。 目前,JWT令牌看起来像是一个魔术字符串,但是解析它并尝试提取到期日期并不是什么大问题。 函数getExpirationDate()将采用JWT令牌作为参数,并在成功时返回到期日期时间戳(失败时返回null ):

const getExpirationDate = (jwtToken?: string): number | null => {
    if (!jwtToken) {
        return null;
    }

    const jwt = JSON.parse(atob(jwtToken.split('.')[1]));

    // multiply by 1000 to convert seconds into milliseconds
    return jwt && jwt.exp && jwt.exp * 1000 || null;
};

And one more util function isExpired() to check is the timestamp expired. This function returns true if the expiration timestamp presented and if it is less than Date.now().

还有一个util函数isExpired()可以检查时间戳是否过期。 如果显示的过期时间戳记小于Date.now() ,则此函数返回true。

const isExpired = (exp?: number) => {
    if (!exp) {
        return false;
    }

    return Date.now() > exp;
};

Time to create first function of the token provider interface. Function getToken() should return token and update it if it is necessary. This function should be async because it may make a network request to update token.

是时候创建令牌提供者接口的第一个功能了。 函数getToken()应该返回令牌并在必要时对其进行更新。 此功能应该是async因为它可能会发出网络请求以更新令牌。

Using created earlier functions we can check is the access tokens expired or not (isExpired(getExpirationDate(_token.accessToken))). And in the first case to make a request for updating token. After that, we can save tokens (with the not implemented yet function setToken()). And finally, we can return access token:

使用创建的早期函数,我们可以检查访问令牌是否已过期( isExpired(getExpirationDate(_token.accessToken)) )。 并在第一种情况下发出更新令牌的请求。 之后,我们可以保存令牌(使用尚未实现的功能setToken() )。 最后,我们可以返回访问令牌:

const getToken = async () => {
    if (!_token) {
        return null;
    }

    if (isExpired(getExpirationDate(_token.accessToken))) {
        const updatedToken = await fetch('/update-token', {
            method: 'POST',
            body: _token.refreshToken
        })
            .then(r => r.json());

        setToken(updatedToken);
    }

    return _token && _token.accessToken;
};

Function isLoggedIn() will be simple: it will return true if _tokens is not null and will not check for access token expiration (in this case we will not know about expiration access token until we get fail on getting token, but usually it is sufficient, and let us keep function isLoggedIn synchronous):

函数isLoggedIn()很简单:如果_tokens不为null ,它将返回true,并且不会检查访问令牌的过期时间(在这种情况下,直到获取令牌失败,我们才会知道过期的访问令牌,但是通常这足够了) ,让我们保持函数isLoggedIn同步):

const isLoggedIn = () => {
    return !!_token;
};

I think it is a good time to create functionality for managing observers. We will implement something similar to the Observer pattern, and first of all, will create an array to store all our observers. We will expect that each element in this array is the function we should call after each change of tokens:

我认为现在是创建用于管理观察者的功能的好时机。 我们将实现类似于Observer模式的东西,首先,将创建一个数组来存储所有观察者。 我们希望该数组中的每个元素都是每次更改令牌后应调用的函数:

let observers: Array<(isLogged: boolean) => void> = [];

Now we can create methods subscribe() and unsubscribe(). The first one will add new observer to the created a bit earlier array, second one will remove observer from the list.

现在我们可以创建方法subscribe()unsubscribe() 。 第一个将新的观察者添加到创建的数组中,第二个将观察者从列表中删除。

const subscribe = (observer: (isLogged: boolean) => void) => {
    observers.push(observer);
};

const unsubscribe = (observer: (isLogged: boolean) => void) => {
    observers = observers.filter(_observer => _observer !== observer);
};

You already can see from the interface of the functions subscribe() and unsubscribe() that we will send to observers only the fact of is the user logged in. But in general, you could send everything you want (the whole token, expiration time, etc...). But for our purposes, it will be enough to send a boolean flag.

您已经可以从函数subscribe()unsubscribe()的接口中看到,我们将仅向用户发送事实给观察者。但是通常,您可以发送所需的所有信息(整个令牌,到期时间等)。 但是出于我们的目的,发送一个布尔标志就足够了。

Let's create a small util function notify() that will take this flag and send to all observers:

让我们创建一个小的util函数notify() ,它将使用此标志并将其发送给所有观察者:

const notify = () => {
    const isLogged = isLoggedIn();
    observers.forEach(observer => observer(isLogged));
};

And last but not least function we need to implement is the setToken(). The purpose of this function is saving tokens in local storage (or clean local storage if the token is empty) and notifying observers about changes. So, I see the goal, I go to the goal.

最后但并非最不重要的功能是我们需要实现的setToken() 。 此功能的目的是将令牌保存在本地存储中(如果令牌为空,则清除本地存储),并通知观察者有关更改。 所以,我看到了目标,我走向了目标。

const setToken = (token: typeof _token) => {
    if (token) {
        localStorage.setItem('REACT_TOKEN_AUTH', JSON.stringify(token));
    } else {
        localStorage.removeItem('REACT_TOKEN_AUTH');
    }
    _token = token;
    notify();
};

Be sure, if you came to this point in the article and found it useful, you already made me happier. Here we finish with the token provider. You can look at your code, play with it and check that it works. In the next part on top of this, we will create more abstract functionality that will be already useful in any React application.

可以肯定的是,如果您在本文中提到这一点并认为它很有用,那么您已经使我更快乐了。 在这里,我们以令牌提供者结束。 您可以查看您的代码,使用它并检查它是否有效。 在此之上的下一部分中,我们将创建更多抽象功能,这些功能在任何React应用程序中都已经有用。

身份验证提供者 (Auth provider)

Let's create a new class of objects that we will call as an Auth provider. The interface will contain 4 methods: hook useAuth() to get fresh status from React component, authFetch() to make requests to the network with the actual token and login(), logout() methods which will proxy calls to the method setToken() of the token provider (in this case, we will have only one entry point to the whole created functionality, and the rest of the code will not have to know about existing of the token provider). As before we will start from the function creator:

让我们创建一个新的对象类,称为Auth提供者。 该界面将包含4种方法:钩useAuth()来获得新的身份从阵营组件, authFetch()发出请求到网络的实际令牌和login() logout()方法,这将代理调用方法setToken() (令牌提供者)(在这种情况下,我们只有一个进入整个已创建功能的入口点,其余代码不必知道令牌提供者的存在)。 和以前一样,我们将从函数创建器开始:

export const createAuthProvider = () => {

    /* Implementation */

    return {
        useAuth,
        authFetch,
        login,
        logout
    }
};

First of all, if we want to use a token provider we need to create an instance of it:

首先,如果要使用令牌提供程序,则需要创建它的实例:

const tokenProvider = createTokenProvider();

Methods login() and logout() simply pass token to the token provider. I separated these methods only for explicit meaning (actually passing empty/null token removes data from local storage):

方法login()logout()只是将令牌传递给令牌提供者。 我仅出于明确含义分离了这些方法(实际上传递空/空令牌会从本地存储中删除数据):

const login: typeof tokenProvider.setToken = (newTokens) => {
    tokenProvider.setToken(newTokens);
};

const logout = () => {
    tokenProvider.setToken(null);
};

The next step is the fetch function. According to my idea, this function should have exactly the same interface as original fetch and return the same format but should inject access token to each request.

下一步是获取功能。 根据我的想法,此函数应具有与原始提取完全相同的接口,并返回相同的格式,但应为每个请求注入访问令牌。

Fetch function should take two arguments: request info (usually URL) and request init (an object with method, body. headers and so on); and returns promise for the response:

提取函数应采用两个参数:请求信息(通常为URL)和请求初始化(具有方法,主体,标头等的对象); 并返回响应的承诺:

const authFetch = async (input: RequestInfo, init?: RequestInit): Promise<Response> => {
    const token = await tokenProvider.getToken();

    init = init || {};

    init.headers = {
        ...init.headers,
        Authorization: `Bearer ${token}`,
    };

    return fetch(input, init);
};

Inside the function we made two things: took a token from the token provider by statement await tokenProvider.getToken(); (getToken already contains the logic of updating the token after expiration) and injecting this token into Authorization header by the line Authorization: 'Bearer ${token}'. After that, we simply return fetch with updated arguments.

在函数内部,我们做了两件事:通过语句await tokenProvider.getToken();从令牌提供者那里获取令牌await tokenProvider.getToken(); ( getToken已经包含在过期后更新令牌的逻辑),并通过Authorization: 'Bearer ${token}'行将此​​令牌注入Authorization标头。 之后,我们只需返回带有更新参数的访存。

So, we already can use the auth provider to save tokens and use them from fetch. The last problem is that we can not react to the token changes from our components. Time to solve it.

因此,我们已经可以使用auth提供程序来保存令牌并从提取中使用它们。 最后一个问题是我们无法对组件中的令牌更改做出React。 是时候解决了。

As I told before we will create a hook useAuth() that will provide information to the component is the user logged or not. To be able to do that we will use hook useState() to keep this information. It is useful because any changes in this state will cause rerender of components that use this hook.

如前所述,我们将创建一个钩子useAuth() ,无论用户是否登录,它都会向组件提供信息。 为此,我们将使用hook useState()保留此信息。 这很有用,因为此状态下的任何更改都将导致使用此挂钩的组件重新呈现。

And we already prepared everything to be able to listen to changes in local storage. A common way to listen to any changes in the system with hooks is using the hook useEffect(). This hook takes two arguments: function and list of dependencies. The function will be fired after the first call of useEffect and then relaunched after any changes in the list of dependencies. In this function, we can start to listen to changes in local storage. But what is important we can return from this function… new function and, this new function will be fired either before relaunching the first one or after the unmounting of the component. In the new function, we can stop listening to the changes and React guarantees, that this function will be fired (at least if no exception happens during this process). Sounds a bit complicated but just look at the code:

我们已经做好了一切准备,可以收听本地存储中的更改。 监听带有钩子的系统中任何更改的一种常见方法是使用钩子useEffect() 。 这个钩子有两个参数:函数和依赖项列表。 第一次调用useEffect之后将触发该函数,然后在依赖项列表中进行任何更改后将重新启动该函数。 在此功能中,我们可以开始侦听本地存储中的更改。 但是重要的是我们可以从该函数中返回……新函数,并且该新函数将在重新启动第一个函数或卸载组件之后被触发。 在新函数中,我们可以停止侦听更改并停止React保证,该函数将被触发(至少在此过程中没有异常发生的情况下)。 听起来有点复杂,但只需看一下代码即可:

const useAuth = () => {
    const [isLogged, setIsLogged] = useState(tokenProvider.isLoggedIn());

    useEffect(() => {
        const listener = (newIsLogged: boolean) => {
            setIsLogged(newIsLogged);
        };

        tokenProvider.subscribe(listener);
        return () => {
            tokenProvider.unsubscribe(listener);
        };
    }, []);

    return [isLogged] as [typeof isLogged];
};

And that's all. We've just created compact and reusable token auth storage with clear API. In the next part, we will look at some usage examples.

就这样。 我们刚刚使用清晰的API创建了紧凑且可重复使用的令牌身份验证存储。 在下一部分中,我们将看一些用法示例。

用法 (Usage)

To start to use what we implemented above, we need to create an instance of the auth provider. It will give us access to the functions useAuth(), authFetch(), login(), logout() related to the same token in the local storage (in general, nothing prevents you to create different instances of auth provider for different tokens, but you will need to parametrize the key you use to store data in the local storage):

要开始使用上面实现的功能,我们需要创建auth提供程序的实例。 它将使我们能够访问与本地存储中的同一令牌相关的函数useAuth()authFetch()login()logout() (通常,没有什么可以阻止您为不同的令牌创建auth provider的不同实例,但是您将需要参数化用于将数据存储在本地存储中的密钥):

export const {useAuth, authFetch, login, logout} = createAuthProvider();

登录表单 (Login form)

Now we can start to use the functions we got. Let's start with the login form component. This component should provide inputs for the user's credentials and save it in the internal state. On submit we need to send a request with the credentials to get tokens and here we can use the function login() to store received tokens:

现在我们可以开始使用获得的功能了。 让我们从登录表单组件开始。 该组件应提供用户凭据的输入,并将其保存在内部状态。 在提交时,我们需要发送带有凭据的请求以获取令牌,在这里我们可以使用函数login()来存储接收到的令牌:

const LoginComponent = () => {
    const [credentials, setCredentials] = useState({
        name: '',
        password: ''
    });

    const onChange = ({target: {name, value}}: ChangeEvent<HTMLInputElement>) => {
        setCredentials({...credentials, [name]: value})
    };

    const onSubmit = (event?: React.FormEvent) => {
        if (event) {
            event.preventDefault();
        }

        fetch('/login', {
            method: 'POST',
            body: JSON.stringify(credentials)
        })
            .then(r => r.json())
            .then(token => login(token))
    };

    return <form onSubmit={onSubmit}>
        <input name="name"
               value={credentials.name}
               onChange={onChange}/>
        <input name="password"
               value={credentials.password}
               onChange={onChange}/>
    </form>
};

And that's all, it's everything we need to store the token. After that, when a token is received, we will not need to apply extra effort to bring it to fetch or in components, because it is already implemented inside the auth provider.

仅此而已,这就是我们存储令牌所需的一切。 此后,当收到令牌时,我们将无需花费额外的精力将其放入或放入组件中,因为它已在auth提供程序内部实现。

Registration form is similar, there are only differences in the number and names of input fields, so I will omit it here.

注册表格是相似的,输入字段的数量和名称只是有所不同,因此在此将其省略。

路由器 (Router)

Also, we can implement routing using the auth provider. Let's assume that we have two packs of routes: one for the registered user and one for not registered. To split them we need to check do we have a token in local storage or not, and here we can use hook useAuth():

另外,我们可以使用auth提供程序来实现路由。 假设我们有两包路由:一包用于注册用户,另一包用于未注册。 要拆分它们,我们需要检查本地存储中是否有令牌,在这里我们可以使用hook useAuth()

export const Router = () => {
    const [logged] = useAuth();

    return <BrowserRouter>
        <Switch>
            {!logged && <>
                <Route path="/register" component={Register}/>
                <Route path="/login" component={Login}/>
                <Redirect to="/login"/>
            </>}
            {logged && <>
                <Route path="/dashboard" component={Dashboard} exact/>
                <Redirect to="/dashboard"/>
            </>}
        </Switch>
    </BrowserRouter>;
};

And the nice thing that it will be rerendered after any changes in local storage, because of useAuth has a subscription to these changes.

而且很高兴在本地存储中进行任何更改后都将重新呈现它,因为useAuth拥有对这些更改的订阅。

提取请求 (Fetch requests)

And then we can get data protected by the token using authFetch. It has the same interface as fetch, so if you already use fetch in the code you can simply replace it by authFetch:

然后,我们可以使用authFetch获得受令牌保护的数据。 它具有与fetch相同的接口,因此,如果您已在代码中使用fetch,则可以简单地将其替换为authFetch

const Dashboard = () => {
    const [posts, setPosts] = useState([]);

    useEffect(() => {
        authFetch('/posts')
            .then(r => r.json())
            .then(_posts => setPosts(_posts))
    }, []);

    return <div>
        {posts.map(post => <div key={post.id}>
            {post.message}
        </div>)}
    </div>
};

摘要 (Summary)

We did it. It was an interesting journey, but it also has the end (maybe even happy).

我们做到了。 这是一段有趣的旅程,但也有结局(甚至高兴)。

We started with the understanding of problems with storing authorization tokens. Then we implemented a solution and finally looked at the examples of how it might be used in the React application.

我们从了解存储授权令牌的问题开始。 然后,我们实现了一个解决方案,并最终查看了如何在React应用程序中使用它的示例。

As I told before, you can find my implementation on GitHub in the library. It solves a bit more generic problem and does not make assumptions about the structure of the object with tokens or how to update the token, so you will need to provide some extra arguments. But the idea of the solution is the same and the repository also contains instructions on how to use it.

如前所述,您可以在库中的GitHub上找到我的实现。 它解决了一些更通用的问题,并且不对带有令牌的对象的结构或如何更新令牌进行假设,因此您将需要提供一些额外的参数。 但是解决方案的思想是相同的,并且存储库还包含有关如何使用它的说明。

Here I can say Thank you for the reading of the article and I hope it was helpful for You.

在这里我可以说谢谢您阅读本文,希望对您有所帮助。

翻译自: https://habr/en/post/485764/

react手机验证码验证码

本文标签: 验证码令牌手机React