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 用户认证与续期机制 
- 集成了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 协议》,转载必须注明作者和本文链接
 
           oahnuyBZC 的个人博客
 oahnuyBZC 的个人博客
         
             
           
           关于 LearnKu
                关于 LearnKu
               
                     
                     
                     粤公网安备 44030502004330号
 粤公网安备 44030502004330号 
 
推荐文章: