我有以下前端:

AuthContext.js:

const { createContext, useState, useContext } = require("react");

const AuthContext = createContext();

export function useAuth() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [auth, setAuth] = useState();

  return (
    <AuthContext.Provider value={{
      auth,
      setAuth
    }}>
      {children}
    </AuthContext.Provider>
  )
}

这里是全局[auth, setAuth]身份验证对象,它将通过JWT身份验证保存accessToken和refreshToken.从数据库登录时填充auth对象:

一百:

import React, { useEffect, useState } from 'react'
import axios from '../api/axios';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

const LOGIN_URL = '/login';

function Login() {
  const { auth, setAuth } = useAuth();

  const navigate = useNavigate();
  const location = useLocation();

  const from = location.state?.from?.pathname || '/';
  const errorMessage = location.state?.errorMessage;

  const [user, setUser] = useState('');
  const [pwd, setPwd] = useState('');
  const [errMsg, setErrMsg] = useState('');


  useEffect(() => {
    setErrMsg('');
  }, [user, pwd]);

  useEffect(() => {
    if (errorMessage) {
      setErrMsg(errorMessage);
    }
  }, [])

  async function handleSubmit(e) {
    e.preventDefault();
    try {
      const res = await axios.post(LOGIN_URL, {
        username: user,
        password: pwd
      });
      const accessToken = res.data.accessToken;
      const refreshToken = res.data.refreshToken;
      const roles = res.data.roles;
      setAuth({ user, pwd, accessToken, refreshToken, roles }); // POPULATING AUTHENTICATION OBEJCT
      console.log(refreshToken); // debug what is current refresh token
      setUser('');
      setPwd('');
      navigate(from, { replace: true });
    } catch (err) {
      if (err.response?.status === 403) {
        setErrMsg('wrong username or password');
      } else {
        setErrMsg('Login Failed');
      }
    }
  }

  return (
    <section>
      <p className={errMsg ? 'errmsg' : 'hide'}>{errMsg}</p>
      <h1>Sign In</h1>
      <form onSubmit={handleSubmit}>
        <label htmlFor="username">Username:</label>
        <input type="text" id="username" onChange={e => setUser(e.target.value)} value={user} required />
        <label htmlFor="password">Password:</label>
        <input type="password" id="password" onChange={e => setPwd(e.target.value)} value={pwd} required />
        <button>Sign In</button>
      </form>
      <p>
        Need an Account? <br />
        <NavLink className="link" to='/register'>Sign Up</NavLink>
      </p>
    </section>
  )
}

export default Login

accessToken过期时,前端向GET /refresh发送请求,以获取新的accesToken.这可以很好地工作:

useRefreshToken.js:

import { useAuth } from '../context/AuthContext';
import axios from '../api/axios';

function useRefreshToken() {
  const { auth, setAuth } = useAuth();

  async function refresh() {
    console.log(auth.refreshToken);
    const res = await axios.get('/refresh', {
      headers: {
        'Refresh-Token': auth.refreshToken
      }
    });
    setAuth(prev => {
      return { ...prev, accessToken: res.data };
    });
    return res.data;
  }
  return refresh;
}

export default useRefreshToken;

此外,只要访问令牌到期(通过受保护资源的401 http状态发信号),就会通过axios拦截器自动完成对/refresh的请求:

一百:

import useRefreshToken from "./useRefreshToken"
import { useAuth } from '../context/AuthContext';
import { axiosJwt } from "../api/axios";
import { useEffect } from "react";

function useAxiosJwt() {
  const { auth } = useAuth();
  const refresh = useRefreshToken();

  useEffect(() => {
    const requestInterceptor = axiosJwt.interceptors.request.use(
      conf => {
        if (!conf.headers['Authorization']) {
          conf.headers['Authorization'] = `Bearer ${auth.accessToken}`;
        }
        return conf;
      }, err => Promise.reject(err)
    )

    const responseInterceptor = axiosJwt.interceptors.response.use(
      res => res,
      async err => {
        const prevReq = err.config;
        if (err.response.status === 401 && !prevReq.sent) {
          prevReq.sent = true;
          const newAccessToken = await refresh();
          prevReq.headers['Authorization'] = `Bearer ${newAccessToken}`;
          return axiosJwt(prevReq);
        }
        return Promise.reject(err);
      }
    )

    return () => {
      axiosJwt.interceptors.request.eject(requestInterceptor);
      axiosJwt.interceptors.request.eject(responseInterceptor);
    }
  }, [auth]);

  return axiosJwt;
}

export default useAxiosJwt

自动刷新功能也运行良好.问题是这refreshToken美元什么时候到期.当发生这种情况时,用户必须再次登录,这样auth对象就会填充新的renhToken(我将在此之后展示后端执行此操作).后台只需在其到期时(达到/refresh时)将其移除,并在登录时创建一个新的.并且后端确实返回正确的新刷新令牌.但是该新的刷新令牌不反映在react 状态(setAuth({ user, pwd, accessToken, refreshToken, roles });)中,即使通过调试console.log(refreshToken)确认确实是新的刷新令牌(因此后端工作良好).因此,当命中下一个/refresh时,REACT发送不再在后端中的旧的刷新令牌(在旧的令牌到期时被移除).那么,为什么Reaction没有反映从后台返回的新刷新Token呢?

以下是Firefox控制台(为了澄清,我补充了一些 comments ):

50362bcc-7ee7-47c3-88bb-6748b4475c67 Login.js:43 // this is first login so it returns currently stored refresh token from backend's database
50362bcc-7ee7-47c3-88bb-6748b4475c67 useRefreshToken.js:8 // debugging the current token
XHRGET
http://localhost:8080/user // asking for protected resource, but access token has expired
[HTTP/1.1 401  15ms]

50362bcc-7ee7-47c3-88bb-6748b4475c67 useRefreshToken.js:8 // again debugging current refresh token
XHRGET
http://localhost:8080/refresh // automatically going for refresh to get new access token, this works fine
[HTTP/1.1 200  22ms]

XHRGET
http://localhost:8080/user //asking for protected resource with new accessToken, works fine
[HTTP/1.1 200  55ms]

XHRGET
http://localhost:8080/user // asking for protected resource again (for testing), again access token expired (it has deliberately low duration for testing)
[HTTP/1.1 401  10ms]

50362bcc-7ee7-47c3-88bb-6748b4475c67 useRefreshToken.js:8 // current refresh token
XHRGET
http://localhost:8080/refresh // now the refresh token itself expired (400 status, chosen by design), so the user has to login again
[HTTP/1.1 400  20ms]

XHRPOST
http://localhost:8080/login // logging to get new refresh token
[HTTP/1.1 200  196ms]

7eef8b87-9be6-42e3-85ae-4d607ceb5e27 Login.js:43 // this is the new refresh token from backend (previous was 50362bcc-7ee7-47c3-88bb-6748b4475c67)
XHRGET
http://localhost:8080/user // asking for protected resource with new refresh token
[HTTP/1.1 200  50ms]

XHRGET
http://localhost:8080/user // but access token expired so going to refresh
[HTTP/1.1 401  11ms]

50362bcc-7ee7-47c3-88bb-6748b4475c67 useRefreshToken.js:8 // debug wrong - old refresh token, the new one was not setAuth'ed, dont know why
XHRGET
http://localhost:8080/refresh // so axios sending the old refresh token (50362bcc-7ee7-47c3-88bb-6748b4475c67), instead of the new one (7eef8b87-9be6-42e3-85ae-4d607ceb5e27) so this is the error.
[HTTP/1.1 403  29ms]

在控制台中,您可以看到新的刷新令牌是从后端发送的,但在Reaction中不会被setAuth反映出来.为什么?

编辑: 下面是后端控制器,以便您了解端点返回的内容.正如我之前所说的,后台运行良好,console.log次调试证明了这一点.这只是为了澄清:

一百:

@RestController
@AllArgsConstructor
public class AuthController {
    private AuthenticationManager authenticationManager;
    private AuthService authService;
    private JwtService jwtService;
    private RefreshTokenService refreshTokenService;

    @PostMapping("/register")
    public void register(@RequestBody RegisterDtoin registerDtoin) {
        authService.createUserByRegistration(registerDtoin);
    }

    @PostMapping("/login") // the refresh token is deleted if expired (done in authService.getUserByLogin, not shown) and recreated and returned (again as correctly shown in console.log(refreshToken)) so LoginDtout is {String acessToken, String refreshToken, Long id, String username, and List roles}, all retuned by this endpoint
    public ResponseEntity<LoginDtoout> login(@RequestBody LoginDtoin loginDtoIn) {
        Authentication authentication = authenticationManager.authenticate(
                 new UsernamePasswordAuthenticationToken(
                         loginDtoIn.username,
                         loginDtoIn.password
                 )
        );
        if(authentication.isAuthenticated()){
            return ResponseEntity.ok(authService.getUserByLogin(loginDtoIn));
        } else {
            throw new UsernameNotFoundException("invalid user request");
        }
    }

    @GetMapping("/refresh") // returns only new accessToken, new refreshToken is obtained from login
        public ResponseEntity<String> refresh(@RequestHeader("Refresh-Token") String rtoken) throws RefreshTokenExpiredException {
        RefreshToken refreshToken = refreshTokenService.findRefreshTokenByToken(rtoken);
        if(refreshTokenService.hasExpired(refreshToken)){
            throw new RefreshTokenExpiredException("refresh token has expired. Please login again to create new one");
        }
        String newJwt = jwtService.generateToken(refreshToken.getUser().getUsername());
        return ResponseEntity.ok(newJwt);
    }
}

源代码: https://github.com/shepherd123456/authentication_spring_and_react

这是为那些从提供的GitHub链接下载源代码的人准备的:当Git克隆源代码并运行后端(Spring bootstrap )时,Spring实体是MySQL表(当然,您的MySQL数据库必须在Application.Properties中重命名).第一次创建时,有表"角色"(角色实体).您必须将其填充为 insert into role(name) values('USER'), ('EDITOR'), ('ADMIN');个 这样,在创建(注册)用户时,会自动为其分配角色"User".就这样.另外,React应用程序在src/main/frontend中,您需要安装依赖项npm i,然后在npm run start中启动create-react-app开发服务器(任何使用过React的人都知道这一点).应该不会有CORS问题,因为Spring BootSecurityConfig.java允许localhost:3000个源,这是React的开发服务器默认运行的地方.但正如我在最初的帖子中所说,后端在这里不是问题.我真的很想知道为什么Reaction的setAuth不在登录时设置新的renhToken(当旧的过期时),而是使用旧的.因为使用旧的会被删除(到期时会自动删除),当然会导致nullException.因为在登录后,必须将正在正确创建的新项设置为react 状态(通过setAuth).

推荐答案

你在useAxiosJwt中没有弹出响应

您的代码:

return () => {
    axiosJwt.interceptors.request.eject(requestInterceptor);
    axiosJwt.interceptors.request.eject(responseInterceptor);
  }

在你弹出interceptors.request秒的时候

将其替换为:

return () => {
  axiosJwt.interceptors.request.eject(requestInterceptor);
  axiosJwt.interceptors.response.eject(responseInterceptor);
}

Explanation:

当您将状态传递到拦截器回调时,状态的值将被截图.无论何时调用这些回调,它们都将使用与快照相同的旧状态.这就是每当发生状态更改时,您必须弹出旧回调并为新回调分配更新数据的原因.

Why was this working with the request interceptor?

您正确地排除了请求者拦截器的回调,这就是它们始终具有最新值的原因.

What happened with the response interceptor?

因为它们没有弹出,所以它们始终拥有旧数据,不仅是刷新令牌,而且如果您登录了auth对象,您将看到其中的所有内容都将是陈旧的数据,如accesToken和其他内容.

点击此处阅读更多信息:https://react.dev/learn/state-as-a-snapshot

Reactjs相关问答推荐

将对象添加到集合中的数组时的no_id字段

在Reaction中更新状态时,我将变得未定义

以下代码是否存在安全问题?

如何使数据库数据在第一次呈现时显示?

使用`Link`包装`tr`标记会在Next.js中产生水合错误14

可以使用mode.css/mode.scss引用常规的类名吗?

如何使用.Aggregate从多个集合中获取数据,包括使用Mongoose+NextJS的SUM值

useRef()的钩子调用无效

React 为什么标签导致 onClick 事件运行 2 次?

BrowserRouter改变了URL但没有链接到页面

React Native 背景动画反转

onChange in useEffect 的最佳实践

如何在 Next Js 13 App Router 中添加 Favicon 图标?

如何到达类组件中的状态?

页面加载时不显示数组中的数据

使用 Jest/React 测试库时,如何测试由 setInterval 函数创建的 DOM 更改?

使用 react-router-dom 时弹出空白页面

使用 React.UseEffect 从 api 获取但我的清理返回不起作用

Mui CardMedia 的图片组件

在 Redux 中更新布尔状态时出错