一步步实现微服务权限管理系统(6)
前言
- 前端参考了很多框架,可谓百花齐放,但很多封装过剩,不利于学习和应用,最终我选择了 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin)
- 后端我将使用 go-zero 来带领大家一步步实现一个权限管理系统
- 本系列项目存放在 purezeroadmin 中,每一部分我都将打tag,并保证每个tag能正常运行。请多点赞和评论。
- 后面示例均为
purezeroadmin
项目为例,你们可以根据需要自建工程来进行试验。均采用vscode
进行试验。 go-zero
常用命令我将放入其对应的makefile
文件中。
待解决
当前jwt都是模拟的,本节加上处理逻辑
后端修改
添加jwt认证
- 修改
user-api/api/user.api
删除了 refreshToken
作为json参数传入,前端采用 jwt 规范传入 refreshToken
type (
UserRefreshTokenReq {
}
UserRefreshTokenResp {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
Expires string `json:"expires"`
}
)
@server (
jwt: Auth // 开启 jwt 认证
)
service user-api {
@doc "刷新token"
@handler UserRefreshToken
post /api/refresh-token (UserRefreshTokenReq) returns (UserRefreshTokenResp)
}
...
@server (
jwt: Auth // 开启 jwt 认证
)
service user-api {
@doc "获取路由"
@handler userRouter
get /api/get-async-routes (UserRouterReq) returns ([]RouterData)
}
user-api/etc/user-api.yaml
# jwt认证相关
Auth:
AccessSecret: "purezeroadminxxx"
AccessExpire: 86400
user-api/internal/config/config.go
type Config struct {
rest.RestConf
Auth struct { // JWT 认证需要的密钥和过期时间配置
AccessSecret string
AccessExpire int64 // 单位为秒
}
}
- 重新生成代码
goctl api go --api user-api/api/user.api --dir user-api --home template
修改jwt相关逻辑
utls/jwtutil/jwt.go
新建工具类函数
package jwtutil
import (
"github.com/golang-jwt/jwt/v4"
)
func GetToken(secretKey string, iat, seconds int64, payload map[string]any) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
for k, v := range payload {
claims[k] = v
}
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
user-api/global/ctxdata.go
增加jwt payload的key
package global
const CtxJwtUserIDKey = "userID"
user-api/internal/logic/userloginlogic.go
修改jwt相关逻辑
func (l *UserLoginLogic) UserLogin(req *types.UserLoginReq) (resp *types.UserLoginResp, err error) {
var userID int64
resp = &types.UserLoginResp{
Username: req.UserName,
}
if req.UserName == "admin" && req.Password == "admin123" {
userID = 1
resp.Avatar = "https://avatars.githubusercontent.com/u/44761321"
resp.Nickname = "小铭"
resp.Roles = []string{"admin"}
resp.Permissions = []string{"*:*:*"}
} else if req.UserName == "common" && req.Password == "common123" {
userID = 2
resp.Avatar = "https://avatars.githubusercontent.com/u/52823142"
resp.Nickname = "小林"
resp.Roles = []string{"common"}
resp.Permissions = []string{"permission:btn:add", "permission:btn:edit"}
} else {
return nil, errors.New("登陆失败")
}
tNow := time.Now()
tExpire := tNow.Add(time.Second * time.Duration(l.svcCtx.Config.Auth.AccessExpire))
mPayload := map[string]any{
global.CtxJwtUserIDKey: userID,
}
accessToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix(), mPayload)
if err != nil {
return nil, err
}
refreshToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix()+86400, mPayload)
if err != nil {
return nil, err
}
resp.AccessToken = accessToken
resp.RefreshToken = refreshToken
resp.Expires = tExpire.Format("2006/01/02 15:04:05")
return resp, nil
}
user-api/helper/helper.go
jwt获取userID
package helper
import (
"backend/user-api/global"
"context"
"fmt"
)
func GetUserIDFromContext(ctx context.Context) (int64, error) {
uid, ok := ctx.Value(global.CtxJwtUserIDKey).(json.Number)
if !ok {
return 0, fmt.Errorf("jwt has no userID")
}
return uid.Int64()
}
user-api/internal/logic/userrefreshtokenlogic.go
修改jwt相关逻辑
func (l *UserRefreshTokenLogic) UserRefreshToken(req *types.UserRefreshTokenReq) (resp *types.UserRefreshTokenResp, err error) {
userID, err := helper.GetUserIDFromContext(l.ctx)
if err != nil {
return nil, err
}
tNow := time.Now()
tExpire := tNow.Add(time.Second * time.Duration(l.svcCtx.Config.Auth.AccessExpire))
mPayload := map[string]any{
global.CtxJwtUserIDKey: userID,
}
accessToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix(), mPayload)
if err != nil {
return nil, err
}
refreshToken, err := jwtutil.GetToken(l.svcCtx.Config.Auth.AccessSecret, tNow.Unix(), tExpire.Unix()+86400, mPayload)
if err != nil {
return nil, err
}
return &types.UserRefreshTokenResp{
AccessToken: accessToken,
RefreshToken: refreshToken,
Expires: tExpire.Format("2006/01/02 15:04:05"),
}, nil
}
user-api/internal/logic/userrouterlogic.go
路由common用户删除页面权限
func (l *UserRouterLogic) UserRouter(req *types.UserRouterReq) (resp []*types.RouterData, err error) {
userID, err := helper.GetUserIDFromContext(l.ctx)
if err != nil {
return nil, err
}
if userID == 1 {
return []*types.RouterData{
{
Path: "/permission/page/index",
Name: "PermissionPage",
Meta: types.Meta{
Title: "页面权限",
Roles: []string{"admin", "common"},
},
},
{
Path: "/permission/button",
Meta: types.Meta{
Title: "按钮权限",
Roles: []string{"admin", "common"},
},
Children: []types.RouterData{
{
Path: "/permission/button/router",
Component: "permission/button/index",
Name: "PermissionButtonRouter",
Meta: types.Meta{
Title: "路由返回按钮权限",
Auths: []string{
"permission:btn:add",
"permission:btn:edit",
"permission:btn:delete",
},
},
},
{
Path: "/permission/button/login",
Component: "permission/button/perms",
Name: "PermissionButtonLogin",
Meta: types.Meta{
Title: "登录返回按钮权限",
},
},
},
},
}, nil
}
return []*types.RouterData{
{
Path: "/permission/page/index",
Name: "PermissionPage",
Meta: types.Meta{
Title: "页面权限",
Roles: []string{"admin"},
},
},
{
Path: "/permission/button",
Meta: types.Meta{
Title: "按钮权限",
Roles: []string{"admin", "common"},
},
Children: []types.RouterData{
{
Path: "/permission/button/router",
Component: "permission/button/index",
Name: "PermissionButtonRouter",
Meta: types.Meta{
Title: "路由返回按钮权限",
Auths: []string{
"permission:btn:add",
"permission:btn:edit",
"permission:btn:delete",
},
},
},
{
Path: "/permission/button/login",
Component: "permission/button/perms",
Name: "PermissionButtonLogin",
Meta: types.Meta{
Title: "登录返回按钮权限",
},
},
},
},
}, nil
}
前端修改
src/views/login/index.vue
登陆采用表单密码
const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
loading.value = true;
useUserStoreHook()
.loginByUsername({ username: ruleForm.username, password: ruleForm.password })
.then(res => {
if (res.code === 0) {
// 获取后端路由
return initRouter().then(() => {
router.push(getTopMenu(true).path).then(() => {
message("登录成功", { type: "success" });
});
});
} else {
message("登录失败", { type: "error" });
}
})
.finally(() => (loading.value = false));
}
});
};
src/utils/http/index.ts
白名单加前缀/api
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
const whiteList = ["/api/refresh-token", "/api/login"];
-
refreshToken 传递jwt
-
src/api/user.ts
/** 刷新`token` */
export const refreshTokenApi = (refreshToken: string) => {
return http.request<RefreshTokenResult>(
"post",
"/api/refresh-token",
{},
{
headers: {
Authorization: `Bearer ${refreshToken}`
}
}
);
};
src/store/modules/user.ts
/** 刷新`token` */
async handRefreshToken(refreshToken) {
return new Promise<RefreshTokenResult>((resolve, reject) => {
refreshTokenApi(refreshToken)
.then(data => {
if (data) {
setToken(data.data);
resolve(data);
}
})
.catch(error => {
reject(error);
});
});
}
src/utils/http/index.ts
// token过期刷新
useUserStoreHook()
.handRefreshToken(data.refreshToken)
.then(res => {
const token = res.data.accessToken;
config.headers["Authorization"] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token));
PureHttp.requests = [];
})
.finally(() => {
PureHttp.isRefreshing = false;
});
测试
- 可以将
AccessExpire
配置修改为 10s, 登陆后刷新,观察是否调用 refreshToken 逻辑 - 测试
common
用户,可观察期没有页眉权限
tag版本
purezeroadmin
项目下
git checkout v1.5.0
接下来
数据持久化