跳到主要内容

gToken

本项目已开源,开源地址 https://github.com/guofusheng007/gtoken

about JWT and gToken

JWT(JSON Web Token)是一种基于Token的身份验证和授权机制,它使用JSON格式来表示用户身份信息。

jwt在前后端分离项目中常用,jwt是token中一种实现方式之一,如JWT-dgrijalvaJWT-form3tech第三方包。

gToken是JWT实现方式之一,用来实现前后端基于Token的身份认证。

gToken使用

gToken是一种非常简单实用的jwt第三方包,token采用AES/CBC模式加密。前后端都可以有效校验token包。

UserID     int         `json:"userid"`     //用户id
UserName string `json:"username"` //用户名称
UserEmail string `json:"email"` //用户邮箱
UserMobile string `json:"mobile"` //用户电话
ExpiresAt time.Time `json:"expire"` //Token的TTL,即有效期。
IssuedAt time.Time `json:"issuedat"` //Token签发时间
Issuer string `json:"issuer"` //Token制作者
Subject string `json:"subject"` //Token应用的项目
TokenID string `json:"tokenid"` //TokenID,主要作用是校验前端提交的token是否真实,防止token泄露而带来的安全风险。

func CreateToken(gtoken *Gtoken,key []byte) (string)
创建token,其中key是16位或24位或32位的字符切片,返回BASE64字串。

func CheckToken(encToken string, key []byte) (*Gtoken,int64,error)
校验token,主要用来检查前端提供的token是否真实有效。
返回参数

  • 第一个, token本身的数据信息
  • 第二个, 本token的TTL值(分钟),0表示已失效。
  • 第三个, err

模块安装 go get github.com/guofusheng007/gtoken 模块引用

import (
"github.com/guofusheng007/gtoken"
)

基本信息

项目目录

# tree gtoken
gtoken
├── crypto.go //加解密模块
├── gtoken.go //token创建和校验
├── example //示例
│   ├── go
│   │   ├── go.mod
│   │   ├── go.sum
│   │   └── main.go //token生成与重签接口
│   └── react
│   ├── crypto.js //token解密模块
│   ├── index.js //react入口
│ ├── test-admin.js //login成功后进入的页面
│ ├── test-login.js //login,初始化token
│ ├── test-update.js //token重签模块,供每个程序调用。
│ └── test-read.js //业务数据,它会调用test-update.js来验证token
└── README.md

目标

  • go与react分离交互示例
  • 使用go开发jwt模块(加密、校验)
  • 使用react前端对jwt进行校验、及刷新token
  • 前后端同时具备token校验能力。

前后端jwt认证流程

  • 前端用户录入正确的用户名称和密,提交给后端,后端收到后对数据进行验证,通过验证后产生初始token并通过headers返回给用户。
  • 前端用户收到后端提交的token后保存在cookie中,供所有页面查看(主要是供token刷新页面使用)
  • 其它页面在渲染生效前调用token刷新程序。
    • token刷新程序从cookie查找token,若找不到,直接跳转到login让用户重新登录,重新生成初始化token
    • 若找到token,但token的TTL已失效,则直接跳转到login让用户重新登录,重新生成初始化token。
    • 若token的ttl值生效中,但TTL时间还很长,那不刷新token,让调用它的页面继续。
    • 若token的ttl值小于或等于某个值(示例中为2分钟),则向后台申请重签token.
  • 后端收到前端的token刷新申请时,对前端提供的token及tokenID进校验。此时后端toke刷新程序做如下识别
    • 若收到的token密文件为'undefined'或null,则返回 {"info":"Token已过期,不能续签,请重新认证后产生新Token"}
    • 若收到的token密文正常,则对密文进行校验。
    • 若密文中TTL值小于或等于0,则token已失效,返顺 {"info":"Token已过期,不能续签,请重新认证后产生新Token"}
    • 若用户提效的tokenid与token密文中的tokenid不一致,说明token已泄露或被伪造,拒绝重签token,返回 {"info":"用户提交的 TokenID 有误"}
    • 若token中的TTL值大于某个指定值(如2分钟),则暂时不用续签,返回如下 {"info":"Token TTL大于2分钟,暂时不需要续签"}
    • 若token中的TTL值小于或等于某个指定值(如2分钟),则允许重新续签Token。 将生成的新Token通过headers返回前端,并返回body信息。 {"info":"Token更新成功"}
  • 前端token刷新程序收到后端重新签发的token后更新旧token(写入cookie),调用页面继续。

提示:

  • 如上验证过程,前端和后端对token的验证是重复的,是有必要,出于安全要求,防止其它用户跳过前端token刷新程序而直接采用curl等来拿到新token.
  • 前端应用认证最长时间,取决于cookie和Token的两个TTL值,最小的TTL是前端最终有效的认证有效期,超过了就需通过login生新Token。

go后端

token生成及校验模块

crypto.go

crypto.go
//加解密方式

package gtoken

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"strconv"
)

//----------------------------------------------------------------
//---------------------CBC模式-----------------------------------
//----------------------------------------------------------------
//注: 本方式的iv由key的前16字组成(而不是自动随机产生),并存储在密文的前16个字节
//填充数据至AES块大小
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}

//移除填充数据
func pkcs5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}

//加密
func EncryptCBC(txt string, key []byte,) (string, error) {
data := []byte(txt)
//key长度:必须是16,24,32
if len:= len(string(key));(len != 16) && (len != 24) && (len != 32) {
err := errors.New("KEY length must 16, 24, or 32,current len:" + strconv.Itoa(len))
return "",err
}

// 采用何种加解密算法,取决于key的长度。
// len(key) = 16,AES-128-GCM
// len(key) = 24,AES-256-GCM
// len(key) = 32,AES-512-GCM
block, err := aes.NewCipher(key) // 分组秘钥
if err != nil {
return "", err
}

blockSize := block.BlockSize() // 获取秘钥块的长度。此值是固定值:16
//blockSize := aes.BlockSize
data = pkcs5Padding(data, blockSize) // 补全码

//加密
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式。IV值,直接取key的一部分即可。
encrypted := make([]byte, len(data)) // 创建数组
blockMode.CryptBlocks(encrypted, data) // 加密
return base64.StdEncoding.EncodeToString(encrypted), nil
}


//解密
func DecryptCBC(txt string, key []byte) (string, error) {
data,_ := base64.StdEncoding.DecodeString(txt)
//data := []byte(txt)
//key长度:必须是16,24,32
if len:= len(string(key));(len != 16) && (len != 24) && (len != 32) {
err := errors.New("KEY length must 16, 24, or 32,current len:" + strconv.Itoa(len))
return "",err
}

// 采用何种加解密算法,取决于key的长度。
// len(key) = 16,AES-128-GCM
// len(key) = 24,AES-256-GCM
// len(key) = 32,AES-512-GCM
block, err := aes.NewCipher(key) // 分组秘钥
if err != nil {
return "", err
}

blockSize := block.BlockSize() // 获取秘钥块的长度。此值是固定值:16
//blockSize := aes.BlockSize

//加密
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式
decrypted := make([]byte, len(data)) // 创建数组
//fmt.Println("err1")
blockMode.CryptBlocks(decrypted, data) // 解密.该函数没有出错返回。
//fmt.Println("err2")
decrypted = pkcs5UnPadding(decrypted) // 去除补全码
return string(decrypted),nil
}

gtoken.go

gtoken.go
package gtoken

import (
"encoding/json"
"math/rand"
"time"
)

type Gtoken struct {
UserID int `json:"userid"`
UserName string `json:"username"`
UserEmail string `json:"email"`
UserMobile string `json:"mobile"`
ExpiresAt time.Time `json:"expire"`
IssuedAt time.Time `json:"issuedat"`
Issuer string `json:"issuer"`
Subject string `json:"subject"`
TokenID string `json:"tokenid"` //动态随机字串,12个随机字母组成的字串,在校验token时识别客户端提交的token和tokenID是否一致。
}


//creae token encText
func CreateToken(gtoken *Gtoken,key []byte) (string){
token, _ := json.Marshal(gtoken)
enc,_ := EncryptCBC(string(token),key)
return enc
}

//解析token
//其中返回值int是token TTL过期时间(分钟),零时表示已过期。
func CheckToken(encToken string, key []byte) (*Gtoken,int64,error) {
var token Gtoken
dec_text,err := DecryptCBC(encToken,key)
if err != nil {
return nil,0,err
} else {
//fmt.Println(dec_text)
json.Unmarshal([]byte(dec_text), &token)
t := int64(token.ExpiresAt.Sub(time.Now()).Minutes())
if (t <= 0) {
t = 0
}
return &token,t,nil
}
}

//产生随机字串,供tokenID使用
func RandomString(length int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, length)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

api接口

main.go

package main

import (
"fmt"
"time"
"github.com/guofusheng007/gtoken"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/filter/cors"
)

type TestController struct {
beego.Controller
}

//Token配置
var key = []byte("012345678901234511111111") // token共享key,其长度为16或24或32个字符切片

//-----------------用户端提供的信息-----------------------
type UserCredentials struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}
//存储页面录入的信息
var user UserCredentials = UserCredentials{}


//基本配置
func init() {
//webadmin
beego.BConfig.Listen.EnableAdmin = true
beego.BConfig.Listen.AdminAddr = "localhost"
beego.BConfig.Listen.AdminPort = 8088

//BindXXX网页数据解析
beego.BConfig.CopyRequestBody = true
//关闭自动渲染
beego.BConfig.WebConfig.AutoRender = false


//CORS安全配置
var corsconf = cors.Options {
AllowAllOrigins: true,
AllowCredentials: true,
AllowOrigins: []string{"*"},
AllowMethods:[]string{"GET","POST","PUT","DELETE","OPTIONS"},
AllowHeaders:[]string{
"Origin",
"Authorization",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Headers",
"Content-Type",
"token",
"tokenid",
},
ExposeHeaders:[]string{
"Content-Length",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Headers",
"Content-Type",
"token",
"tokenid",
},
}
//所有url需要命中该中间函数,即打开url前先运行该配置。
beego.InsertFilter("/*", beego.BeforeRouter, cors.Allow(&corsconf))
}

func main() {
//认证处理/token更新
var auth = new(TestController)
beego.Router("/login", auth, "post:Login_v2")
beego.Router("/updatetoken", auth, "get:UpdateToken") //查看cookies和headers

//运行beego服务
beego.Run()
}
//------------------------------------------------

//API 用户login
//认证后,把凭证写入cookie或session
func (c *TestController) Login_v2() { //react提交post用户帐号和密码以加密方式提交给接口
//react提交post用户帐号和密码
c.BindJSON(&user)
fmt.Printf("%+v\n",user) //查看用户端提供的body数据
//c.ShowHeadCookie() //查看header、cookie.回显到服务端console口

//通过验证后产生新token返回组用户
if ((user.Username == "guofs") && (user.Password == "123321")) {
//token信息,如下值不能作为公共变量,否则时间不会变化。
var token = gtoken.Gtoken{
UserID: 10,
UserName: "guofs",
UserEmail: "guofs@139.com",
UserMobile: "13700000000",
ExpiresAt: time.Now().Add(time.Minute * 6), //第一次签发token时Token的TTL为:5分钟
IssuedAt: time.Now(),
Issuer: "guofs",
Subject: "webtest",
TokenID: gtoken.RandomString(12),
}
//fmt.Println(token.TokenID)
token_enc := gtoken.CreateToken(&token,key)
c.Ctx.Output.Header("Token",token_enc)
c.Ctx.WriteString(`{"info":"验证成功"}`)
//fmt.Printf("token:%#v\n",token)
////fmt.Printf("token:%v\n",token)

} else {
c.Ctx.Output.Header("Token","")
c.Ctx.WriteString(`{"info":"验证失败"}`)
}
}

//供react客户端刷新token(即续签过程)
func (c *TestController) UpdateToken() {
//从headers中获取客户端提取token认息
token_enc := c.Ctx.Request.Header.Get("Token")
token_id := c.Ctx.Request.Header.Get("Tokenid")
//fmt.Printf("旧Token:%v\n",token_enc)
//fmt.Printf("旧TokenID:%v\n",token_id)

//判断token的有效性
if ((token_enc == "undefined") || (token_enc == "")) {
fmt.Println("Token已过期,不能续签,请重新认证后产生新Token")
c.Ctx.Output.Header("Token","")
c.Ctx.WriteString(`{"info":"Token已过期,不能续签,请重新认证后产生新Token"}`)
return
}

//查验客户端提交的token是否合法,若不能解析,则直接跳出。
oldToken, ExpiresTime,err := gtoken.CheckToken(token_enc,key)
if err != nil {
c.Ctx.Output.Header("Token","")
c.Ctx.WriteString(`{"info":"Token非法,不能解析,请重新认证后产生新Token"}`)
fmt.Println("Token非法,不能解析,请重新认证后产生新Token")
return
}
//fmt.Printf("旧Token将在 %d 分钟后过期\n",ExpiresTime)

//token失效处理
if (ExpiresTime <= 0) {
fmt.Println("Token已过期,不能续签,请重新认证后产生新Token")
c.Ctx.Output.Header("Token","")
c.Ctx.WriteString(`{"info":"Token已过期,不能续签,请重新认证后产生新Token"}`)
return
}

//当用户提交 tokenID与token密文中的tokenid不相同时,禁止更新token.
if (token_id != oldToken.TokenID ) {
//该check是为了token泄露后而造在的安全风险。
c.Ctx.Output.Header("Token",token_enc)
c.Ctx.WriteString(`{"info":"用户提交的 TokenID 有误"}`)
fmt.Println("用户提交的 TokenID 有误")
return
}

//当ttl > 2 时暂时不需要update token
if (ExpiresTime > 2 ) {
//该check是为了减少不必要的计算,减省cpu计算压力
c.Ctx.Output.Header("Token",token_enc)
c.Ctx.WriteString(`{"info":"Token TTL大于2分钟,暂时不需要续签"}`)
fmt.Println("Token TTL大于2分钟,暂时不需要续签")
return
}


//token更新。当token的TTL小于或等2分钟时,需及时续签。
if ((ExpiresTime <= 2 ) && (token_id == oldToken.TokenID )) {
//token信息
var token = gtoken.Gtoken{
UserID: 10,
UserName: "guofs",
UserEmail: "guofs@139.com",
UserMobile: "13700000000",
ExpiresAt: time.Now().Add(time.Minute * 5), //续签时Token的TTL为:5分钟
IssuedAt: time.Now(),
Issuer: "guofs",
Subject: "webtest",
TokenID: gtoken.RandomString(12),
}
fmt.Println(token.TokenID)
//新token密文
token_enc_New := gtoken.CreateToken(&token,key)
fmt.Println("新token密文:",token_enc_New)
//返回新token
c.Ctx.Output.Header("Token",token_enc_New)
c.Ctx.WriteString(`{"info":"Token更新成功"}`)
}
}


//遍历所有Header和cookie
func (c *TestController) ShowHeadCookie() {
//header打印
//单一值
//fmt.Println("Content-Type:", c.Ctx.Request.Header.Get("Content-Type"))
//遍历所有Header
fmt.Println("-------------Header-------------------")
for k, v := range c.Ctx.Request.Header {
fmt.Printf("k:%v,v:%+v\n", k, v)
}
//cookie查看
fmt.Println("-------------Cookie-------------------")
//单一值
//ck, _ := c.Ctx.Request.Cookie("username")
//遍历所有cookie
for _, v := range c.Ctx.Request.Cookies() {
// fmt.Printf("%v=%v,%s\n", v.Name,v.Value,v.Expires.Format("2006-01-02 15:04:05"))
fmt.Printf("%v=%v\n", v.Name,v.Value)
}
}

react前端

token校验模块

crypto.js

import CryptoJS from 'crypto-js';


//-------------------------普通方式,无iv----------------------------------------
// 如下函数应用于它们之间或与go配合,都是可以的。
//将key的前16字节作为iv
//加密
function Encrypt(data,key) {
const KEY = CryptoJS.enc.Utf8.parse(key);
const iv = key.slice(0,16)
//console.log("iv:",iv)
let encrypted = CryptoJS.AES.encrypt(data, KEY, //CryptoJS.AES.encrypt已将iv隐藏在密文的前16个字节中。
{
iv: CryptoJS.enc.Utf8.parse(iv), //偏移量
mode: CryptoJS.mode.CBC, //加密模式
padding: CryptoJS.pad.Pkcs7 //填充
}
);
return encrypted.toString();
//return encrypted.toString(CryptoJS.enc.Utf8.);
//return CryptoJS.enc.Utf8.stringify(encrypted.toString())
}

//解密
//准备工作:
// 解密前需了解密文来源,了解加密者的加密算法,如iv生成办法、iv存储方式,block填充方式等等。
// 例如:有些加密者会采用随机数用为iv,并隐藏在密文件的前12或16字节中,或其它位置。
// 而有些加密者会直接采用key的前16字节作为iv。
//
//本例的解密操作,是针对密文件为basse64格式,并 iv 隐藏在密文件的前16字节中
//取IV
function getIV(data) {
const encoder = new TextEncoder()
const DataByte = encoder.encode(data) //将密文转字节切片
const ivByte = DataByte.slice(0, 16) //取字节切片前16字节,即iv字节切片
const ivStr = new TextDecoder('utf-8').decode(ivByte) //将iv字节切片转字串
return ivStr
}
//解密
function Decrypt(data,key) {
//iv提取:办法一,从密文中提取前16字节。通用。
/*
const KEY = CryptoJS.enc.Utf8.parse(key);
const iv = getIV(data)
*/
//由于本次解密的密文的iv是由key的前16字节组成,可从key中直接提取。
const KEY = CryptoJS.enc.Utf8.parse(key);
const iv = key.slice(0,16)
//console.log("iv:",iv)

let decrypted = CryptoJS.AES.decrypt(data, KEY,
{
iv : CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return decrypted.toString(CryptoJS.enc.Utf8);
//return CryptoJS.enc.Utf8.stringify(decrypted) //此类书写也是可以的。
}


//对外开放定义的函数
export {
Encrypt,
Decrypt,
}

login及token刷新程序

index.js

import React from 'react';  
import ReactDOM from 'react-dom/client';
import {BrowserRouter,Routes,Route} from 'react-router-dom'

import Admin from './test-admin.js';
import Login from './test-login.js';
import Read from './test-read.js';


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<Routes>
<Route path = '/admin' element = {<Admin />} />
<Route path = '/login' element = {<Login />} />
<Route path = '/read' element = {<Read />} />
</Routes>
</BrowserRouter>
)

test-admin.js

import React from 'react';
import {Link } from 'react-router-dom'

export default function Admin() {
return (
<div>
<Link to="/read">查看记录</Link><br />
<Link to="/write">增改删</Link><br />
</div>
)
}

test-login.js

import React from 'react';
import {Link} from 'react-router-dom'
import { useNavigate } from 'react-router-dom'
import cookie from 'react-cookies'

export default function Login() {
const Token = "react_submit_token_8483"
const navigate = useNavigate();
function handleSubmit(event) {
event.preventDefault(); // 阻止表单提交
const jsonData =
{
username: document.getElementById('username').value,
password: document.getElementById('password').value,
}
//console.log(JSON.stringify(jsonData, null, 2)); //输出到浏览器的console接口

//识别提交的按扭
var url = ''
if (event.target.id === 'login') {
url = 'http://192.168.3.110:8080/login'
console.log("login被单击")
}
if (event.target.id === 'register') {
url = 'http://192.168.3.110:8080/login'
console.log("register被单击")
}

//写入数据
fetch(url,{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Token': Token,
},
body: JSON.stringify(jsonData)
})
.then((response) => {
if (!response.ok) {
console.log('Failed to submit data');
throw new Error('Network response was not ok.');
}
//提取后台提供的token,并写入cookie
const token = response.headers.get('token')
console.log("token值:",token)
var ttl = new Date(new Date().getTime() + 24*60*60*1000) //1000表示1秒。
cookie.save('TOKEN', token,{expires: ttl });
return response.json() //将url的返回body信息转换为json格式
})
.then((data) => { //此部分测试正常
console.log(data) //将返回信息回显到浏览器的console接口
document.getElementById('opmsg').textContent = data.info //将post时返回信息显示到指定标签上。
if (data.info === '验证成功') {
document.getElementById('opmsg').textContent = "验证成功,请跳转到管理admin页面"
navigate("/admin",{ replace: false }); //跳转到管理admin页面
}
})
.catch(error => {
console.error('Error:', error);
document.getElementById('opmsg').textContent = '后台服务异常若其它'
});
// console.log("test2") 此行的执行次序早于上面的 console.log("test1")
}

return (
<div>
<Link to="/">Home</Link><br />
<form action="">
用户名称:<input type="text" id="username" /><br/>
用户密码:<input type="password" id="password" /><br/>
<button id="login" type="submit" onClick={handleSubmit}>登录</button>
<button id="register" type="submit" onClick={handleSubmit}>注册</button>
</form>
<p id="opmsg"></p> {/* 显示提示信息 */}
</div>
)
}

test-update.js

//v2: 采用普通函数方式
import axios from 'axios';
import cookie from 'react-cookies'
import { Decrypt } from './crypto.js';

const Authinfo = {
Msg: '',
Auth: false
}

export function UpdateToken () {
//react解密使用,由后端提供。前后端务必保持一致。
const keyDec = '012345678901234511111111'

//从cookie中读取token
const token = cookie.load('TOKEN')
//判断token是否存在,若不存在,就退出
//console.log("token:",token)
if ((token === undefined) || (token === '')) {
console.log('cookie中没有token记录。请跳转到login页面重新生成token');
Authinfo.Msg = 'cookie中没有token记录。请跳转到login页面重新生成token'
Authinfo.Auth = false
return Authinfo
}

//若token存在,则解密token
const txt_token = JSON.parse(Decrypt(token,keyDec))

//const ExpiresAt = "2024-03-29T11:12:37.4536506+08:00";
const tokenExpires = new Date(txt_token.expire);
const timeDiff = Math.round((tokenExpires.valueOf() - (new Date()).valueOf()) / (60*1000))
//console.log("解密当前token:",txt_token)
//console.log("username:",txt_token.username)

//当ttl <= 0 时,token已失效,直接跳转到login页面。
if (timeDiff <= 0) {
console.log(`token已失效,请跳转到login页面重新生成token。`)
Authinfo.Msg = 'token已失效,请跳转到login页面重新生成token'
Authinfo.Auth = false
return Authinfo
}

//当ttl > 2 时,暂时不需要更新,减少服务端计算压力。
if (timeDiff > 2) {
console.log(`token TTL ${timeDiff} 分钟,暂时不用更新.当ttl小于或等2时才更新`)
Authinfo.Msg = `token TTL ${timeDiff} 分钟,暂时不用更新.当ttl小于或等2时才更新`
Authinfo.Auth = true
return Authinfo
}

//更新token的模块,如下axios是一个整体。
try {
var flag = true
axios({
url:'http://192.168.3.110:8080/updatetoken',
method: 'get', //get或post等html方法。不区分大小写
headers: {
'Content-Type':'application/json',
'Token': token,
'Tokenid': txt_token.tokenid,
},
}).then(response => {
if(response && response.status === 200){
const token = response.headers['token'];
var ttl = new Date(new Date().getTime() + 60*60*1000)
cookie.save('TOKEN', token,{expires: ttl });
console.log("token:",token)
console.log("token更新成功")
flag = true
}else{
// 响应失败
console.log('Failed to submit data');
flag = false
}
})
if (flag) {
Authinfo.Msg = 'token更新成功'
Authinfo.Auth = true
} else {
Authinfo.Msg = 'Failed to submit data'
Authinfo.Auth = false
}
return Authinfo
} catch (error) {
console.error("Error fetching data", error);
Authinfo.Msg = 'Failed to submit data'
Authinfo.Auth = false
return Authinfo
}
}

test-read.js

import React, { useEffect, useState} from 'react';
import {Link,useNavigate } from 'react-router-dom'
import { UpdateToken } from './test-update.js';



//--------动态态数据(从api中读取)-------------------------------
export default function Read() {
//--------验证token---------------------
const navigate = useNavigate();
//调用updatetoken函数
var auth = UpdateToken()
console.log("authInfo",auth)
//识别返回信息
if ( !auth.Auth ) {
console.log("认证过期,",auth.Msg)
navigate("/login",{ replace: false }); //跳转到login页面,重新生成token
} else {
console.log("认证未过期,",auth.Msg)
}

//--------业务模块---------------------
//定义状态变量
const [data, setData] = useState([]);
//更新状态,即单击按扭时执行一次fetch
function change() {
// 异步获取数据的逻辑
fetch('http://192.168.3.110:8080/read')
.then(response => response.json())
.then(data => {
setData(data);
document.getElementById('opmsg').textContent = ''
})
.catch(error => {
console.error('Error:', error);
document.getElementById('opmsg').textContent = 'error,超时,可能网络异常'
});
}

//当App被调用一次时,useEffect就执行一次fetch
// 或
useEffect(() => {
change()
}, []);



return (
<div>
<Link to="/">Home</Link><br />
<button onClick={change}> 刷新 </button><p id="opmsg"></p>
{data.map( (user) => (
<div key={user.id}>
{user.name},{user.tel}
</div>
))}
</div>
);
};

测试

Alt text Alt text Alt text