java转go——第二阶段

前言

java转go第二阶段,本阶段时间一个星期,目标Gin的路由分组、中间件、请求验证、JWT鉴权实现功能,完成一个系统实现注册/登录/权限控制、集成MySQL存储用户数据、使用Redis缓存热点数据。

第一阶段学习记录
自己写的项目源码地址

一、学习经过

  • 阅读博客Golang gin框架,在集成gorm以前的讲述很详细很nice,后面的感觉还没写完有待完善。

  • 借助豆包了解了一下请求验证、JWT鉴权

  • 跟着大庆哥的视频了解了一下go的mvc架构这么搭,不过感觉他这个也没特别严谨的工程化但目前入门写demo也是够了,另外他用的sqlite数据库,我给改成MySQL了,视频

  • gorm中文文档硬看有些枯燥

  • 借助豆包实现具备权限认证的登录系统,豆包生成的前端一直报错!!!但我又看不懂前端,这次结束调整计划先去学前端,go的微服务先放放!!!调了一整天也没解决前端页面跳转时请求头未携带token的问题,决定系统学习一下授权认证,再无法解决就立即开始学前端。

  • 爽,调通了。看了一些视频和博客以后开始对代码一句句读,后端无逻辑错误,最后发现后端的控制台请求了两次一次200,一次401,定位到是前端重复发送了请求且第二次请求没把token带上,删了一部分代码后就通了。期间对我帮助最大的是这个博客在 Go 项目中实现 JWT 用户认证与续期机制

  • 通过接入casbin,实现了权限管理,参考了官方文档一篇博客

  • 集成了redis,用ping进行了新增和查询的测试

    二、 学习整理

    1. token认证

    先来看看token的基本实现思路在这里插入图片描述
    同时来了解一下token的结构:
    Header(算法、类型)、Payload(用户 ID、权限、过期时间)、Signature(防篡改)。
    最后看看实现细节:
    用到的外部包:”github.com/golang-jwt/jwt/v4”
    这里我只进行了最简单的jwt token认证,还有更高级的refresh token等以后再进一步学习

后端处理流程auth_handler.go:

func Login(c *gin.Context) {
// 将前端请求体里的数据绑定到 loginRequest 结构体
    var loginRequest struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }
    if err := c.ShouldBindJSON(&loginRequest); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数格式错误"})
        return
    }
    // 从数据库查询用户信息
    if user == nil || !utils.ComparePasswords(user.Password, loginRequest.Password) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
        return
    }
    //比对用户信息,主要是对前端传入的密码加密后进行比对
    if user == nil || !utils.ComparePasswords(user.Password, loginRequest.Password) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
        return
    }
    // 生成令牌
    token, err := utils.GenerateToken(user.Username, user.Role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "生成令牌失败"})
        return
    }
    // 设置令牌到响应头中
    c.Header("Authorization", "Bearer "+token)
    // 返回令牌给客户端
    c.JSON(http.StatusOK, gin.H{"token": token})
}

令牌的生成与校验token_utils:


// 加载.env文件
func init() {
    err := godotenv.Load()
    if err != nil {
        log.Fatalf("Error loading .env file: %v", err)
    }
}

// SecretKey 从.env文件中获取JWT_SECRET
var SecretKey = []byte(os.Getenv("JWT_SECRET"))

// GenerateToken 生成 JWT 令牌
func GenerateToken(username, role string) (string, error) {
    // 创建声明
    claims := jwt.MapClaims{
        "username": username,
        "role":     role,
        "exp":      time.Now().Add(time.Hour * 24).Unix(), // 令牌过期时间设置为 24 小时后
    }

    // 创建一个新的令牌对象,指定签名方法为 HS256
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    // 使用密钥对令牌进行签名并获取字符串形式的令牌
    tokenString, err := token.SignedString(SecretKey)
    if err != nil {
        return "", err
    }

    return tokenString, nil
}

查看源码
这是创建一个令牌包括header和payload,此时还未进行加密

func NewWithClaims(method SigningMethod, claims Claims) *Token {
    return &Token{
        Header: map[string]interface{}{
            "typ": "JWT",
            "alg": method.Alg(),
        },
        Claims: claims,
        Method: method,
    }
}

这里开始对token进行加密

func (t *Token) SignedString(key interface{}) (string, error) {
    var sig, sstr string
    var err error
    if sstr, err = t.SigningString(); err != nil {
        return "", err
    }
    if sig, err = t.Method.Sign(sstr, key); err != nil {
        return "", err
    }
    return strings.Join([]string{sstr, sig}, "."), nil
}

对header和payload进行HS64加密,对签名通过接口的方式使用设定的加密方法进行加密,我这里用的HS256

func (t *Token) SigningString() (string, error) {
    var err error
    var jsonValue []byte

    if jsonValue, err = json.Marshal(t.Header); err != nil {
        return "", err
    }
    header := EncodeSegment(jsonValue)

    if jsonValue, err = json.Marshal(t.Claims); err != nil {
        return "", err
    }
    claim := EncodeSegment(jsonValue)

    return strings.Join([]string{header, claim}, "."), nil
}
func EncodeSegment(seg []byte) string {
    return base64.RawURLEncoding.EncodeToString(seg)
}
type SigningMethod interface {
    Verify(signingString, signature string, key interface{}) error // Returns nil if signature is valid
    Sign(signingString string, key interface{}) (string, error)    // Returns encoded signature or error
    Alg() string                                                   // returns the alg identifier for this method (example: 'HS256')
}

生成令牌的说完了,来看看后端如何拿到前端传回来的token进行校验
设置需要认证的路由进行分组,设置中间件进行拦截routes.go:

// 需要认证的路由
    protectedRoutes := r.Group("/")
    protectedRoutes.Use(middleware.AuthMiddleware())
    protectedRoutes.GET("/dashboard", handlers.Dashboard)

前端代码
对前端不太熟悉就不多解读了,大概逻辑就是前端/login请求拿到后端放到响应体的token,然后将token设置到请求头fetch跳转到需要验证tokrn的/dashboard页面

document.addEventListener('DOMContentLoaded', function () {
    const loginForm = document.getElementById('loginForm');
    const registerForm = document.getElementById('registerForm');
    const logoutButton = document.getElementById('logoutButton');
    const welcomeMessage = document.getElementById('welcomeMessage');
    const errorMessage = document.getElementById('errorMessage');

    if (loginForm) {
        loginForm.addEventListener('submit', async (e) => {
            e.preventDefault();
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            const data = {
                username: username,
                password: password
            };
            try {
                const response = await fetch('/login', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                });
                const result = await response.json();
                if (response.ok) {
                    const token = result.token;
                    localStorage.setItem('token', token);
                    console.log('Stored token1:', token); // 打印存储的 token
                    // 直接在当前页面加载仪表盘内容
                    const dashboardResponse = await fetch('/dashboard', {
                        method: 'GET',
                        headers: {
                            'Authorization': `Bearer ${token}`
                        }
                    });
                    console.log('Stored token2:', token); // 打印存储的 token
                    if (dashboardResponse.ok) {
                        const dashboardHtml = await dashboardResponse.text();
                        document.open();
                        document.write(dashboardHtml);
                        document.close();
                    } else {
                        const dashboardError = await dashboardResponse.json();
                        errorMessage.textContent = dashboardError.error;
                        console.log('Stored token3:', token);
                    }
                } else {
                    errorMessage.textContent = result.error;
                    console.log('Stored token4:', token);
                }
            } catch (error) {
                errorMessage.textContent = '请求出错: ' + error.message;
            }
        });
    }

    if (registerForm) {
        registerForm.addEventListener('submit', async (e) => {
            e.preventDefault();
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            const data = {
                username: username,
                password: password
            };
            try {
                const response = await fetch('/register', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                });
                const result = await response.json();
                if (response.ok) {
                    window.location.href = '/login';
                } else {
                    errorMessage.textContent = result.error;
                }
            } catch (error) {
                errorMessage.textContent = '请求出错: ' + error.message;
            }
        });
    }

    if (logoutButton) {
        logoutButton.addEventListener('click', () => {
            localStorage.removeItem('token');
            window.location.href = '/login';
        });
    }

    if (welcomeMessage) {
        const token = localStorage.getItem('token');
        if (token) {
            const payload = JSON.parse(atob(token.split('.')[1]));
            welcomeMessage.textContent = `欢迎,${payload.username}!你是 ${payload.role}。`;
        } else {
            window.location.href = '/login';
        }
    }

    // 移除页面加载时的请求逻辑
    // window.addEventListener('load', async () => {
    //     const token = localStorage.getItem('token');
    //     if (token) {
    //         try {
    //             const response = await fetch('/dashboard', {
    //                 method: 'GET',
    //                 headers: {
    //                     'Authorization': `Bearer ${token}`
    //                 }
    //             });
    //             if (response.ok) {
    //                 const dashboardHtml = await response.text();
    //                 document.open();
    //                 document.write(dashboardHtml);
    //                 document.close();
    //             } else {
    //                 const dashboardError = await response.json();
    //                 errorMessage.textContent = dashboardError.error;
    //             }
    //         } catch (error) {
    //             errorMessage.textContent = '请求出错: ' + error.message;
    //         }
    //     }
    // });
});

通过中间件对页面请求进行处理auth_middleware,中间有一部分逻辑是casbin逻辑认证的后面说

var enforcer *casbin.Enforcer

// SetEnforcer 设置 Casbin 实例
func SetEnforcer(e *casbin.Enforcer) {
    enforcer = e
}

// AuthMiddleware 认证中间件
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从请求头中获取认证令牌
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
            c.Abort()
            return
        }
        // 提取认证令牌
        tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
        token, err := utils.ValidateToken(tokenString)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌解析失败"})
            c.Abort()
            return
        }

        if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
            username := claims["username"].(string)
            role := claims["role"].(string)
            c.Set("username", username)
            c.Set("role", role)

            // 进行 Casbin 权限验证
            obj := c.Request.URL.Path
            act := c.Request.Method
            ok, err := enforcer.Enforce(username, obj, act)
            if err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "权限验证出错" + err.Error()})
                c.Abort()
                return
            }
            if !ok {
                c.JSON(http.StatusForbidden, gin.H{"error": "权限不足"})
                c.Abort()
                return
            }

            c.Next()
        } else {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "令牌验证未通过"})
            c.Abort()
            return
        }
    }
}

验证 JWT 令牌token_utils.go

// ValidateToken 验证 JWT 令牌
func ValidateToken(tokenString string) (*jwt.Token, error) {
    // 解析令牌
    return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // 验证签名方法是否为预期的 HS256
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, errors.New("签名方法错误")
        }
        // 返回用于验证的密钥
        return SecretKey, nil
    })
}

查看源码:
这个的源码比较长,我只贴关键部分,有兴趣的可以自己去看看详细的源码

入口是Parse函数,传入了一个字符串类型的token以及一个函数以及一些杂七杂八的设置参数

func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
    return NewParser(options...).Parse(tokenString, keyFunc)
}

哪些杂七杂八的参数会在NewParser里面进行解析并返回一个Parser对象,而我们没有那些参数,暂时不做展开

func NewParser(options ...ParserOption) *Parser {
    p := &Parser{}

    // loop through our parsing options and apply them
    for _, option := range options {
        option(p)
    }

    return p
}

通过Parser调用它的方法Parse传入参数tokenString 和我们自定义的函数

func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
    return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc)
}

ParseWithClaims函数主要通过调用ParseUnverified函数将tokenString 按照.分成三个部分,然后反序列化为header和payload存入token进行返回

func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error){
    token, parts, err := p.ParseUnverified(tokenString, claims)

将token传给我们自定义的函数,通过断言检测解码出来的签名算法是否一致,一致则返回密钥进行比对,一致然后一路将*jwt.Token类型的token返回出去,中间件发现令牌无误就放行通过

if key, err = keyFunc(token); err != nil {
        // keyFunc returned an error
        if ve, ok := err.(*ValidationError); ok {
            return token, ve
        }
        return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}
    }

//自定义的函数
func(token *jwt.Token) (interface{}, error) {
        // 验证签名方法是否为预期的 HS256
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, errors.New("签名方法错误")
        }
        // 返回用于验证的密钥
        return SecretKey, nil
    }

2 casbin进行权限认证

中文文档
用到的外部包:
“github.com/casbin/casbin/v2”
“github.com/casbin/gorm-adapter/v3”

对于casbin我只是一个初学者 只学会了它的一种应用,有机会后面会在对它进行深度学习

首先,casbin是通过配置文件来设置访问控制模式。它有两个配置文件,model.conf和policy.csv。 其中,model.conf存储了访问模型,policy.csv存储了特定的用户权限配置,不过一般由于文件太大会使用数据库存储。
如果用数据库进行存储,那么就需要通过适配器来读取policy.csv,适配器官网有对应的包,根据你的数据库来选择使用。
当有了这两个配置文件就可以创建enforcer,casbin的执行器
我们这里在casbin.go文件下对casbin进行了初始化,然后将这个执行器传入了权限认证的中间件中方便进行操作

func InitCasbin(db *gorm.DB) {
    // 初始化casbin,创建数据表
    a, err := gormadapter.NewAdapterByDBUseTableName(db, "", "casbin_rule")
    if err != nil {
        log.Fatalf("无法初始化casbin gormadapter: %v", err)
    }
    e, err = casbin.NewEnforcer("casbin/rbac_model.conf", a)
    if err != nil {
        log.Fatalf("无法初始化casbin enforcer: %v", err)
    }
    err = e.LoadPolicy()
    if err != nil {
        log.Fatalf("无法加载casbin策略: %v", err)
    }
    middleware.SetEnforcer(e)
}

auth_middleware.go中的验证逻辑代码,完整代码可以看上面或者,gitee:

// 进行 Casbin 权限验证
            obj := c.Request.URL.Path
            act := c.Request.Method
            ok, err := enforcer.Enforce(username, obj, act)
            if err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "权限验证出错" + err.Error()})
                c.Abort()
                return
            }
            if !ok {
                c.JSON(http.StatusForbidden, gin.H{"error": "权限不足"})
                c.Abort()
                return
            }

本来是准备看看它的实现源码的,但封装的太深了,以后有机会再说吧,下面结合conf文件来讲讲我的理解:
整体rabc_model.conf

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act, eft

[role_definition]
g = _,_

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = (g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act) || g(r.sub,"admin")

在这里插入图片描述

[request_definition]
r = sub, obj, act

这里对请求进行了定义,一个请求必须为一个对象(sub)通过一个动作(act)访问了一个资源(obj),比如r=alice,/login,GET alice通过get请求访问了login页面
在这里插入图片描述

[policy_definition]
p = sub, obj, act, eft

这里对策略进行了定义,一个策略必须为一个对象(sub)通过一个动作(act)访问了一个资源(obj)有怎样的结果(eft),比如p=alice,/login,GET,allow alice通过get请求访问了login页面结果为allow
在这里插入图片描述

[role_definition]
g = _,_

比如g, alice, data2_admin
这的意思是alice 是角色 data2_admin的一个成员,随意alice也拥有data2_admin的权限
在这里插入图片描述

[matchers]
m = (g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act) || g(r.sub,"admin")

这里设定了匹配规则,对角色和策略进行匹配
在这里插入图片描述

[policy_effect]
e = some(where (p.eft == allow))

这句的意思是
some → “至少一个” 或 “存在一个”
where (p.eft == allow) → “条件是策略的效果(eft)等于允许(allow)”
找到至少一个策略,其中效果(effect)是允许(allow)的

当调用enforcer.Enforce,首先会解析为符合请求定义的请求格式,然后根据匹配器将解析后的请求与 policy.csv 中的策略规则进行匹配。在匹配到一条或多条策略规则之后,需要依据 model.conf 中的策略效果定义来决定最终的决策结果。根据策略匹配和效果评估的结果,Enforce 方法会返回 ok(布尔值,表示是否允许访问)和 err(错误信息,如果在处理过程中出现错误)。

总结

顺利完成第二阶段任务,用时3天,期间发现对前端知识欠缺较为严重,调整原计划先对前端基础进行学习。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!