我有以下前端:
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).