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) [Demo](https://oa.veypi.com)
## 使用方法
## Auth ## 引入
/init.go
code用来兑换本身应用token和兑换其他应用付出权限token ```go
oa code 可以用来生成其他code和刷新本身code
### 依赖库
```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" "time"
"github.com/veypi/OneAuth/cfg" "github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/errs"
"github.com/veypi/OneAuth/libs/auth" "github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/models" "github.com/veypi/OneAuth/models"
"github.com/veypi/OneBD/rest" "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) { func appKey(x *rest.X) (any, error) {
id := x.Params.Get("app_id") id := x.Params.Get("app_id")
if id == "" { if id == "" {
return nil, errs.ArgsInvalid.WithStr("missing app_id") return nil, rest.ErrArgMissing.WithArgs("app_id")
} }
data := &models.App{} data := &models.App{}
data.ID = id data.ID = id
@ -65,8 +64,9 @@ func appList(x *rest.X) (any, error) {
models.App models.App
UserStatus string `json:"user_status"` UserStatus string `json:"user_status"`
}, 0, 10) }, 0, 10)
token, err := auth.CheckJWT(x) tokenAny, err := auth.CheckJWT(x)
if err == nil { if err == nil {
token := tokenAny.(*auth.Claims)
uid := token.UID uid := token.UID
query := cfg.DB().Table("apps").Select("apps.*,app_users.status user_status") query := cfg.DB().Table("apps").Select("apps.*,app_users.status user_status")
if opts.Name != nil { if opts.Name != nil {

@ -68,7 +68,7 @@ func tokenPost(x *rest.X) (any, error) {
claim.UID = refresh.UID claim.UID = refresh.UID
claim.Name = refresh.Name claim.Name = refresh.Name
claim.Icon = refresh.Icon 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 typ == "app" {
if refresh.AID == aid { if refresh.AID == aid {
// refresh token // refresh token
@ -113,7 +113,7 @@ func tokenPost(x *rest.X) (any, error) {
claim.UID = refresh.UID claim.UID = refresh.UID
claim.Name = refresh.Name claim.Name = refresh.Name
claim.Icon = refresh.Icon 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{ claim.Access = auth.Access{
{Name: "fs", TID: "/", Level: auth.Do}, {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)) ncode, err := utils.AesDecrypt([]byte(data.Code), code, []byte(data.Salt))
if err != nil || ncode != data.ID { 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 { if opts.Nickname != nil {
data.Nickname = *opts.Nickname data.Nickname = *opts.Nickname

@ -7,11 +7,18 @@
package cfg package cfg
import "time"
type Options struct { type Options struct {
DSN string `json:"dsn"` // Data Source Name DSN string `json:"dsn"` // Data Source Name
DB string `json:"db"` // DB type: mysql, postgres, sqlite DB string `json:"db"` // DB type: mysql, postgres, sqlite
ID string `json:"id"` ID string `json:"id"`
Key string `json:"key"` 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 package errs
import ( import "github.com/veypi/OneBD/rest"
"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}
}
var ( var (
ArgsInvalid = New(40001, "args invalid") AuthNotFound = rest.NewError("auth not found").WithCode(40100)
DuplicateKey = New(40002, "duplicate key") AuthFailed = rest.NewError("auth failed").WithCode(40101)
AuthNotFound = New(40100, "auth not found") AuthExpired = rest.NewError("auth expired").WithCode(40102)
AuthFailed = New(40101, "auth failed") AuthInvalid = rest.NewError("auth invalid").WithCode(40103)
AuthExpired = New(40102, "auth expired") AuthNoPerm = rest.NewError("auth no permission").WithCode(40104)
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")
) )

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

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

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

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

@ -107,7 +107,7 @@
access_api = { access_api = {
next: async (page, size) => { next: async (page, size) => {
console.log(id) 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> </script>

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

@ -84,15 +84,6 @@
rows = [] 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 = [] user_role_data = []
selected = {} selected = {}
show_user = async (row) => { show_user = async (row) => {
@ -105,7 +96,7 @@
} }
user_role_api = { user_role_api = {
next: async (page, size) => { 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, cancel: true,
persistent: true persistent: true
}).onOk(() => { }).onOk(() => {
api.user.reset(id).then(() => { $api.user.reset(id).then(() => {
msg.Info('重置成功 ') msg.Info('重置成功 ')
}).catch((e) => { }).catch((e) => {
msg.Warn('失败 ' + e) msg.Warn('失败 ' + e)
@ -132,11 +123,11 @@
history.back() history.back()
return return
} }
api.Get(`/api/app/${id}`) $api.Get(`/api/app/${id}`)
.then((data) => { .then((data) => {
Object.assign(app, data) Object.assign(app, data)
document.title = `${app.name} - 项目主页` document.title = `${app.name} - 项目主页`
api.Get(`/api/user/`).then((e) => { $api.Get(`/api/user/`).then((e) => {
rows = e rows = e
console.log(e) console.log(e)
}) })

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

@ -132,7 +132,7 @@
loadUserData = async () => { loadUserData = async () => {
try { try {
isLoading = true; isLoading = true;
const response = await api.Get("/api/user/" + user.id); const response = await $api.Get("/api/user/" + user.id);
if (response) { if (response) {
user = { user = {
id: response.id, 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) { if (response) {
successMessage = "个人信息更新成功!"; successMessage = "个人信息更新成功!";

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>oa</title> <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/tailwind/tailwind.min.css" rel="stylesheet">
<link href="/assets/libs/animate/animate.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"> <link href="/assets/libs/font-awesome/css/all.min.css" rel="stylesheet">
@ -15,5 +15,17 @@
<page-404></page-404> <page-404></page-404>
</vrouter> </vrouter>
</body> </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> </html>

@ -6,10 +6,14 @@
*/ */
class TokenService { class TokenService {
__root = ''
constructor() { constructor() {
this.tokenKey = 'access'; this.tokenKey = 'access';
this.refreshTokenKey = 'refresh'; this.refreshTokenKey = 'refresh';
} }
setRoot(root) {
this.__root = root;
}
setToken(token) { setToken(token) {
localStorage.setItem(this.tokenKey, token); localStorage.setItem(this.tokenKey, token);
@ -27,7 +31,7 @@ class TokenService {
return localStorage.getItem(this.refreshTokenKey); return localStorage.getItem(this.refreshTokenKey);
} }
clearTokens() { clearToken() {
localStorage.removeItem(this.tokenKey); localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey); localStorage.removeItem(this.refreshTokenKey);
} }
@ -54,24 +58,19 @@ class TokenService {
} }
return this.__cache return this.__cache
} }
logout(root) { logout() {
console.log(this) this.clearToken();
root = root || this.__root location.href = this.__root + '/login?redirect=' + window.location.pathname;
this.clearTokens();
location.href = root + '/login?redirect=' + window.location.pathname;
} }
__root = '' async refreshToken() {
async refreshToken(root) {
root = root || this.__root
this.__root = root
const refreshToken = this.getRefreshToken(); const refreshToken = this.getRefreshToken();
if (!refreshToken) { if (!refreshToken) {
// this.logout() this.logout()
return; return;
} }
try { try {
let data = await fetch(root + '/api/token', { let data = await fetch(this.__root + '/api/token', {
method: 'post', method: 'post',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh: refreshToken }) body: JSON.stringify({ refresh: refreshToken })
@ -79,11 +78,11 @@ class TokenService {
if (data.code === 0) { if (data.code === 0) {
this.setToken(data.data); this.setToken(data.data);
} else { } else {
this.clearTokens() this.clearToken()
} }
} catch (e) { } catch (e) {
console.error('Token刷新失败:', e); console.error('Token刷新失败:', e);
this.clearTokens() this.clearToken()
// logout(); // logout();
} }
} }
@ -94,21 +93,122 @@ class TokenService {
const currentTime = Date.now() / 1000; const currentTime = Date.now() / 1000;
return decoded.exp < currentTime; return decoded.exp < currentTime;
} }
fetch() { wrapAxios(instance) {
let that = this
return (url, options) => { // 定义一个标志,用于防止在刷新令牌时发送多个刷新请求
const token = that.getToken(); let isRefreshing = false;
if (token) { // 定义一个队列,用于存储在刷新令牌期间失败的请求
if (!options) { let failedQueue = [];
options = {};
/**
* 处理等待队列中的请求
* 刷新令牌成功后将队列中的请求重新发送刷新失败则拒绝这些请求
* @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