Skip to content

一步步实现微服务权限管理系统(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)
}
- 增加Auth配置 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

接下来

数据持久化

Comments