我无法为特定的身份验证提供程序提供帮助(从未使用过 Office 365),但这是您需要遵循的一般步骤:
- 发送请求以获取访问和刷新令牌
- 将令牌存储在通过重新加载/重新启动保留数据的存储中(对于 Web,它是 localStorage,对于 RN sqlite 或 asyncstorage 或您使用的任何东西)
- 保存可用于所有组件(Redux、Context API 甚至您自己的解决方案)的令牌和身份验证状态。当用户进行身份验证、取消身份验证或令牌过期时,这是显示/隐藏应用程序部分所必需的
- 您需要以某种方式知道令牌何时过期(无法说明如何操作,但 API 文档应该有一些信息)并使用
setTimeout 来刷新
- 刷新令牌时,应将其持久化(参见 n.2)并更新全局身份验证状态(参见 n.3)
- 当应用刚刚(重新)启动时,检查您是否有访问/刷新令牌保留在存储中(请参阅 n.2)并相应地更新全局身份验证状态(请参阅 n.3)
- 您的路由应该对身份验证状态更改做出反应(请参阅路由库文档,有关受保护/经过身份验证的路由)。显示敏感内容的组件也应该对身份验证状态更改做出反应。
这是我的 Reactjs 身份验证解决方案(遗憾的是没有 RN 示例),它使用 JWT 针对我自己的 API 对客户端进行身份验证。这种情况下的访问令牌也是刷新令牌。我使用一种没有 Redux 的方法,只使用纯 React 和 JS。我希望这会对你有所帮助。
import { useCallback, useState, useEffect } from "react";
import JWT from "jsonwebtoken";
import { ENV } from "../config";
import { useLanguageHeaders } from "./i18n";
const decodeToken = (token) =>
typeof token === "string" ? JWT.decode(token) : null;
//This class is responsible for authentication,
//refresh and global auth state parts
//I create only one instance of AuthProvider and export it,
//so it's kind of singleton
class AuthProvider {
//Getter for _authStatus
get authStatus() {
return this._authStatus;
}
constructor({ tokenEndpoint, refreshEndpoint, refreshLeeway = 60 }) {
this._tokenEndpoint = tokenEndpoint;
this._refreshEndpoint = refreshEndpoint;
this._refreshLeeway = refreshLeeway;
//When app is loaded, I load token from local storage
this._loadToken();
//And start refresh function that checks expiration time each second
//and updates token if it will be expired in refreshLeeway seconds
this._maybeRefresh();
}
//This method is called in login form
async authenticate(formData, headers = {}) {
//Making a request to my API
const response = await fetch(this._tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
...headers,
},
redirect: "follow",
body: JSON.stringify(formData),
});
const body = await response.json();
if (response.status === 200) {
//Authentication successful, persist token and update _authStatus
this._updateToken(body.token);
} else {
//Error happened, replace stored token (if any) with null
//and update _authStatus
this._updateToken(null);
throw new Error(body);
}
}
//This method signs user out by replacing token with null
unauthenticate() {
this._updateToken(null);
}
//This is needed so components and routes are able to
//react to changes in _authStatus
addStatusListener(listener) {
this._statusListeners.push(listener);
}
//Components need to unsubscribe from changes when they unmount
removeStatusListener(listener) {
this._statusListeners = this._statusListeners.filter(
(cb) => cb !== listener
);
}
_storageKey = "jwt";
_refreshLeeway = 60;
_tokenEndpoint = "";
_refreshEndpoint = "";
_refreshTimer = undefined;
//This field holds authentication status
_authStatus = {
isAuthenticated: null,
userId: null,
};
_statusListeners = [];
//This method checks if token refresh is needed, performs refresh
//and calls itself again in a second
async _maybeRefresh() {
clearTimeout(this._refreshTimer);
try {
const decodedToken = decodeToken(this._token);
if (decodedToken === null) {
//no token - no need to refresh
return;
}
//Note that in case of JWT expiration date is built-in in token
//itself, so I do not need to make requests to check expiration
//Otherwise you might want to store expiration date in _authStatus
//and localStorage
if (
decodedToken.exp * 1000 - new Date().valueOf() >
this._refreshLeeway * 1000
) {
//Refresh is not needed yet because token will not expire soon
return;
}
if (decodedToken.exp * 1000 <= new Date().valueOf()) {
//Somehow we have a token that is already expired
//Possible when user loads app after long absence
this._updateToken(null);
throw new Error("Token is expired");
}
//If we are not returned from try block earlier, it means
//we need to refresh token
//In my scenario access token itself is used to get new one
const response = await fetch(this._refreshEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
redirect: "follow",
body: JSON.stringify({ token: this._token }),
});
const body = await response.json();
if (response.status === 401) {
//Current token is bad, replace it with null and update _authStatus
this._updateToken(null);
throw new Error(body);
} else if (response.status === 200) {
//Got new token, replace existing one
this._updateToken(body.token);
} else {
//Network error, maybe? I don't care unless its 401 status code
throw new Error(body);
}
} catch (e) {
console.log("Something is wrong when trying to refresh token", e);
} finally {
//Finally block is executed even if try block has return statements
//That's why I use it to schedule next refresh try
this._refreshTimer = setTimeout(this._maybeRefresh.bind(this), 1000);
}
}
//This method persist token and updates _authStatus
_updateToken(token) {
this._token = token;
this._saveCurrentToken();
try {
const decodedToken = decodeToken(this._token);
if (decodedToken === null) {
//No token
this._authStatus = {
...this._authStatus,
isAuthenticated: false,
userId: null,
};
} else if (decodedToken.exp * 1000 <= new Date().valueOf()) {
//Token is expired
this._authStatus = {
...this._authStatus,
isAuthenticated: false,
userId: null,
};
} else {
//Token is fine
this._authStatus = {
...this._authStatus,
isAuthenticated: true,
userId: decodedToken.id,
};
}
} catch (e) {
//Token is so bad that can not be decoded (malformed)
this._token = null;
this._saveCurrentToken();
this._authStatus = {
...this._authStatus,
isAuthenticated: false,
userId: null,
};
throw e;
} finally {
//Notify subscribers that _authStatus is updated
this._statusListeners.forEach((listener) => listener(this._authStatus));
}
}
//Load previously persisted token (called in constructor)
_loadToken() {
this._updateToken(window.localStorage.getItem(this._storageKey));
}
//Persist token
_saveCurrentToken() {
if (typeof this._token === "string") {
window.localStorage.setItem(this._storageKey, this._token);
} else {
window.localStorage.removeItem(this._storageKey);
}
}
}
//Create authProvider instance
const authProvider = new AuthProvider(ENV.auth);
//This hook gives a component a function to authenticate user
export const useAuthenticate = () => {
const headers = useLanguageHeaders();
return useCallback(
async (formData) => {
await authProvider.authenticate(formData, headers);
},
[headers]
);
};
//This hook gives a function to unauthenticate
export const useUnauthenticate = () => {
return useCallback(() => authProvider.unauthenticate(), []);
};
//This hook allows components to get authentication status
//and react to changes
export const useAuthStatus = () => {
const [authStatus, setAuthStatus] = useState(authProvider.authStatus);
useEffect(() => {
authProvider.addStatusListener(setAuthStatus);
return () => {
authProvider.removeStatusListener(setAuthStatus);
};
}, []);
return authStatus;
};
功能组件内的这行代码可以知道用户是否经过身份验证:const { isAuthenticated } = useAuthStatus();