26 changed files with 496 additions and 151 deletions
@ -1,7 +1,18 @@ |
|||||||
|
|
||||||
[auth] |
[http] |
||||||
ignoreUrl = [ |
addr = ":9999" |
||||||
"/ws", |
|
||||||
"/auth/captcha" |
|
||||||
] |
|
||||||
|
|
||||||
|
[auth] |
||||||
|
aesTokenKey = "9Nz3Y6DES3msAFndz4QsJAECUIFkf+KKaRa+jRnNALk=" |
||||||
|
ignoreUrl = [ |
||||||
|
"/ws", |
||||||
|
"/auth/captcha", |
||||||
|
"/auth/authorize", |
||||||
|
"/user/findByName", |
||||||
|
] |
||||||
|
# 需要输入图形验证码的url |
||||||
|
captchaUrl = [ |
||||||
|
"/auth/authorize" |
||||||
|
] |
||||||
|
[sqlite] |
||||||
|
dbPath = "etc/texas-poker.db" |
||||||
|
|||||||
@ -0,0 +1,50 @@ |
|||||||
|
package conf |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"github.com/BurntSushi/toml" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
confPath string |
||||||
|
Conf *Config |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
// go run xx -conf=xx.toml
|
||||||
|
flag.StringVar(&confPath, "conf", "etc/config.toml", "default config path.") |
||||||
|
|
||||||
|
Conf = Default() |
||||||
|
_, err := toml.DecodeFile("etc/config.toml", Conf) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
// fmt.Printf("%v", Conf)
|
||||||
|
} |
||||||
|
|
||||||
|
func Default() *Config { |
||||||
|
return &Config{ |
||||||
|
Auth: &Auth{ |
||||||
|
IgnoreUrl: []string{}, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type Config struct { |
||||||
|
Auth *Auth |
||||||
|
Http *Http |
||||||
|
Sqlite *Sqlite |
||||||
|
} |
||||||
|
|
||||||
|
type Auth struct { |
||||||
|
AesTokenKey string |
||||||
|
IgnoreUrl []string |
||||||
|
} |
||||||
|
|
||||||
|
type Http struct { |
||||||
|
Addr string |
||||||
|
} |
||||||
|
|
||||||
|
type Sqlite struct { |
||||||
|
DbPath string |
||||||
|
} |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
package dao |
||||||
|
|
||||||
|
import ( |
||||||
|
"gorm.io/driver/sqlite" |
||||||
|
"gorm.io/gorm" |
||||||
|
"texas-poker-bk/internal/conf" |
||||||
|
) |
||||||
|
|
||||||
|
var Dao *Persistent |
||||||
|
|
||||||
|
type Persistent struct { |
||||||
|
Sqlite *gorm.DB |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
Dao = &Persistent{ |
||||||
|
Sqlite: newSqlite(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func newSqlite() (db *gorm.DB) { |
||||||
|
db, err := gorm.Open(sqlite.Open(conf.Conf.Sqlite.DbPath), &gorm.Config{ |
||||||
|
QueryFields: true, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
return db |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
package entity |
||||||
|
|
||||||
|
import ( |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Id int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||||
|
type Model struct { |
||||||
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` |
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` |
||||||
|
} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
package entity |
||||||
|
|
||||||
|
type User struct { |
||||||
|
Model |
||||||
|
Id int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` |
||||||
|
Username string `gorm:"column:username" json:"username"` // comment:用户名
|
||||||
|
Password string `gorm:"column:password" json:"password"` // comment:密码
|
||||||
|
Nickname string `gorm:"column:nickname" json:"nickname"` // comment:昵称
|
||||||
|
Version int64 `gorm:"column:version" json:"version"` // comment:更新版本锁
|
||||||
|
} |
||||||
|
|
||||||
|
func (u *User) TableName() string { |
||||||
|
return "t_user" |
||||||
|
} |
||||||
@ -1,12 +0,0 @@ |
|||||||
package model |
|
||||||
|
|
||||||
type Message struct { |
|
||||||
T string `json:"t"` // action 时间主题
|
|
||||||
Ms int64 `json:"ms"` // 毫秒时间戳
|
|
||||||
D []byte `json:"d"` // 消息数据
|
|
||||||
} |
|
||||||
|
|
||||||
// Identity 身份认证消息
|
|
||||||
type Identity struct { |
|
||||||
Token string `json:"t"` |
|
||||||
} |
|
||||||
@ -0,0 +1,5 @@ |
|||||||
|
package message |
||||||
|
|
||||||
|
// Alert 弹窗消息
|
||||||
|
type Alert struct { |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
package message |
||||||
|
|
||||||
|
// ReqIdentity 身份认证消息
|
||||||
|
type ReqIdentity struct { |
||||||
|
Token string `json:"t"` |
||||||
|
} |
||||||
|
|
||||||
|
type ResIdentity struct { |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
package message |
||||||
|
|
||||||
|
type Message struct { |
||||||
|
T string `json:"t"` // action 时间主题
|
||||||
|
Ms int64 `json:"ms"` // 毫秒时间戳
|
||||||
|
D []byte `json:"d"` // 消息数据
|
||||||
|
} |
||||||
|
|
||||||
|
type Res struct { |
||||||
|
Code int32 `json:"code"` |
||||||
|
Msg string `json:"msg"` |
||||||
|
Data any `json:"data"` |
||||||
|
} |
||||||
|
|
||||||
|
func NewRes(code int32, msg string, data any) *Res { |
||||||
|
return &Res{Code: code, Msg: msg, Data: data} |
||||||
|
} |
||||||
|
|
||||||
|
func NewResSuccess(data any) *Res { |
||||||
|
return &Res{Code: 0, Data: data} |
||||||
|
} |
||||||
|
|
||||||
|
func NewResFail(msg string) *Res { |
||||||
|
return &Res{Code: 0, Msg: msg} |
||||||
|
} |
||||||
@ -1,96 +0,0 @@ |
|||||||
package server |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/base64" |
|
||||||
"encoding/json" |
|
||||||
"github.com/gin-gonic/gin" |
|
||||||
"net/http" |
|
||||||
"texas-poker-bk/tool/security" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
SubjectKey = "auth:subject" |
|
||||||
sc = &scEtc{} // 加密配置
|
|
||||||
) |
|
||||||
|
|
||||||
// Subject 认证对象
|
|
||||||
type Subject struct { |
|
||||||
Id int64 `json:"id,omitempty"` // 用户id
|
|
||||||
Name string `json:"name,omitempty"` // 用户名
|
|
||||||
Time int64 `json:"time,omitempty"` // 生成时间戳
|
|
||||||
} |
|
||||||
|
|
||||||
type scEtc struct { |
|
||||||
securityKey string |
|
||||||
keyBytes []byte |
|
||||||
} |
|
||||||
|
|
||||||
func init() { |
|
||||||
sc.securityKey = "9Nz3Y6DES3msAFndz4QsJAECUIFkf+KKaRa+jRnNALk=" |
|
||||||
keyBytes, err := base64.URLEncoding.DecodeString(sc.securityKey) |
|
||||||
if err != nil { |
|
||||||
panic(err) |
|
||||||
} |
|
||||||
sc.keyBytes = keyBytes |
|
||||||
} |
|
||||||
|
|
||||||
// SubjectAuthFilter 认证过滤器
|
|
||||||
func SubjectAuthFilter(ctx *gin.Context) { |
|
||||||
auth := ctx.GetHeader("Authorization") |
|
||||||
if auth == "" { |
|
||||||
// TODO config ignore urls
|
|
||||||
//if isIgnoreUrl(ctx.Request.URL.Path) {
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
ctx.JSON(http.StatusUnauthorized, gin.H{"msg": "请登录后进行操作"}) |
|
||||||
ctx.Abort() |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
defer func() { |
|
||||||
// 捕获aes解析错误
|
|
||||||
if r := recover(); r != nil { |
|
||||||
ctx.JSON(http.StatusUnauthorized, gin.H{"msg": "认证失败,请重新登录"}) |
|
||||||
ctx.Abort() |
|
||||||
} |
|
||||||
}() |
|
||||||
// TODO 这里返回 error,不然后捕获后续执行 handler 的 panic
|
|
||||||
subject := DecodeSubject(auth) |
|
||||||
ctx.Set(SubjectKey, subject) |
|
||||||
} |
|
||||||
|
|
||||||
// EncodeSubject 对subject对象aesCBC加密并返回base64Std编码的 token
|
|
||||||
// subject 客户端对象
|
|
||||||
func EncodeSubject(subject *Subject) string { |
|
||||||
bytes, err := json.Marshal(subject) |
|
||||||
if err != nil { |
|
||||||
panic(err) |
|
||||||
} |
|
||||||
encode := security.EncryptAesCBC(bytes, sc.keyBytes) |
|
||||||
return base64.URLEncoding.EncodeToString(encode) |
|
||||||
} |
|
||||||
|
|
||||||
// DecodeSubject 解码并解密token返回subject对象
|
|
||||||
// auth base64Std 编码的 token
|
|
||||||
func DecodeSubject(auth string) *Subject { |
|
||||||
bytes, err := base64.URLEncoding.DecodeString(auth) |
|
||||||
if err != nil { |
|
||||||
panic(err) |
|
||||||
} |
|
||||||
decode := security.DecryptAesCBC(bytes, sc.keyBytes) |
|
||||||
subject := &Subject{} |
|
||||||
err = json.Unmarshal(decode, subject) |
|
||||||
if err != nil { |
|
||||||
panic(err) |
|
||||||
} |
|
||||||
return subject |
|
||||||
} |
|
||||||
|
|
||||||
// GetSubject 从请求上下文中获取客户端对象, 请求经过了认证过滤器
|
|
||||||
func GetSubject(ctx *gin.Context) *Subject { |
|
||||||
subject, exists := ctx.Get(SubjectKey) |
|
||||||
if !exists { |
|
||||||
return nil |
|
||||||
} |
|
||||||
return subject.(*Subject) |
|
||||||
} |
|
||||||
@ -1,12 +1,151 @@ |
|||||||
package service |
package service |
||||||
|
|
||||||
import "github.com/gin-gonic/gin" |
import ( |
||||||
|
"encoding/base64" |
||||||
|
"encoding/json" |
||||||
|
"github.com/gin-gonic/gin" |
||||||
|
"net/http" |
||||||
|
"texas-poker-bk/internal/conf" |
||||||
|
"texas-poker-bk/internal/model/entity" |
||||||
|
"texas-poker-bk/internal/session" |
||||||
|
"texas-poker-bk/tool/collect" |
||||||
|
"texas-poker-bk/tool/security" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
// Login 登录或注册
|
// http: 登录注册、创建房间
|
||||||
func Login(ctx *gin.Context) { |
// ws: 大厅、房间、房间状态
|
||||||
|
|
||||||
|
var ( |
||||||
|
SubjectKey = "auth:session" |
||||||
|
aesTokenKeyBytes []byte // token aes 加密 key
|
||||||
|
) |
||||||
|
|
||||||
|
// Authorize 登录或注册
|
||||||
|
func Authorize(ctx *gin.Context) { |
||||||
|
username := ctx.PostForm("username") |
||||||
|
password := ctx.PostForm("password") |
||||||
|
|
||||||
|
//username := ctx.Param("username")
|
||||||
|
//password := ctx.Param("password")
|
||||||
|
if username == "" || password == "" { |
||||||
|
ctx.JSON(http.StatusBadRequest, gin.H{"message": "用户名或密码不能为空"}) |
||||||
|
return |
||||||
|
} |
||||||
|
user := userDao.FindUserByName(username) |
||||||
|
if user == nil { |
||||||
|
// 注册
|
||||||
|
user = registerUser(username, password) |
||||||
|
} else { |
||||||
|
// 校验密码
|
||||||
|
if user.Password != password { |
||||||
|
ctx.JSON(http.StatusUnauthorized, gin.H{"message": "用户名或密码有误"}) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
sub := &session.Subject{Id: user.Id, Name: user.Username, Time: time.Now().UnixMilli()} |
||||||
|
token, err := EncodeSubject(sub) |
||||||
|
if err != nil { |
||||||
|
ctx.JSON(http.StatusUnauthorized, gin.H{"message": err.Error()}) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.JSON(http.StatusOK, gin.H{"data": gin.H{"token": token}}) |
||||||
|
} |
||||||
|
|
||||||
|
// 注册用户到DB
|
||||||
|
func registerUser(username string, password string) *entity.User { |
||||||
|
user := &entity.User{ |
||||||
|
Username: username, |
||||||
|
Password: password, |
||||||
|
Nickname: username, |
||||||
|
} |
||||||
|
// TODO error
|
||||||
|
userDao.dao.Save(user) |
||||||
|
return user |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
keyBytes, err := base64.StdEncoding.DecodeString(conf.Conf.Auth.AesTokenKey) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
aesTokenKeyBytes = keyBytes |
||||||
|
} |
||||||
|
|
||||||
|
// SubjectAuthFilter 认证过滤器
|
||||||
|
func SubjectAuthFilter(ctx *gin.Context) { |
||||||
|
|
||||||
|
// 检查是不需要登录的 url
|
||||||
|
// ignore auth urls
|
||||||
|
if collect.IsNotEmptySlice(conf.Conf.Auth.IgnoreUrl) { |
||||||
|
for _, ignoreUrl := range conf.Conf.Auth.IgnoreUrl { |
||||||
|
if ignoreUrl == ctx.Request.URL.Path { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
auth := ctx.GetHeader("Authorization") |
||||||
|
if auth == "" { |
||||||
|
ctx.JSON(http.StatusUnauthorized, gin.H{"message": "请登录后进行操作"}) |
||||||
|
ctx.Abort() |
||||||
|
} |
||||||
|
|
||||||
|
defer func() { |
||||||
|
// 捕获aes解析错误
|
||||||
|
if r := recover(); r != nil { |
||||||
|
ctx.JSON(http.StatusUnauthorized, gin.H{"message": "认证失败,请重新登录"}) |
||||||
|
ctx.Abort() |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
// 这里返回 error,不然后捕获后续执行 handler 的 panic
|
||||||
|
subject, err := DecodeSubject(auth) |
||||||
|
if err != nil { |
||||||
|
ctx.JSON(http.StatusUnauthorized, gin.H{"message": "认证失败,请重新登录"}) |
||||||
|
ctx.Abort() |
||||||
|
} else { |
||||||
|
// 设置token用户到请求上下文
|
||||||
|
ctx.Set(SubjectKey, subject) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// EncodeSubject 对subject对象aesCBC加密并返回base64Std编码的 token
|
||||||
|
// session 客户端对象
|
||||||
|
func EncodeSubject(subject *session.Subject) (string, error) { |
||||||
|
bytes, err := json.Marshal(subject) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
encode, err := security.EncryptAesCBC(bytes, aesTokenKeyBytes) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return base64.URLEncoding.EncodeToString(encode), nil |
||||||
} |
} |
||||||
|
|
||||||
func Register() { |
// DecodeSubject 解码并解密token返回subject对象
|
||||||
|
// auth base64Std 编码的 token
|
||||||
|
func DecodeSubject(auth string) (*session.Subject, error) { |
||||||
|
bytes, err := base64.URLEncoding.DecodeString(auth) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
decode := security.DecryptAesCBC(bytes, aesTokenKeyBytes) |
||||||
|
subject := &session.Subject{} |
||||||
|
err = json.Unmarshal(decode, subject) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return subject, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetSubject 从请求上下文中获取客户端对象, 请求经过了认证过滤器
|
||||||
|
func GetSubject(ctx *gin.Context) *session.Subject { |
||||||
|
subject, exists := ctx.Get(SubjectKey) |
||||||
|
if !exists { |
||||||
|
panic("request subject not exists!") |
||||||
|
} |
||||||
|
return subject.(*session.Subject) |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,48 @@ |
|||||||
|
package service |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"github.com/gin-gonic/gin" |
||||||
|
"gorm.io/gorm" |
||||||
|
"net/http" |
||||||
|
"texas-poker-bk/internal/dao" |
||||||
|
"texas-poker-bk/internal/model/entity" |
||||||
|
) |
||||||
|
|
||||||
|
var userDao *User = &User{dao: dao.Dao.Sqlite} |
||||||
|
|
||||||
|
func init() { |
||||||
|
err := dao.Dao.Sqlite.AutoMigrate(&entity.User{}) |
||||||
|
if err != nil { |
||||||
|
fmt.Println("user table", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type User struct { |
||||||
|
dao *gorm.DB |
||||||
|
} |
||||||
|
|
||||||
|
func FindUserByName(ctx *gin.Context) { |
||||||
|
username := ctx.Param("username") |
||||||
|
user := userDao.FindUserByName(username) |
||||||
|
if user == nil { |
||||||
|
ctx.JSON(http.StatusOK, gin.H{ |
||||||
|
"message": "not found", |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.JSON(http.StatusOK, gin.H{ |
||||||
|
"message": "ok", |
||||||
|
"data": user, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func (u *User) FindUserByName(username string) *entity.User { |
||||||
|
user := &entity.User{} |
||||||
|
tx := u.dao.Model(user).Where("username=?", username).Limit(1).Scan(user) |
||||||
|
// 查询出结果时 tx.RowsAffected 固定=1
|
||||||
|
if tx.RowsAffected == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return user |
||||||
|
} |
||||||
@ -1,4 +1,4 @@ |
|||||||
package subject |
package session |
||||||
|
|
||||||
// NetAccount 已认证的长连接用户
|
// NetAccount 已认证的长连接用户
|
||||||
type NetAccount struct { |
type NetAccount struct { |
||||||
@ -1,4 +1,4 @@ |
|||||||
package subject |
package session |
||||||
|
|
||||||
import ( |
import ( |
||||||
"github.com/gorilla/websocket" |
"github.com/gorilla/websocket" |
||||||
@ -1,4 +1,4 @@ |
|||||||
package subject |
package session |
||||||
|
|
||||||
// Player 玩家: table,balance,(hand card)
|
// Player 玩家: table,balance,(hand card)
|
||||||
type Player struct { |
type Player struct { |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
package session |
||||||
|
|
||||||
|
// Subject 认证对象
|
||||||
|
type Subject struct { |
||||||
|
Id int64 `json:"id,omitempty"` // 用户id
|
||||||
|
Name string `json:"name,omitempty"` // 用户名
|
||||||
|
Time int64 `json:"time,omitempty"` // 生成时间戳
|
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
package collect |
||||||
|
|
||||||
|
// IsEmptySlice 切片是否为空
|
||||||
|
func IsEmptySlice[T any](slice []T) bool { |
||||||
|
return slice == nil || len(slice) == 0 |
||||||
|
} |
||||||
|
|
||||||
|
func IsNotEmptySlice[T any](slice []T) bool { |
||||||
|
return !IsEmptySlice(slice) |
||||||
|
} |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<title>websocket</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
<input type="text" id="uname"> |
||||||
|
<button id="btn">连接</button> |
||||||
|
|
||||||
|
<script type="text/javascript"> |
||||||
|
document.getElementById("btn").onclick = function() { |
||||||
|
const uname = document.getElementById("uname").value |
||||||
|
console.log(uname) |
||||||
|
connect(uname) |
||||||
|
} |
||||||
|
function connect(uname) { |
||||||
|
const ws = new WebSocket("ws://localhost:9999/ws") |
||||||
|
|
||||||
|
ws.onopen = (e) => { |
||||||
|
console.log('open', e) |
||||||
|
document.getElementById("uname").value = "" |
||||||
|
document.getElementById("btn").innerText = '发送' |
||||||
|
document.getElementById("btn").onclick = function () { |
||||||
|
const msg = document.getElementById("uname").value |
||||||
|
const token = "AgOjcdf3goeYDX3lwWWwkXtVpcrL-l2rX8csrRKgs3_-BC3JOx0l6nZU0MV25eIn" |
||||||
|
ws.send(JSON.stringify({t: token})) |
||||||
|
document.getElementById("uname").value = '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ws.onmessage = (e) => { |
||||||
|
console.log('msg:', e.data) |
||||||
|
} |
||||||
|
|
||||||
|
ws.onerror = (a,b,c) => { |
||||||
|
console.log('error:', a, b, c) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
Loading…
Reference in new issue