feat: change axios and add auth refresh

v3
veypi 4 months ago
parent d472464d8a
commit 3b9cbe1c1b

@ -4,18 +4,13 @@
[Demo](https://oa.veypi.com)
## 使用方法
## Auth
## 引入
/init.go
code用来兑换本身应用token和兑换其他应用付出权限token
oa code 可以用来生成其他code和刷新本身code
```go
### 依赖库
```bash
docker run -dit --name=tsdb -v /Users/veypi/test/vdb:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics -search.latencyOffset=1s
nats-server -c ./script/nats.cfg
```

@ -6,7 +6,6 @@ import (
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/errs"
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/models"
"github.com/veypi/OneBD/rest"
@ -19,7 +18,7 @@ var _ = Router.Get("/:app_id/key", auth.Check("app", "app_id", auth.DoDelete), a
func appKey(x *rest.X) (any, error) {
id := x.Params.Get("app_id")
if id == "" {
return nil, errs.ArgsInvalid.WithStr("missing app_id")
return nil, rest.ErrArgMissing.WithArgs("app_id")
}
data := &models.App{}
data.ID = id
@ -65,8 +64,9 @@ func appList(x *rest.X) (any, error) {
models.App
UserStatus string `json:"user_status"`
}, 0, 10)
token, err := auth.CheckJWT(x)
tokenAny, err := auth.CheckJWT(x)
if err == nil {
token := tokenAny.(*auth.Claims)
uid := token.UID
query := cfg.DB().Table("apps").Select("apps.*,app_users.status user_status")
if opts.Name != nil {

@ -68,7 +68,7 @@ func tokenPost(x *rest.X) (any, error) {
claim.UID = refresh.UID
claim.Name = refresh.Name
claim.Icon = refresh.Icon
claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10))
claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(cfg.Config.TokenExpire))
if typ == "app" {
if refresh.AID == aid {
// refresh token
@ -113,7 +113,7 @@ func tokenPost(x *rest.X) (any, error) {
claim.UID = refresh.UID
claim.Name = refresh.Name
claim.Icon = refresh.Icon
claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10))
claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(cfg.Config.TokenExpire))
claim.Access = auth.Access{
{Name: "fs", TID: "/", Level: auth.Do},
}

@ -60,7 +60,7 @@ func userPost(x *rest.X) (any, error) {
}
ncode, err := utils.AesDecrypt([]byte(data.Code), code, []byte(data.Salt))
if err != nil || ncode != data.ID {
return nil, rest.ErrInternalServer.AppendString("code decrypt failed")
return nil, rest.ErrInternalServer.WithString("code decrypt failed")
}
if opts.Nickname != nil {
data.Nickname = *opts.Nickname

@ -7,11 +7,18 @@
package cfg
import "time"
type Options struct {
DSN string `json:"dsn"` // Data Source Name
DB string `json:"db"` // DB type: mysql, postgres, sqlite
ID string `json:"id"`
Key string `json:"key"`
TokenExpire time.Duration `json:"token_expire"` // Token expiration time in seconds
}
var Config = &Options{}
var Config = &Options{
TokenExpire: time.Minute / 2,
ID: "test",
Key: "asdfghjklqwertyuiopzxcvbnm1234567890",
}

@ -7,96 +7,12 @@
package errs
import (
"errors"
"fmt"
"net/http"
"github.com/go-sql-driver/mysql"
"github.com/veypi/OneBD/rest"
"github.com/veypi/utils/logv"
"gorm.io/gorm"
)
func JsonResponse(x *rest.X, data any) error {
x.WriteHeader(http.StatusOK)
return x.JSON(map[string]any{"code": 0, "data": data})
}
func JsonErrorResponse(x *rest.X, err error) {
code, msg := errIter(err)
x.WriteHeader(code / 100)
x.JSON(map[string]any{"code": code, "err": msg})
}
func errIter(err error) (code int, msg string) {
code = 50000
msg = err.Error()
switch e := err.(type) {
case *CodeErr:
code = e.Code
msg = e.Msg
case *mysql.MySQLError:
if e.Number == 1062 {
code = DuplicateKey.Code
msg = DuplicateKey.Msg
} else {
logv.Warn().Msgf("unhandled db error %d: %s", e.Number, err)
msg = "db error"
}
case interface{ Unwrap() error }:
code, _ = errIter(e.Unwrap())
default:
if errors.Is(e, gorm.ErrRecordNotFound) {
code = ResourceNotFound.Code
msg = ResourceNotFound.Msg
} else {
logv.Warn().Msgf("unhandled error type: %T\n%s", err, err)
msg = e.Error()
}
}
return
}
type CodeErr struct {
Code int
Msg string
}
func (c *CodeErr) Error() string {
return fmt.Sprintf("code: %d, msg: %s", c.Code, c.Msg)
}
func (c *CodeErr) WithErr(e error) error {
nerr := &CodeErr{
Code: c.Code,
Msg: fmt.Errorf("%s: %w", c.Msg, e).Error(),
}
return nerr
}
func (c *CodeErr) WithStr(m string) error {
nerr := &CodeErr{
Code: c.Code,
Msg: fmt.Errorf("%s: %s", c.Msg, m).Error(),
}
return nerr
}
// New creates a new CodeMsg.
func New(code int, msg string) *CodeErr {
return &CodeErr{Code: code, Msg: msg}
}
import "github.com/veypi/OneBD/rest"
var (
ArgsInvalid = New(40001, "args invalid")
DuplicateKey = New(40002, "duplicate key")
AuthNotFound = New(40100, "auth not found")
AuthFailed = New(40101, "auth failed")
AuthExpired = New(40102, "auth expired")
AuthInvalid = New(40103, "auth invalid")
AuthNoPerm = New(40104, "no permission")
NotFound = New(40400, "not found")
ResourceNotFound = New(40401, "resource not found")
DBError = New(50010, "db error")
AuthNotFound = rest.NewError("auth not found").WithCode(40100)
AuthFailed = rest.NewError("auth failed").WithCode(40101)
AuthExpired = rest.NewError("auth expired").WithCode(40102)
AuthInvalid = rest.NewError("auth invalid").WithCode(40103)
AuthNoPerm = rest.NewError("auth no permission").WithCode(40104)
)

@ -8,7 +8,6 @@
package auth
import (
"context"
"errors"
"fmt"
"strings"
@ -50,40 +49,45 @@ func ParseJwt(tokenString string) (*Claims, error) {
return claims, nil
}
func CheckJWT(x *rest.X) (*Claims, error) {
func checkJWT(x *rest.X) (*Claims, error) {
authHeader := x.Request.Header.Get("Authorization")
if authHeader == "" {
authHeader = x.Request.URL.Query().Get("Authorization")
if authHeader == "" {
return nil, errs.AuthNotFound
}
}
// Token is typically in the format "Bearer <token>"
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
return nil, errs.AuthInvalid
}
// Parse the token
claims, err := ParseJwt(tokenString)
if err != nil {
return nil, err
}
x.Set("token", claims)
return claims, nil
}
func Check(target string, pid string, l AuthLevel) func(x *rest.X) error {
return func(x *rest.X) error {
claims, err := CheckJWT(x)
func CheckJWT(x *rest.X) (any, error) {
return checkJWT(x)
}
func Check(target string, pid string, l AuthLevel) func(x *rest.X) (any, error) {
return func(x *rest.X) (any, error) {
claims, err := checkJWT(x)
if err != nil {
// return err
return nil, err
// return nil, err
}
tid := ""
if pid != "" {
tid = x.Params.Get(pid)
}
if !claims.Access.Check(target, tid, l) {
// return errs.AuthNoPerm
// return nil, errs.AuthNoPerm
}
x.Request = x.Request.WithContext(context.WithValue(x.Request.Context(), "uid", claims.UID))
return nil
return claims, nil
}
}

@ -179,7 +179,7 @@
init_url: init_url,
};
api.Post('/api/app', newApp)
$api.Post('/api/app', newApp)
.then(() => {
onsuccess()
name = '';

@ -117,7 +117,7 @@
token.logout()
}
user = token.body()
api.wrapFetch(token.fetch())
token.wrapAxios($axios)
$env.Guser = user
</script>

@ -414,7 +414,7 @@
};
sync = () => {
show_create_app = false
api.Get('/api/app').then((res) => {
$api.Get('/api/app').then((res) => {
apps = res;
loading = false;
});

@ -107,7 +107,7 @@
access_api = {
next: async (page, size) => {
console.log(id)
return await api.Get(access_url, {query: {page, size, app_id: id, role_id: selected_role.id || ''}})
return await $api.Get(access_url, {query: {page, size, app_id: id, role_id: selected_role.id || ''}})
}
}
</script>

@ -304,7 +304,7 @@
return
}
api.Get(`/api/app/${id}`)
$api.Get(`/api/app/${id}`)
.then((data) => {
Object.assign(app, data)
document.title = `${app.name} - 项目主页`

@ -84,15 +84,6 @@
rows = []
update_status = (id, n, old) => {
api.app.user(app.id).update(id, n).then(() => {
msg.Info('修改成功')
}).catch(() => {
const a = rows.find(a => a.id = id) || {}
a.status = old
})
}
user_role_data = []
selected = {}
show_user = async (row) => {
@ -105,7 +96,7 @@
}
user_role_api = {
next: async (page, size) => {
return await api.Get(user_role_url, {query: {page, size}})
return await $api.Get(user_role_url, {query: {page, size}})
}
}
@ -116,7 +107,7 @@
cancel: true,
persistent: true
}).onOk(() => {
api.user.reset(id).then(() => {
$api.user.reset(id).then(() => {
msg.Info('重置成功 ')
}).catch((e) => {
msg.Warn('失败 ' + e)
@ -132,11 +123,11 @@
history.back()
return
}
api.Get(`/api/app/${id}`)
$api.Get(`/api/app/${id}`)
.then((data) => {
Object.assign(app, data)
document.title = `${app.name} - 项目主页`
api.Get(`/api/user/`).then((e) => {
$api.Get(`/api/user/`).then((e) => {
rows = e
console.log(e)
})

@ -6,7 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录与注册</title>
<meta name="description" content="用户登录与注册页面" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<style>
* {
box-sizing: border-box;
@ -378,7 +377,7 @@
}
signUpError = '';
try {
const response = await api.Post('/api/user', {
const response = await $axios.post('/api/user', {
username: signUpForm.username,
code: btoa(signUpForm.password),
});
@ -396,7 +395,7 @@
handleSignIn = async (e) => {
e.preventDefault();
try {
const loginResponse = await api.Post('/api/user/login', {
const loginResponse = await $axios.post('/api/user/login', {
username: signInForm.username,
code: btoa(signInForm.password),
});

@ -132,7 +132,7 @@
loadUserData = async () => {
try {
isLoading = true;
const response = await api.Get("/api/user/" + user.id);
const response = await $api.Get("/api/user/" + user.id);
if (response) {
user = {
id: response.id,
@ -168,7 +168,7 @@
};
// 发送更新请求
const response = await api.Patch("/api/user/" + user.id, updateData);
const response = await $api.Patch("/api/user/" + user.id, updateData);
if (response) {
successMessage = "个人信息更新成功!";

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>oa</title>
<script type="module" key='vyes' src="/assets/v.js"></script>
<script type="module" key='vyes' src="/vyes/v.js"></script>
<link href="/assets/libs/tailwind/tailwind.min.css" rel="stylesheet">
<link href="/assets/libs/animate/animate.min.css" rel="stylesheet">
<link href="/assets/libs/font-awesome/css/all.min.css" rel="stylesheet">
@ -15,5 +15,17 @@
<page-404></page-404>
</vrouter>
</body>
<script setup>
if (typeof $node !== 'undefined') {
$axios.interceptors.response.use(function (response) {
if (response.data && response.data.code === 0) {
return response.data.data
}
return response;
}, function (error) {
return Promise.reject(error);
});
}
</script>
</html>

@ -6,10 +6,14 @@
*/
class TokenService {
__root = ''
constructor() {
this.tokenKey = 'access';
this.refreshTokenKey = 'refresh';
}
setRoot(root) {
this.__root = root;
}
setToken(token) {
localStorage.setItem(this.tokenKey, token);
@ -27,7 +31,7 @@ class TokenService {
return localStorage.getItem(this.refreshTokenKey);
}
clearTokens() {
clearToken() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey);
}
@ -54,24 +58,19 @@ class TokenService {
}
return this.__cache
}
logout(root) {
console.log(this)
root = root || this.__root
this.clearTokens();
location.href = root + '/login?redirect=' + window.location.pathname;
logout() {
this.clearToken();
location.href = this.__root + '/login?redirect=' + window.location.pathname;
}
__root = ''
async refreshToken(root) {
root = root || this.__root
this.__root = root
async refreshToken() {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
// this.logout()
this.logout()
return;
}
try {
let data = await fetch(root + '/api/token', {
let data = await fetch(this.__root + '/api/token', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh: refreshToken })
@ -79,11 +78,11 @@ class TokenService {
if (data.code === 0) {
this.setToken(data.data);
} else {
this.clearTokens()
this.clearToken()
}
} catch (e) {
console.error('Token刷新失败:', e);
this.clearTokens()
this.clearToken()
// logout();
}
}
@ -94,21 +93,122 @@ class TokenService {
const currentTime = Date.now() / 1000;
return decoded.exp < currentTime;
}
fetch() {
let that = this
return (url, options) => {
const token = that.getToken();
if (token) {
if (!options) {
options = {};
wrapAxios(instance) {
// 定义一个标志,用于防止在刷新令牌时发送多个刷新请求
let isRefreshing = false;
// 定义一个队列,用于存储在刷新令牌期间失败的请求
let failedQueue = [];
/**
* 处理等待队列中的请求
* 刷新令牌成功后将队列中的请求重新发送刷新失败则拒绝这些请求
* @param {Error|null} error - 如果刷新令牌失败则为错误对象
* @param {string|null} token - 刷新成功后获取的新令牌
*/
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
// 刷新失败,拒绝队列中的所有请求
prom.reject(error);
} else {
// 刷新成功,使用新令牌解决队列中的所有请求
prom.resolve(token);
}
if (!options.headers) {
options.headers = {};
});
// 清空队列
failedQueue = [];
};
// 请求拦截器:在发送请求前添加认证令牌
instance.interceptors.request.use(config => {
const token = this.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
options.headers.Authorization = `Bearer ${token}`;
return config;
}, error => {
// 对请求错误做些什么
return Promise.reject(error);
});
let that = this
// 响应拦截器:处理响应数据,特别是针对 401 状态码进行令牌刷新和重试
instance.interceptors.response.use((response) => {
// 任何 2xx 范围内的状态码都会触发此函数
// 这里可以添加其他全局的成功响应处理逻辑
return response;
}, async function(error) {
// 任何超出 2xx 范围的状态码都会触发此函数
const originalRequest = error.config;
console.log(error)
// 检查错误响应状态码是否为 401 (未授权)
// 并且确保这不是一个已经重试过的请求 (通过 originalRequest._retry 标记)
if (error.response && error.response.status === 401 && !originalRequest._retry) {
// 标记此请求为已重试,避免无限循环
originalRequest._retry = true;
// 统计该请求的重试次数
originalRequest.__retryCount = originalRequest.__retryCount || 0;
originalRequest.__retryCount++;
// 如果重试次数超过 3 次,则不再重试,直接跳转到登录页
if (originalRequest.__retryCount >= 3) {
that.clearToken();
// 跳转到登录页,并带上当前页面的路径作为重定向参数
window.location.href = that.__root + '/login?redirect=' + window.location.pathname;
// 拒绝原始请求的 Promise停止后续处理
return Promise.reject(error);
}
return fetch(url, options);
// 如果当前正在进行令牌刷新
if (isRefreshing) {
// 将当前失败的请求添加到队列中,等待新令牌
return new Promise(resolve => {
failedQueue.push({ resolve, reject: (err) => { throw err; } });
}).then(token => {
// 刷新成功后,使用新令牌更新请求头
originalRequest.headers.Authorization = `Bearer ${token}`;
// 重新发送原始请求
return instance(originalRequest);
}).catch(err => {
// 如果队列中的请求被拒绝,则抛出错误
return Promise.reject(err);
});
}
// 如果没有正在进行令牌刷新,则设置标志为 true开始刷新
isRefreshing = true;
try {
// 发送请求来刷新令牌
await that.refreshToken();
const newToken = that.getToken();
// 更新原始请求的 Authorization 头为新的令牌
originalRequest.headers.Authorization = `Bearer ${newToken}`;
// 处理等待队列中的所有请求,用新的令牌重新发送
processQueue(null, newToken);
// 重新发送最初导致 401 错误的请求
return instance(originalRequest);
} catch (refreshError) {
// 如果刷新令牌本身也失败了 (例如refresh token 已过期)
// 清除本地令牌
that.clearToken();
// 拒绝等待队列中的所有请求
processQueue(refreshError);
that.logout();
// 拒绝原始请求的 Promise
return Promise.reject(refreshError);
} finally {
// 无论成功或失败,最后都要将刷新标志重置为 false
isRefreshing = false;
}
}
// 对于其他类型的错误,或不是需要重试的 401 错误,直接拒绝 Promise
return Promise.reject(error);
});
}
}

Loading…
Cancel
Save