26 changed files with 496 additions and 151 deletions
@ -1,7 +1,18 @@
|
||||
|
||||
[auth] |
||||
ignoreUrl = [ |
||||
"/ws", |
||||
"/auth/captcha" |
||||
] |
||||
[http] |
||||
addr = ":9999" |
||||
|
||||
[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 |
||||
|
||||
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 登录或注册
|
||||
func Login(ctx *gin.Context) { |
||||
// http: 登录注册、创建房间
|
||||
// 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 已认证的长连接用户
|
||||
type NetAccount struct { |
||||
@ -1,4 +1,4 @@
|
||||
package subject |
||||
package session |
||||
|
||||
import ( |
||||
"github.com/gorilla/websocket" |
||||
@ -1,4 +1,4 @@
|
||||
package subject |
||||
package session |
||||
|
||||
// Player 玩家: table,balance,(hand card)
|
||||
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