The following are all text and code for testing Copied from one of the Go Practice Projects
📝 用Golang实现用户注册登录功能
In this mini project, postgres and redis are used, my notes on the simple use of postgres and redis are avaliable
目标
- 打开localhost:3000/signup,填写注册信息
- 用户名(验证要求:手机号码或邮箱)
- 密码(拥有大小写字母及数字,至少8位)
- 打开localhost:3000/signin,显示登录框(可填写用户名和密码),登录框下方有按钮可以调到signup页面注册
- 若登录成功 -> 跳转localhost:3000/profile,显示用户名及Login Succeeded
- 若登录失败 -> 停留当前页面,显示Login Failed
- 登录状态保持在前端,未登录状态打开localhost:3000/profile,跳转至localhost:3000/signin
模块需求
- 任选一种web framework https://github.com/mingrammer/go-web-framework-stars
- 数据库使用postgres
- 登录状态缓存使用redis
运行
$ chmod +x redis.sh
$ ./redis.sh
$ go build -o app
$ ./app
注:需要安装postgres和redis
展示
运行后,先会别要求输入postgres的连接数据,当postgres连接成功后就可以打开网页了 打开浏览器输入网址
localhost:3000
就会进入index页面
点击profile,会跳转登陆界面
登陆界面有按钮可以跳转注册页面
注册界面会对用户提供的用户名和密码进行检查
注册成功后会跳转profile界面
点击home可以到index界面,此时导航栏的显示是登陆状态的显示(有logout,无register和login)
点击logout即可退出,退出后会被跳转到首页,此时导航栏显示为未登陆状态的显示
点击login,进行登陆,登陆时会检查用户名和密码是否存在于数据库中
各golang文件功能
sqlHandeler.go
负责postgres数据库的操作,包括
- 连接数据库
- 断开数据库
- 向数据库插入数据
- 在数据库中查询数据
redisHandler.go
负责redis数据库的操作,包括
- 连接redis server
- 断开与redis server的连接
- 向redis中添加session token和用户名
- 根据session token确定用户是否登陆
- 根据session token来查询用户名
- 从redis中删除session token和其对应的用户名
main.go
项目的入口,负责
- 调用sqlHandler和redisHandler的连接和断开方法
- 启动路由
- 加载网页模版
- 启动网页
- 提供渲染所有页面的方法(render function)
routes.go
主要负责路由
- 根据url调用handlers.user中的方法
- 负责展示index页面
handlers.user.go
负责处理用户的操作并调用main中的方法来渲染和用户操作相对应的html handlers.user提供的操作有
- 用户登陆
- 展示登陆界面
- 生成session token
- 展示用户profile
- 用户登出
- 展示注册界面
- 用户注册
models.user.go
具体负责通过sqlHandler提供的方法与postgres数据库交互来提供用户的信息验证
- 检查用户名和密码是否对应
- 具体负责注册新用户
- 检查用户名是否可用
- 检查用户名是否为邮箱或手机
- 检查密码格式是否正确
- 检查用户名是否可用(如果用户名不在数据库中即为可用)
middleware.auth.go
确保用户已经登陆/已经登出
- 确保用户已经登陆,如未登陆会产生panic
- 确保用户未登陆,如已经登陆会产生panic
各html文件介绍
header.html
html文件的header格式
footer.html
html文件的footer格式
menu.html
在header中被使用 会根据用户是否登陆展示不同的项目
index.html
网站主界面
register.html
用户注册界面
login.html
用户登陆界面
logged.html
profile展示界面
Note
Code adapted from https://github.com/demo-apps/go-gin-app
附录
目录结构
代码
sqlHandeler.go
package main
import (
"database/sql"
"fmt"
"strconv"
"strings"
"io/ioutil"
_ "github.com/lib/pq"
)
var (
host = "localhost"
port = 5432
user = "postgres"
password = ""
dbname = "postgres"
)
var db *sql.DB
func checkErr(err error) {
if err != nil {
panic(err)
}
}
func getDbInfo() {
var userHost string
fmt.Print("Server[localhost]: ")
fmt.Scanln(&userHost)
if strings.TrimSpace(userHost) != "" {
host = userHost
}
var userDB string
fmt.Print("Database[postgres]: ")
fmt.Scanln(&userDB)
if strings.TrimSpace(userDB) != "" {
dbname = userDB
}
var userPort string
fmt.Print("Port[5432]: ")
fmt.Scanln(&userPort)
if strings.TrimSpace(userPort) != "" {
intPort, err := strconv.Atoi(userPort)
checkErr(err)
port = intPort
}
var userName string
fmt.Print("Username[postgres]: ")
fmt.Scanln(&userName)
if strings.TrimSpace(userName) != "" {
user = userName
}
fmt.Print("Password for user postgres: ")
fmt.Scanln(&password)
}
func insert(givenAcc string, givenPass string) {
psqlInfo := fmt.Sprintf("INSERT INTO users(account, password) VALUES('%s','%s');", givenAcc, givenPass)
_, err := db.Exec(psqlInfo)
checkErr(err)
}
func query(givenAcc string) (hasAccount bool, pass string) {
hasAccount = false
pass = ""
psqlInfo := fmt.Sprintf("SELECT password FROM users WHERE account='%s';", givenAcc)
info, err := db.Query(psqlInfo)
checkErr(err)
// fmt.Printf("info has type %T\n", info)
for info.Next() {
err = info.Scan(&pass)
checkErr(err)
hasAccount = true
}
return
} // query
func initDB() {
getDbInfo()
openDBSQL := fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
sqlBytes, err := ioutil.ReadFile("createTable.sql")
checkErr(err)
sqlCommand := string(sqlBytes)
var errOpenDB error
db, errOpenDB = sql.Open("postgres", openDBSQL)
// fmt.Printf("db has type of %T\n", db)
checkErr(errOpenDB)
_, err = db.Exec(sqlCommand)
checkErr(err)
}
func closeDB() {
db.Close()
}
redisHandler.go
package main
import (
"fmt"
"os"
"github.com/gomodule/redigo/redis"
)
var c redis.Conn
func errCheck(info string, err error) {
if err != nil {
fmt.Printf("%s %v\n", info, err)
os.Exit(-1)
}
}
func connectRedis() {
var err error
c, err = redis.Dial("tcp", "127.0.0.1:6379")
errCheck("Connect to redis error:", err)
}
func closeRedis() {
c.Close()
}
func addInRedis(sessionIDGiven string, account string) {
_, err := c.Do("SET", sessionIDGiven, account)
errCheck("redis set failed:", err)
}
func isLoggedInRedis(sessionIDGiven string) bool {
is_key_exit, err := redis.Bool(c.Do("EXISTS", sessionIDGiven))
errCheck("error:", err)
return is_key_exit
}
func getInRedis(sessionIDGiven string) string {
account, err := redis.String(c.Do("GET", sessionIDGiven))
errCheck("redis get failed:", err)
return account
}
func removedInRedis(sessionIDGiven string) {
_, err := c.Do("DEL", sessionIDGiven)
errCheck("redis delete failed:", err)
}
main.go
// main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
var router *gin.Engine
func main() {
// init the database
initDB()
defer closeDB()
// init the redis Server
connectRedis()
defer closeRedis()
// Set the router as the default one provided by Gin
router = gin.Default()
// Process the templates at the start so that they don't have to be loaded
// from the disk again. This makes serving HTML pages very fast.
router.LoadHTMLGlob("templates/*")
// initialize the routes
initializeRoutes()
// Start serving the application
router.Run(":3000")
}
// Render one of HTML, JSON or CSV based on the 'Accept' header of the request
// If the header doesn't specify this, HTML is rendered, provided that
// the template name is present
func render(c *gin.Context, data gin.H, templateName string) {
token, _ := c.Cookie("token")
// 设置html中的is_logged_in
data["is_logged_in"] = isLoggedInRedis(token)
// fmt.Println(data["is_logged_in"])
switch c.Request.Header.Get("Accept") {
case "application/json":
// Respond with JSON
c.JSON(http.StatusOK, data["payload"])
case "application/xml":
// Respond with XML
c.XML(http.StatusOK, data["payload"])
default:
// Respond with HTML
c.HTML(http.StatusOK, templateName, data)
}
}
routes.go
// routes.go
package main
import (
"github.com/gin-gonic/gin"
)
func initializeRoutes() {
// Handle the index route
router.GET("/", showIndexPage)
// Group user related routes together
userRoutes := router.Group("/")
{
// Handle the GET requests at /signin
// Show the login page
// Ensure that the user is not logged in by using the middleware
userRoutes.GET("signin", ensureNotLoggedIn(), showLoginPage)
// Handle POST requests at /profile
// Ensure that the user is not logged in by using the middleware
userRoutes.POST("profile", ensureNotLoggedIn(), performLogin)
// Handle GET requests at /logout
// Ensure that the user is logged in by using the middleware
userRoutes.GET("logout", ensureLoggedIn(), logout)
// Handle GET request at /profile
userRoutes.GET("profile", viewProfile)
// Handle the GET requests at /signup
// Show the registration page
// Ensure that the user is not logged in by using the middleware
userRoutes.GET("signup", ensureNotLoggedIn(), showRegistrationPage)
// Handle POST requests at /signup
// Ensure that the user is not logged in by using the middleware
userRoutes.POST("signup", ensureNotLoggedIn(), register)
}
}
func showIndexPage(c *gin.Context) {
// Call the render function with the name of the template to render
render(c, gin.H{
"title": "Home Page",
}, "index.html")
}
handlers.user.go
// handlers.user.go
package main
import (
"fmt"
"math/rand"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func showLoginPage(c *gin.Context) {
// Call the render function with the name of the template to render
render(c, gin.H{
"title": "Login",
}, "login.html")
}
func performLogin(c *gin.Context) {
// Obtain the POSTed username and password values
username := c.PostForm("username")
password := c.PostForm("password")
// Check if the username/password combination is valid
if isUserValid(username, password) {
// If the username/password is valid set the token in a cookie
token := generateSessionToken()
fmt.Println(token)
c.SetCookie("token", token, 3600, "", "", false, true)
addInRedis(token, username)
// redirect to profile page
c.Redirect(http.StatusMovedPermanently, "/profile")
} else {
// If the username/password combination is invalid,
// show the error message on the login page
c.HTML(http.StatusBadRequest, "login.html", gin.H{
"ErrorTitle": "Login Failed",
"ErrorMessage": "Invalid credentials provided"})
}
}
func generateSessionToken() string {
// We're using a random 16 character string as the session token
// This is NOT a secure way of generating session tokens
// DO NOT USE THIS IN PRODUCTION
return strconv.FormatInt(rand.Int63(), 16)
}
func viewProfile(c *gin.Context) {
token, _ := c.Cookie("token")
loggedIn := isLoggedInRedis(token)
if loggedIn {
username := getInRedis(token)
render(c, gin.H{
"title": "Successful Login",
"payload": username}, "logged.html")
} else {
c.Redirect(http.StatusTemporaryRedirect, "/signin")
}
}
func logout(c *gin.Context) {
token, _ := c.Cookie("token")
removedInRedis(token)
// Clear the cookie
c.SetCookie("token", "", -1, "", "", false, true)
// Redirect to the home page
c.Redirect(http.StatusTemporaryRedirect, "/")
}
func showRegistrationPage(c *gin.Context) {
// Call the render function with the name of the template to render
render(c, gin.H{
"title": "Register"}, "register.html")
}
func register(c *gin.Context) {
// Obtain the POSTed username and password values
username := c.PostForm("username")
password := c.PostForm("password")
if _, err := registerNewUser(username, password); err == nil {
// If the user is created, set the token in a cookie and log the user in
token := generateSessionToken()
c.SetCookie("token", token, 3600, "", "", false, true)
addInRedis(token, username)
// redirect to profile page
c.Redirect(http.StatusMovedPermanently, "/profile")
} else {
// If the username/password combination is invalid,
// show the error message on the login page
c.HTML(http.StatusBadRequest, "register.html", gin.H{
"ErrorTitle": "Registration Failed",
"ErrorMessage": err.Error()})
}
}
models.user.go
// models.user.go
package main
import (
"errors"
"regexp"
"strings"
"unicode"
)
// Check if the username and password combination is valid
func isUserValid(username, password string) bool {
hasAcc, passwordInDB := query(username)
if hasAcc {
if passwordInDB == password {
return true
}
}
return false
}
// Register a new user with the given username and password
func registerNewUser(username, password string) (string, error) {
if strings.TrimSpace(password) == "" {
return "", errors.New("The password can't be empty")
} else if !isUsernameAvailable(username) {
return "", errors.New("The username isn't available")
}
//pattern := `\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*` //匹配电子邮箱
pattern := `^[0-9a-z][_.0-9a-z-]{0,31}@([0-9a-z][0-9a-z-]{0,30}[0-9a-z]\.){1,4}[a-z]{2,4}$`
reg := regexp.MustCompile(pattern)
isEmail := reg.MatchString(username)
// 匹配手机
regular := "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$"
reg2 := regexp.MustCompile(regular)
isMobile := reg2.MatchString(username)
// 验证用户名
if (isEmail || isMobile) != true {
return "", errors.New("Your user name should either be phone number or email address!")
}
// 验证密码长度
if len(password) < 8 {
return "", errors.New("Your password is too short!")
}
// 验证密码是否有大小写
var hasUpperCase, hasLowercase bool
for _, c := range password {
switch {
case unicode.IsUpper(c):
hasUpperCase = true
if hasLowercase {
break
}
case unicode.IsLower(c):
hasLowercase = true
if hasUpperCase {
break
}
}
}
if (hasLowercase && hasUpperCase) != true {
return "", errors.New("Your password should contain both upper and lower cases!")
}
insert(username, password)
return username, nil
}
// Check if the supplied username is available
func isUsernameAvailable(username string) bool {
hasAcc,_ := query(username)
if hasAcc {
return false
}
return true
}
middleware.auth.go
// middleware.auth.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// This middleware ensures that a request will be aborted with an error
// if the user is not logged in
func ensureLoggedIn() gin.HandlerFunc {
return func(c *gin.Context) {
// If the token is not in redis
// the user is not logged in
token, _ := c.Cookie("token")
loggedIn := isLoggedInRedis(token)
if !loggedIn {
//if token, err := c.Cookie("token"); err != nil || token == "" {
c.AbortWithStatus(http.StatusUnauthorized)
}
}
}
// This middleware ensures that a request will be aborted with an error
// if the user is already logged in
func ensureNotLoggedIn() gin.HandlerFunc {
return func(c *gin.Context) {
//If the token is in the redis
// the user is already logged in
token, _ := c.Cookie("token")
loggedIn := isLoggedInRedis(token)
if loggedIn {
// if token, err := c.Cookie("token"); err == nil || token != "" {
c.AbortWithStatus(http.StatusUnauthorized)
}
}
}
redis.sh
#!/bin/sh
redis-server
createTable.sql
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id SERIAL PRIMARY KEY,
account VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL
);
header.html
<!--header.html-->
<!doctype html>
<html>
<head>
<!--Use the title variable to set the title of the page-->
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<!--Use bootstrap to make the application look nice-->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script async src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body class="continer">
<!--Embed the menu.html template at this location-->
footer.html
<!--footer.html-->
</body>
</html>
menu.html
<!--menu.html-->
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">
Home
</a>
</div>
<ul class="nav navbar-nav">
<!--Display this link only when the user is not logged in-->
<li><a href="/signin">Profile</a></li>
<!--Display this link only when the user is not logged in-->
<li><a href="/signup">Register</a></li>
<!--Display this link only when the user is not logged in-->
<li><a href="/signin">Login</a></li>
<!--Display this link only when the user is logged in-->
<li><a href="/profile">Profile</a></li>
<!--Display this link only when the user is logged in-->
<li><a href="/logout">Logout</a></li>
</ul>
</div>
</nav>
index.html
<!--index.html-->
<!--Embed the header.html template at this location-->
<h1>Hello Gin!</h1>
<!--Embed the footer.html template at this location-->
register.html
<!--register.html-->
<!--Embed the header.html template at this location-->
<h1>Register</h1>
<div class="panel panel-default col-sm-6">
<div class="panel-body">
<!--If there's an error, display the error-->
<p class="bg-danger">
:
</p>
<!--Create a form that POSTs to the `/u/register` route-->
<form class="form" action="/signup" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" class="form-control" id="password" placeholder="Password">
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
</div>
<!--Embed the footer.html template at this location-->
login.html
<!--login.html-->
<!--Embed the header.html template at this location-->
<h1>Login</h1>
<div class="panel panel-default col-sm-6">
<div class="panel-body">
<!--If there's an error, display the error-->
<p class="bg-danger">
:
</p>
<!--Create a form that POSTs to the `/u/login` route-->
<form class="form" action="/profile" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<br>
<form action="/signup" method="GET">
<button type="submit" class="btn btn-primary">Sign up</button>
</form>
</div>
</div>
<!--Embed the footer.html template at this location-->
logged.html
<!--index.html-->
<!--Embed the header.html template at this location-->
<h1>用户名:</h1>
<h1>Login succeeded!</h1>
<!--Embed the footer.html template at this location-->