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 协议》,转载必须注明作者和本文链接
推荐文章: