Merging other languages

This commit is contained in:
James Miranda
2016-09-23 18:01:10 -03:00
parent 380a8ee74c
commit de3c5bdaa4
490 changed files with 24539 additions and 24588 deletions

View File

@@ -1,215 +1,222 @@
# 6.2 Go如何使用session
通过上一小节的介绍我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案目前Go标准包没有为session提供任何支持这小节我们将会自己动手来实现go版本的session管理和创建。
## session创建过程
session的基本原理是由服务器为每个会话维护一份信息数据客户端和服务端依靠一个全局唯一的标识来访问这份数据以达到交互的目的。当用户访问Web应用时服务端程序会随需要创建session这个过程可以概括为三个步骤
- 生成全局唯一标识符sessionid
- 开辟数据存储空间。一般会在内存中创建相应的数据结构但这种情况下系统一旦掉电所有的会话数据就会丢失如果是电子商务类网站这将造成严重的后果。所以为了解决这类问题你可以将会话数据写到文件里或存储在数据库中当然这样会增加I/O开销但是它可以实现某种程度的session持久化也更有利于session的共享
- 将session的全局唯一标示符发送给客户端。
以上三个步骤中最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义数据无非可以放到请求行、头域或Body里所以一般来说会有两种常用的方式cookie和URL重写。
1. Cookie
服务端通过设置Set-cookie头就可以将session的标识符传送到客户端而客户端此后的每一次请求都会带上这个标识符另外一般包含session信息的cookie会将失效时间设置为0(会话cookie)即浏览器进程有效时间。至于浏览器怎么处理这个0每个浏览器都有自己的方案但差别都不会太大(一般体现在新建浏览器窗口的时候)
2. URL重写
所谓URL重写就是在返回给用户的页面里的所有的URL后面追加session标识符这样用户在收到响应之后无论点击响应页面里的哪个链接或提交表单都会自动带上session标识符从而就实现了会话的保持。虽然这种做法比较麻烦但是如果客户端禁用了cookie的话此种方案将会是首选。
## Go实现session管理
通过上面session创建过程的讲解读者应该对session有了一个大体的认识但是具体到动态页面技术里面又是怎么实现session的呢下面我们将结合session的生命周期lifecycle来实现go语言版本的session管理。
### session管理设计
我们知道session管理涉及到如下几个因素
- 全局session管理器
- 保证sessionid 的全局唯一性
- 为每个客户关联一个session
- session 的存储(可以存储到内存、文件、数据库等)
- session 过期处理
接下来我将讲解一下我关于session管理的整个设计思路以及相应的go代码示例
### Session管理器
定义一个全局的session管理器
type Manager struct {
cookieName string //private cookiename
lock sync.Mutex // protects session
provider Provider
maxlifetime int64
}
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
provider, ok := provides[provideName]
if !ok {
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
}
return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
}
Go实现整个的流程应该也是这样的在main包中创建一个全局的session管理器
var globalSessions *session.Manager
//然后在init函数中初始化
func init() {
globalSessions, _ = NewManager("memory","gosessionid",3600)
}
我们知道session是保存在服务器端的数据它可以以任何的方式存储比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口用以表征session管理器底层存储结构。
type Provider interface {
SessionInit(sid string) (Session, error)
SessionRead(sid string) (Session, error)
SessionDestroy(sid string) error
SessionGC(maxLifeTime int64)
}
- SessionInit函数实现Session的初始化操作成功则返回此新的Session变量
- SessionRead函数返回sid所代表的Session变量如果不存在那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量
- SessionDestroy函数用来销毁sid对应的Session变量
- SessionGC根据maxLifeTime来删除过期的数据
那么Session接口需要实现什么样的功能呢有过Web开发经验的读者知道对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作所以我们的Session接口也就实现这四个操作。
type Session interface {
Set(key, value interface{}) error //set session value
Get(key interface{}) interface{} //get session value
Delete(key interface{}) error //delete session value
SessionID() string //back current sessionID
}
>以上设计思路来源于database/sql/driver先定义好接口然后具体的存储session的结构实现相应的接口并注册后相应功能这样就可以使用了以下是用来随需注册存储session的结构的Register函数的实现。
var provides = make(map[string]Provider)
// Register makes a session provide available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, provider Provider) {
if provider == nil {
panic("session: Register provide is nil")
}
if _, dup := provides[name]; dup {
panic("session: Register called twice for provide " + name)
}
provides[name] = provider
}
### 全局唯一的Session ID
Session ID是用来识别访问Web应用的每一个用户因此必须保证它是全局唯一的GUID下面代码展示了如何满足这一需求
func (manager *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
return base64.URLEncoding.EncodeToString(b)
}
### session创建
我们需要为每个来访用户分配或获取与他相关连的Session以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联如果没有则创建之。
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
manager.lock.Lock()
defer manager.lock.Unlock()
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
sid := manager.sessionId()
session, _ = manager.provider.SessionInit(sid)
cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
http.SetCookie(w, &cookie)
} else {
sid, _ := url.QueryUnescape(cookie.Value)
session, _ = manager.provider.SessionRead(sid)
}
return
}
我们用前面login操作来演示session的运用
func login(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
r.ParseForm()
if r.Method == "GET" {
t, _ := template.ParseFiles("login.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("username"))
} else {
sess.Set("username", r.Form["username"])
http.Redirect(w, r, "/", 302)
}
}
### 操作值:设置、读取和删除
SessionStart函数返回的是一个满足Session接口的变量那么我们该如何用他来对session数据进行操作呢
上面的例子中的代码`session.Get("uid")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作:
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
通过上面的例子可以看到Session的操作和操作key/value数据库类似:Set、Get、Delete等操作
因为Session有过期的概念所以我们定义了GC操作当访问过期时间满足GC的触发条件后将会引起GC但是当我们进行了任意一个session操作都会对Session实体进行更新都会触发对最后访问时间的修改这样当GC的时候就不会误删除还在使用的Session实体。
### session重置
我们知道Web应用中有用户退出这个操作那么当用户退出应用的时候我们需要对该用户的session数据进行销毁操作上面的代码已经演示了如何使用session重置操作下面这个函数就是实现了这个功能
//Destroy sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
return
} else {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionDestroy(cookie.Value)
expiration := time.Now()
cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
http.SetCookie(w, &cookie)
}
}
### session销毁
我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动
func init() {
go globalSessions.GC()
}
func (manager *Manager) GC() {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionGC(manager.maxlifetime)
time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
}
我们可以看到GC充分利用了time包中的定时器功能当超时`maxLifeTime`之后调用GC函数这样就可以保证`maxLifeTime`时间内的session都是可用的类似的方案也可以用于统计在线用户数之类的。
## 总结
至此 我们实现了一个用来在Web应用中全局管理Session的SessionManager定义了用来提供Session存储实现Provider的接口,下一小节我们将会通过接口定义来实现一些Provider,供大家参考学习。
## links
* [目录](<preface.md>)
* 上一节: [session和cookie](<06.1.md>)
* 下一节: [session存储](<06.3.md>)
# 6.2 How to use sessions in Go
In section 6.1, we learned that sessions are one solution for verifying users, and that for now, the Go standard library does not have baked-in support for sessions or session handling. So, we're going to implement our own version of a session manager in Go.
## Creating sessions
The basic principle behind sessions is that a server maintains information for every single client, and clients rely on unique session ids to access this information. When users visit the web application, the server will create a new session with the following three steps, as needed:
- Create a unique session id
- Open up a data storage space: normally we save sessions in memory, but you will lose all session data if the system is accidentally interrupted. This can be a very serious issue if web application deals with sensitive data, like in electronic commerce for instance. In order to solve this problem, you can instead save your session data in a database or file system. This makes data persistence more reliable and easy to share with other applications, although the tradeoff is that more server-side IO is needed to read and write these sessions.
- Send the unique session id to the client.
The key step here is to send the unique session id to the client. In the context of a standard HTTP response, you can either use the response line, header or body to accomplish this; therefore, we have two ways to send session ids to clients: by cookies or URL rewrites.
- Cookies: the server can easily use `Set-cookie` inside of a response header to save a session id to a client, and a client can then this cookie for future requests; we often set the expiry time for for cookies containing session information to 0, which means the cookie will be saved in memory and only deleted after users have close their browsers.
- URL rewrite: append the session id as arguments in the URL for all pages. This way seems messy, but it's the best choice if clients have disabled cookies in their browsers.
## Use Go to manage sessions
We've talked about constructing sessions, and you should now have a general overview of it, but how can we use sessions on dynamic pages? Let's take a closer look at the life cycle of a session so we can continue implementing our Go session manager.
### Session management design
Here is a list of some of the key considerations in session management design.
- Global session manager.
- Keep session id unique.
- Have one session for every user.
- Session storage in memory, file or database.
- Deal with expired sessions.
Next, we'll examine a complete example of a Go session manager and the rationale behind some of its design decisions.
### Session manager
Define a global session manager:
type Manager struct {
cookieName string //private cookiename
lock sync.Mutex // protects session
provider Provider
maxlifetime int64
}
func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
provider, ok := provides[provideName]
if !ok {
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
}
return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
}
Create a global session manager in the `main()` function:
var globalSessions *session.Manager
// Then, initialize the session manager
func init() {
globalSessions = NewManager("memory","gosessionid",3600)
}
We know that we can save sessions in many ways including in memory, the file system or directly into the database. We need to define a `Provider` interface in order to represent the underlying structure of our session manager:
type Provider interface {
SessionInit(sid string) (Session, error)
SessionRead(sid string) (Session, error)
SessionDestroy(sid string) error
SessionGC(maxLifeTime int64)
}
- `SessionInit` implements the initialization of a session, and returns new a session if it succeeds.
- `SessionRead` returns a session represented by the corresponding sid. Creates a new session and returns it if it does not already exist.
- `SessionDestroy` given an sid, deletes the corresponding session.
- `SessionGC` deletes expired session variables according to `maxLifeTime`.
So what methods should our session interface have? If you have any experience in web development, you should know that there are only four operations for sessions: set value, get value, delete value and get current session id. So, our session interface should have four methods to perform these operations.
type Session interface {
Set(key, value interface{}) error //set session value
Get(key interface{}) interface{} //get session value
Delete(key interface{}) error //delete session value
SessionID() string //back current sessionID
}
This design takes its roots from the `database/sql/driver`, which defines the interface first, then registers specific structures when we want to use it. The following code is the internal implementation of a session register function.
var provides = make(map[string]Provider)
// Register makes a session provider available by the provided name.
// If a Register is called twice with the same name or if the driver is nil,
// it panics.
func Register(name string, provider Provider) {
if provider == nil {
panic("session: Register provide is nil")
}
if _, dup := provides[name]; dup {
panic("session: Register called twice for provide " + name)
}
provides[name] = provider
}
### Unique session ids
Session ids are for identifying users of web applications, so they must be unique. The following code shows how to achieve this goal:
func (manager *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
return base64.URLEncoding.EncodeToString(b)
}
### Creating a session
We need to allocate or get an existing session in order to validate user operations. The `SessionStart` function is for checking if any there are any sessions related to the current user, creating a new session non are found.
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
manager.lock.Lock()
defer manager.lock.Unlock()
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
sid := manager.sessionId()
session, _ = manager.provider.SessionInit(sid)
cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
http.SetCookie(w, &cookie)
} else {
sid, _ := url.QueryUnescape(cookie.Value)
session, _ = manager.provider.SessionRead(sid)
}
return
}
Here is an example that uses sessions for a login operation.
func login(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
r.ParseForm()
if r.Method == "GET" {
t, _ := template.ParseFiles("login.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("username"))
} else {
sess.Set("username", r.Form["username"])
http.Redirect(w, r, "/", 302)
}
}
### Operation value: set, get and delete
The `SessionStart` function returns a variable that implements a session interface. How do we use it?
You saw `session.Get("uid")` in the above example for a basic operation. Now let's examine a more detailed example.
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
As you can see, operating on sessions simply involves using the key/value pattern in the Set, Get and Delete operations.
Because sessions have the concept of an expiry time, we define the GC to update the session's latest modify time. This way, the GC will not delete sessions that have expired but are still being used.
### Reset sessions
We know that web application have a logout operation. When users logout, we need to delete the corresponding session. We've already used the reset operation in above example -now let's take a look at the function body.
//Destroy sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
return
} else {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionDestroy(cookie.Value)
expiration := time.Now()
cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
http.SetCookie(w, &cookie)
}
}
### Delete sessions
Let's see how to let the session manager delete a session. We need to start the GC in the `main()` function:
func init() {
go globalSessions.GC()
}
func (manager *Manager) GC() {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionGC(manager.maxlifetime)
time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
}
We see that the GC makes full use of the timer function in the `time` package. It automatically calls GC when the session times out, ensuring that all sessions are usable during `maxLifeTime`. A similar solution can be used to count online users.
## Summary
So far, we implemented a session manager to manage global sessions in the web application and defined the `Provider` interface as the storage implementation of `Session`. In the next section, we are going to talk about how to implement `Provider` for additional session storage structures, which you will be able to reference in the future.
## Links
- [Directory](preface.md)
- Previous section: [Session and cookies](06.1.md)
- Next section: [Session storage](06.3.md)