ACChe

ACChe 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

举头思算法,低头写代码...

个人动态

ACChe 收藏了文章 · 3月30日

JS箭头函数之:为何用?怎么用?何时用?

在现代JS中最让人期待的特性就是关于箭头函数,用=>来标识。箭头函数有两个主要的优点:

  • 更简短的函数;
  • 更直观的作用域和this的绑定(不绑定this)

因为这些优点,箭头函数比起其他形式的函数声明更受欢迎。比如,受欢迎的airbnb eslint configuration库会强制使用JavaScript箭头函数创建匿名函数。
然而,箭头函数有优点,也有一些“缺点”。这就需要在使用的时候做一些权衡。下面就从为何用、怎么用、何时用,这个三部分做一些总结。

为何用?

引入箭头函数有两个方面的作用:更简短的函数并且不绑定this

更简短的函数

var elements = ['h1', 'div', 'span', 'section'];

elements.map(function(el) {
  return el.length; // 返回数组: [2, 3, 4, 7]
});

// 从上面的普通函数可以改写为如下的箭头函数
elements.map((el) => {
  return el.length; // [2, 3, 4, 7]
});

// 当箭头函数只有一个参数时,可以省略参数的圆括号
elements.map(el => {
  return el.length; // [2, 3, 4, 7]
});

// 当箭头函数体只有一个`return`语句时,可以省略`return`关键字和方法体的花括号
elements.map(el => el.length); // [2, 3, 4, 7]

// 在这个例子中,因为我们只需要`length`属性,所以可以使用参数结构
// 需要注意的是字符串`"length"`是我们想要获得的属性名称,而`elLength`则只是个变量名,可以替换成任意合法的变量名
elements.map(({ "length": elLength }) => elLength); // [2, 3, 4, 7]

不绑定this

在箭头函数出现之前,每个新定义的函数都有它自己的this值(在构造函数的情况下是一个新对象,在严格模式的函数调用中则为undefined,如果该函数被作为"对象方法"调用则为基础对象等)。
而箭头函数并没有它自己的执行上下,实际上,这就意味着代码中的thisarguments都是继承它的父函数

const obj = {
  name: 'test object',
  createAnonFunction: function() {
    return function() { 
      console.log(this.name);
      return this;
    }
  },
  createArrowFunction: function() {
    return () => {
      console.log(this.name);
      return this;
    }
  }
}

const anon = obj.createAnonFunction();
anon(); // undefined
anon() === window // true
  
const arrow = obj.createArrowFunction();
arrow(); // 'test object'
arrow() === obj // true

第一个匿名参数有自己的上下文(指向的并非obj对象),当被赋值给anon变量且调用时,this发生了改变,指向了window。另一个,箭头函数与创建它的函数有相同的上下文,故指向obj对象。

通过call或者apply调用

由于箭头函数没有自己的this指针,通过call()或者apply()方法调用一个函数时,只能传递参数(不能绑定this),它们的第一个参数会被忽略。

var adder = {
  base: 1,
  add: function(a) {
    var f = v => v + this.base;
    return f(a);
  },
  addByCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base: 2
    };
    return f.call(b, a)
  }
}

adder.add(1); // 2
adder.addByCall(1); // 2

不绑定arguments

箭头函数不绑定Arguments对象。因此,在本示例中,arguments只是引用了封闭作用域内的arguments:

function foo(n) {
  var f = () => arguments[0] + n; // 隐式绑定 foo 函数的arguments对象,arguments[0]是 n
  return f(); 
}

foo(1); // 2

在大多数情况下,使用剩余参数是相对使用arguments对象的更好选择。

function foo(arg) {
  var f = (...agrs) => args[0];
  return f(arg);
}
foo(1); // 1

function foo(arg1, arg2) {
  var f = (...args) => args[1];
  return f(arg1, arg2);
}
foo(1, 2); // 2

怎么用?

优化代码

比如你有一个有值的数组,你想去map遍历每一项,这时箭头函数就非常推荐:

const words = ['hello', 'WORLD', 'Whatever'];
const downcasedWords = words.map(word => word.toLowerCase());

一个及其常见的例子就是返回一个对象的某个值:

const names = objects.map(object => object.name);

类似的,当用forEach来替换传统for循环的时候,实际上箭头函数会直观的保持this来自于父一级:

this.examples.forEach(example => {
  this.runExample(example);
});

Promise和Promise链

当在编写异步编程时,箭头函数也会让代码更加直观和简洁。
这是箭头函数的理想位置,特别是如果您生成的函数是有状态的,同时想引用对象中的某些内容。

this.doSomethingAsync().then((result) => {
  this.storeResult(result);
});

对象转换

箭头函数的另一个常见而且十分有用的地方就是用于封装的对象转换。
例如在Vue.js中,有一种通用模式,就是使用mapStateVuex存储的各个部分,直接包含到Vue组件中。
这涉及到定义一套mappers,用于从原对象到完整的转换输出,这在组件问题中实十分有必要的。这一系列简单的转换,使用箭头函数是最合适不过的。比如:

export default {
  computed: {
    ...mapState([
      'results',
      'users'
    ])
  }
}

何时用?(不推荐使用场景)

使用new操作符

箭头函数不能用作构造器,和new一起使用会抛出错误。

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

使用prototype属性

箭头函数没有prototype属性。

var Foo = () => {};
console.log(Foo.prototype); // undefined

使用yield关键字

yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作生成器。

深层调用

如果你将函数定义为箭头函数,并且在他们之间来回调用,当你调试bug的时候你将被代码困惑,甚至得到如下的错误信息:

{anonymous}(){anonymous}(){anonymous}(){anonymous}(){anonymous}() //anonymous

常见错误

返回对象字面量

记住用params => { object: literal }这种简单的语法返回对象字面量是行不通的。

var func = () => { foo: 1 };
func(); // undefined

var func = () => { foo: function() {} };   
// SyntaxError: function statement requires a name

这是因为花括号{}里面的代码被解析为一系列语句(即 foo 被认为是一个标签,而非对象字面量的组成部分)。
所以,记得用圆括号把对象字面量包起来:

var func = () => ({foo: 1});

换行

箭头函数在参数和箭头之间不能换行。

var func = ()
           => 1; 
// SyntaxError: expected expression, got '=>'

解析顺序

虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。

let callback;

callback = callback || function() {}; // ok

callback = callback || () => {};      
// SyntaxError: invalid arrow-function arguments

callback = callback || (() => {});    // ok

更多示例

使用三元运算符

var foo = a => a > 15 ? 15 : a;

foo(10); // 10
foo(16); // 15

闭包

// 标准的闭包函数
function Add() {
  var i = 0;
  return function() {
    return (++i);
  }
}

var add = Add();
add(); // 1
add(); // 2

// 箭头函数体的闭包(i = 0是默认参数)
var Add = (i = 0) => { return (() => (++i)) };
var add = Add();
add(); // 1
add(); // 2

// 因为仅有一个返回,return及括号也可以省略
var Add = (i = 0) => () => (++i);

箭头函数递归

var fact = (x) => ( x == 0 ?  1 : x*fact(x-1) );
fact(5);       // 120

总结

箭头函数是JS语言中十分特别的属性,并且使很多情形中代码更加的变化莫测。尽管如此,就像其他的语言特性,他们有各自的优缺点。因此我们使用它应该仅仅是作为一种工具,而不是无脑的简单的全部替换为箭头函数。

查看原文

ACChe 赞了文章 · 3月30日

JS箭头函数之:为何用?怎么用?何时用?

在现代JS中最让人期待的特性就是关于箭头函数,用=>来标识。箭头函数有两个主要的优点:

  • 更简短的函数;
  • 更直观的作用域和this的绑定(不绑定this)

因为这些优点,箭头函数比起其他形式的函数声明更受欢迎。比如,受欢迎的airbnb eslint configuration库会强制使用JavaScript箭头函数创建匿名函数。
然而,箭头函数有优点,也有一些“缺点”。这就需要在使用的时候做一些权衡。下面就从为何用、怎么用、何时用,这个三部分做一些总结。

为何用?

引入箭头函数有两个方面的作用:更简短的函数并且不绑定this

更简短的函数

var elements = ['h1', 'div', 'span', 'section'];

elements.map(function(el) {
  return el.length; // 返回数组: [2, 3, 4, 7]
});

// 从上面的普通函数可以改写为如下的箭头函数
elements.map((el) => {
  return el.length; // [2, 3, 4, 7]
});

// 当箭头函数只有一个参数时,可以省略参数的圆括号
elements.map(el => {
  return el.length; // [2, 3, 4, 7]
});

// 当箭头函数体只有一个`return`语句时,可以省略`return`关键字和方法体的花括号
elements.map(el => el.length); // [2, 3, 4, 7]

// 在这个例子中,因为我们只需要`length`属性,所以可以使用参数结构
// 需要注意的是字符串`"length"`是我们想要获得的属性名称,而`elLength`则只是个变量名,可以替换成任意合法的变量名
elements.map(({ "length": elLength }) => elLength); // [2, 3, 4, 7]

不绑定this

在箭头函数出现之前,每个新定义的函数都有它自己的this值(在构造函数的情况下是一个新对象,在严格模式的函数调用中则为undefined,如果该函数被作为"对象方法"调用则为基础对象等)。
而箭头函数并没有它自己的执行上下,实际上,这就意味着代码中的thisarguments都是继承它的父函数

const obj = {
  name: 'test object',
  createAnonFunction: function() {
    return function() { 
      console.log(this.name);
      return this;
    }
  },
  createArrowFunction: function() {
    return () => {
      console.log(this.name);
      return this;
    }
  }
}

const anon = obj.createAnonFunction();
anon(); // undefined
anon() === window // true
  
const arrow = obj.createArrowFunction();
arrow(); // 'test object'
arrow() === obj // true

第一个匿名参数有自己的上下文(指向的并非obj对象),当被赋值给anon变量且调用时,this发生了改变,指向了window。另一个,箭头函数与创建它的函数有相同的上下文,故指向obj对象。

通过call或者apply调用

由于箭头函数没有自己的this指针,通过call()或者apply()方法调用一个函数时,只能传递参数(不能绑定this),它们的第一个参数会被忽略。

var adder = {
  base: 1,
  add: function(a) {
    var f = v => v + this.base;
    return f(a);
  },
  addByCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base: 2
    };
    return f.call(b, a)
  }
}

adder.add(1); // 2
adder.addByCall(1); // 2

不绑定arguments

箭头函数不绑定Arguments对象。因此,在本示例中,arguments只是引用了封闭作用域内的arguments:

function foo(n) {
  var f = () => arguments[0] + n; // 隐式绑定 foo 函数的arguments对象,arguments[0]是 n
  return f(); 
}

foo(1); // 2

在大多数情况下,使用剩余参数是相对使用arguments对象的更好选择。

function foo(arg) {
  var f = (...agrs) => args[0];
  return f(arg);
}
foo(1); // 1

function foo(arg1, arg2) {
  var f = (...args) => args[1];
  return f(arg1, arg2);
}
foo(1, 2); // 2

怎么用?

优化代码

比如你有一个有值的数组,你想去map遍历每一项,这时箭头函数就非常推荐:

const words = ['hello', 'WORLD', 'Whatever'];
const downcasedWords = words.map(word => word.toLowerCase());

一个及其常见的例子就是返回一个对象的某个值:

const names = objects.map(object => object.name);

类似的,当用forEach来替换传统for循环的时候,实际上箭头函数会直观的保持this来自于父一级:

this.examples.forEach(example => {
  this.runExample(example);
});

Promise和Promise链

当在编写异步编程时,箭头函数也会让代码更加直观和简洁。
这是箭头函数的理想位置,特别是如果您生成的函数是有状态的,同时想引用对象中的某些内容。

this.doSomethingAsync().then((result) => {
  this.storeResult(result);
});

对象转换

箭头函数的另一个常见而且十分有用的地方就是用于封装的对象转换。
例如在Vue.js中,有一种通用模式,就是使用mapStateVuex存储的各个部分,直接包含到Vue组件中。
这涉及到定义一套mappers,用于从原对象到完整的转换输出,这在组件问题中实十分有必要的。这一系列简单的转换,使用箭头函数是最合适不过的。比如:

export default {
  computed: {
    ...mapState([
      'results',
      'users'
    ])
  }
}

何时用?(不推荐使用场景)

使用new操作符

箭头函数不能用作构造器,和new一起使用会抛出错误。

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

使用prototype属性

箭头函数没有prototype属性。

var Foo = () => {};
console.log(Foo.prototype); // undefined

使用yield关键字

yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作生成器。

深层调用

如果你将函数定义为箭头函数,并且在他们之间来回调用,当你调试bug的时候你将被代码困惑,甚至得到如下的错误信息:

{anonymous}(){anonymous}(){anonymous}(){anonymous}(){anonymous}() //anonymous

常见错误

返回对象字面量

记住用params => { object: literal }这种简单的语法返回对象字面量是行不通的。

var func = () => { foo: 1 };
func(); // undefined

var func = () => { foo: function() {} };   
// SyntaxError: function statement requires a name

这是因为花括号{}里面的代码被解析为一系列语句(即 foo 被认为是一个标签,而非对象字面量的组成部分)。
所以,记得用圆括号把对象字面量包起来:

var func = () => ({foo: 1});

换行

箭头函数在参数和箭头之间不能换行。

var func = ()
           => 1; 
// SyntaxError: expected expression, got '=>'

解析顺序

虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。

let callback;

callback = callback || function() {}; // ok

callback = callback || () => {};      
// SyntaxError: invalid arrow-function arguments

callback = callback || (() => {});    // ok

更多示例

使用三元运算符

var foo = a => a > 15 ? 15 : a;

foo(10); // 10
foo(16); // 15

闭包

// 标准的闭包函数
function Add() {
  var i = 0;
  return function() {
    return (++i);
  }
}

var add = Add();
add(); // 1
add(); // 2

// 箭头函数体的闭包(i = 0是默认参数)
var Add = (i = 0) => { return (() => (++i)) };
var add = Add();
add(); // 1
add(); // 2

// 因为仅有一个返回,return及括号也可以省略
var Add = (i = 0) => () => (++i);

箭头函数递归

var fact = (x) => ( x == 0 ?  1 : x*fact(x-1) );
fact(5);       // 120

总结

箭头函数是JS语言中十分特别的属性,并且使很多情形中代码更加的变化莫测。尽管如此,就像其他的语言特性,他们有各自的优缺点。因此我们使用它应该仅仅是作为一种工具,而不是无脑的简单的全部替换为箭头函数。

查看原文

赞 21 收藏 17 评论 0

ACChe 赞了文章 · 3月30日

Go Web编程--给自己写的服务器添加错误和访问日志

错误日志和访问日志是一个服务器必须支持的功能,我们教程里使用的服务器到目前为止还没有这两个功能。正好前两天也写了篇介绍logrus日志库的文章,那么今天的文章里就给我们自己写的服务器加上错误日志和访问日志的功能。在介绍添加访问日志的时候会介绍一种通过编写中间件获取HTTP响应的StausCodeBody的方法。

Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复gohttp11获取本文源代码

初始化日志记录器

我们先来做一下初始化工作,在项目里初始化记录错误日志和访问日志的记录器Logger

// ./utils/vlog
package vlog

import (
    "github.com/sirupsen/logrus"
    "os"
)

var ErrorLog *logrus.Logger
var AccessLog *logrus.Logger
var errorLogFile = "./tmp/log/error.log"
var accessLogFile = "./tmp/log/access.log"
func init () {
    initErrorLog()
    initAccessLog()
}

func initErrorLog() {
    ErrorLog = logrus.New()
    ErrorLog.SetFormatter(&logrus.JSONFormatter{})
    file , err := os.OpenFile(errorLogFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0755)
    if err != nil {
        panic(err)
    }
    ErrorLog.SetOutput(file)
}

func initAccessLog() {
    AccessLog = logrus.New()
    AccessLog.SetFormatter(&logrus.JSONFormatter{})
    file , err := os.OpenFile(accessLogFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0755)
    if err != nil {
        panic(err)
    }
    AccessLog.SetOutput(file)
}
  • 我们新定义一个packageinit函数中来初始化记录器,这样服务器成功启动前就会初始化好记录器。
  • /tmp/log这个目录要提前创建好,执行init函数时会自动创建好access.logerror.log

添加错误日志

我们创建服务器使用的net/http包的Server类型中,有一个ErrorLog字段供开发者设置记录错误日志用的记录器Logger,默认使用的是log包默认的记录器(应该是系统的标准错误):

type Server struct {
   Addr    string  // TCP address to listen on, ":http" if empty
   Handler Handler // handler to invoke, http.DefaultServeMux if nil
   ...
   // ErrorLog specifies an optional logger for errors accepting
   // connections, unexpected behavior from handlers, and
   // underlying FileSystem errors.
   // If nil, logging is done via the log package's standard logger.
   ErrorLog *log.Logger
     ...
}

我们之前在创建服务器的时候自己实现了Server类型的对象,那么现在要做的就是将上面初始化好的错误日志的记录器指定给ServerErrorLog字段。

func main() {
  ...
  // 将`logrus`的Logger转换为io.Writer
    errorWriter := vlog.ErrorLog.Writer()
  // 记得关闭io.Writer
    defer errorWriter.Close()

    server := &http.Server{
        Addr:    ":8080",
        Handler: muxRouter,
    // 用记录器转换成的io.Writer创建log.Logger
        ErrorLog: log.New(vlog.ErrorLog.Writer(), "", 0),
    }
    ...
}

添加好错误日志的记录器后,我们找个路由处理函数,在里面故意制造运行时错误验证一下是否能记录到错误。

func (*HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   ints := []int{0, 1, 2}
   fmt.Fprintf(w, "%v", ints[0:5])
}

在上面处理函数中,通过切片表达式越界故意制造了一个运行时错误,打开error.log后能看到文件里已经记录到这个运行时错误及其Stack trace

添加访问日志

Server对象可以设置错误日志的记录器不一样,访问日志只能是我们通过自己编写中间件的方式来实现了。在记录访问日志的中间件里我们会记录ipmethodpathqueryrequest_bodystatusresponse_body这些个字段的内容。

statusresponse_body两个字段来自请求对应的响应。响应在net/http包里是用http.ResponseWriter接口表示的

type ResponseWriter interface {
    Header() Header

    Write([]byte) (int, error)

    WriteHeader(statusCode int)
}

接口本身以及net/http提供的实现都没有让我们进行读取的方法,所以在编写的用于记录访问日志的中间件里需要对net/http库本身实现的ResponseWriter做一层包装。

利用Go语言结构体类型嵌套匿名类型后,结构体拥有了被嵌套类型的所有导出字段和方法的特性,我们可以很方便地对原来的ResponseWriter做一层包装,然后只重新实现需要更改的方法即可:

type ResponseWithRecorder struct {
   http.ResponseWriter
   statusCode int
   body bytes.Buffer
}

func (rec *ResponseWithRecorder) WriteHeader(statusCode int) {
   rec.ResponseWriter.WriteHeader(statusCode)
   rec.statusCode = statusCode
}

func (rec *ResponseWithRecorder) Write(d []byte) (n int, err error) {
   n, err = rec.ResponseWriter.Write(d)
   if err != nil {
      return
   }
   rec.body.Write(d)

   return
}

定义好新的类型后我们重新实现了WriteHeaderWrite方法,在向原来的ReponseWriter中写入后也会向ResponseWriteRecoder.statusCodeResponseWriteRecoder.body写入对应的数据。这样我们就可以在中间件里通过这两个字段访问响应码和响应数据了。

记录访问日志的中间件定义如下:

func AccessLogging (f http.Handler) http.Handler {

    // 创建一个新的handler包装http.HandlerFunc
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        buf := new(bytes.Buffer)
        buf.ReadFrom(r.Body)
        logEntry := vlog.AccessLog.WithFields(logrus.Fields{
            "ip": r.RemoteAddr,
            "method": r.Method,
            "path": r.RequestURI,
            "query": r.URL.RawQuery,
            "request_body": buf.String(),

        })

        wc := &ResponseWithRecorder{
            ResponseWriter: w,
            statusCode: http.StatusOK,
            body: bytes.Buffer{},
        }

        // 调用下一个中间件或者最终的handler处理程序
        f.ServeHTTP(wc, r)

        defer logEntry.WithFields(logrus.Fields{
            "status": wc.statusCode,
            "response_body": wc.body.String(),
        }).Info()

    })
}

Router上应用创建好的AccessLogging中间件后,就可以正常的记录服务器的访问日志了。

// router/router.go
func RegisterRoutes(r *mux.Router) {
    ...
    // apply Logging middleware
    r.Use(middleware.Logging(), middleware.AccessLogging)
    ...
}

不过有两点需要注意一下

  • 这里为了演示获取响应数据记录了response_body字段,如果是接口响应内容记录下还可以,但是如果是HTML还是不记录的为好。
  • 初始化ResponseWithRecorder时默认设置了statusCode时因为,服务器正确返回响应时不会显式调用WriteHeader方法,只有在返回NOT_FOUND之类的错误的时候才会调用WriteHeader方法,针对这种情况需要在初始化的时候把statusCode的默认值设置为200

现在再访问服务器后打开access.log会看到刚刚的访问日志,就能看到刚刚请求的urlmethod,客户端IP等信息了。

{"ip":"......","level":"info","method":"GET","msg":"","path":"/index/","query":"","request_body":"","response_body":"Hello World1","status":200,"time":"2020-03-26T04:21:46Z"}

注意:文章只为说明演示方便,获取IP的方法无法获取代理后的真实IP,请悉知。

前文回顾

深入学习用Go编写HTTP服务器

Go Web 编程--超详细的模板库应用指南

Go Web编程--使用Go语言创建静态文件服务器

Go Web编程--SecureCookie实现客户端Session管理

关于如何收集,标准化和集中化处理Golang日志的一些建议

查看原文

赞 5 收藏 2 评论 0

ACChe 赞了文章 · 3月17日

Go Web编程--SecureCookie实现客户端Session管理

f1786a17fea647790228ee7a47bc02d649da5fda.jpg
Web应用开发中Session是在用户和服务器之间进行交换的非持久化交互信息。当用户登录时,可以在用户和服务器之间生成Session,然后来回交换数据,并在用户登出时销毁Sessiongorilla/sessions软件包提供了易于使用的Go语言Session实现。该软件包提供了两种不同的实现。第一个是文件系统存储,它将每个会话存储在服务器的文件系统中。另一个是Cookie存储,它使用我们上篇文章讲的SecureCookie在客户端上存储会话。同时还提供了用户自定义Session存储实现的选项,我们可以根据应用的需求自己实现Session存储。因为我们的教程是学会使用为目的就不大费周章的去实现MySQL或者Redis版本的Session存储了,我们直接使用软件包提供的Cookie实现来完成本节的Session相关内容。

Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复gohttp09获取本文源代码

使用Cookie存储用户Session的优缺点

客户端使用Cookie管理用户Session较之在服务器进行用户的Session管理会有一些优势。客户端Session增加了应用程序的可伸缩性,因为所有的会话数据都存储在用户端,因此可以将用户的请求平衡到不同的远端服务器,也不必在服务器端对所有用户的会话进行统一管理,所以使用Cookie存储用户Session会更简单一些。

当然有优势就必定有劣势,客户端Cookie的整体大小是有限制的。目前,Google Chrome浏览器将Cookie限制为4096个字节。

客户端会话还意味着无法终止会话,从而导致注销不完整。如果用户在退出前保存了Cookie中的会话信息,则他们可以使用该会话信息创建一个新的Cookie,然后继续使用该应用程序,为了最大程度地降低安全风险,我们可以将会话Cookie设置为在合理的时间内过期,使用加密后的ScureCookie存储数据,同时还要避免在其中存储敏感信息(即使是服务端管理Session也不应该存储类似密码这种敏感信息)。

总之在考虑使用客户端还是服务端存储用户Session时一定要根据应用的使用场景来选择,这一点很重要。

安装gorilla/sessions

在开始编码前先来安装一下gorilla/sessions软件包,

$ go get github.com/gorilla/sessions

并简单看一下软件包功能特性的介绍

  • 方便地设置签名(也可以选择加密)的Cookie
  • 自带将会话存储在Cookie或服务端文件系统中的SessionStore实现。
  • 支持Flash消息:读取即销毁的会话数据。
  • 支持方便地切换会话数据的持久化方式。
  • 为不同的Session存储提供统一的接口和基础设施。

演示用户Session设计实现

我们今天的示例代码是用gorilla/sessions提供的CookieSessionStore实现一个简单的系统登录功能。

我们会定义如下几个路由:

  • /user/login 用户登录验证,验证成功后在用户Session数据中标记用户是已验证的。
  • /user/logout 用户登出,会在Session中标记用户是未认证的。
  • /user/secret 通过用户Session判断用户是否已认证,未认证返回403 Forbidden错误。

为了达到演示目的的同时减少文章中出现过多代码,我们不会做前端页面,通过命令行cURL直接请求上面几个URL验证我们的系统登录功能。

初始化工作

我们现在项目的handler目录下新建一个user子目录,用于存放使用到用户Session的处理程序

...
handler/
└── user/
    └── init.go
    └── login.go
    └── logout.go
    └── secret.go
...
main.go

其下的四个分别是包的初始化程序init.go以及存放上面说的三个路由处理程序的.go源文件。

初始化Session存储

我们把Session存储的初始化工作放在user包的init函数中,这样首次导入user包时即可完成相关的初始化工作。

package user

import "github.com/gorilla/sessions"

const (
    //64位
    cookieStoreAuthKey = "..."
    //AES encrypt key必须是16或者32位
    cookieStoreEncryptKey = "..."
)

var sessionStore *sessions.CookieStore

func init () {
    sessionStore = sessions.NewCookieStore(
        []byte(cookieStoreAuthKey),
        []byte(cookieStoreEncryptKey),
    )

    sessionStore.Options = &sessions.Options{
        HttpOnly: true,
        MaxAge:   60 * 15,
    }

}

实现登录验证

// login.go
var sessionCookieName = "user-session"
func Login(w http.ResponseWriter, r *http.Request) {
    session, err := sessionStore.Get(r, sessionCookieName)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    // 登录验证
    name := r.FormValue("name")
    pass := r.FormValue("password")
    _, err = logic.AuthenticateUser(name, pass)
    if err != nil {
        http.Error(w, err.Error(), http.StatusUnauthorized)
        return
    }
    // 在session中标记用户已经通过登录验证
    session.Values["authenticated"] = true
    err = session.Save(r, w)

    fmt.Fprintln(w, "登录成功!", err)
}
  • 我们将浏览器Cookie中存储用户SessionCookie-Name设置成了user-session
  • 登录验证就是简单的用户名和密码查找匹配的用户,在之前的文章应用数据库应用 ORM两篇文章中有在MySQL数据库中创建users表,并介绍了怎么使用ORM操作数据库,没有看过的同学可以回看一下。
  • 登录验证成功后在Sessionauthenticated中标记了用户已通过认证。session.Values是类型map[interface{}]interface{}的别名,所以可以往其中存储任意类型的数据。

实现登出

登出我们这里就是简单的将Sessionauthenticated的值设置成了false.

//logout.go
func Logout(w http.ResponseWriter, r *http.Request) {
   session, _ := sessionStore.Get(r, sessionCookieName)
   
   session.Values["authenticated"] = false
   session.Save(r, w)
}

使用Session认证用户

//secret.go
func Secret(w http.ResponseWriter, r *http.Request) {
   session, _ := sessionStore.Get(r, sessionCookieName)

   if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
      http.Error(w, "Forbidden", http.StatusForbidden)
      return
   }

   fmt.Fprintln(w, "这里还是空空如也!")
}
  • 使用Session中存储的数据值都是接口类型的,所以使用时要先对其进行类型断言session.Values["authenticated"].(bool)
  • 如果authenticated的值不为true或者是从Session中获取不到对应的值,这里直接返回HTTP 403 Forbidden错误。

注册路由

// router.go
func RegisterRoutes(r *mux.Router) {
  ...
  userRouter := r.PathPrefix("/user").Subrouter()
  userRouter.HandleFunc("/login", user.Login).Methods("POST")
  userRouter.HandleFunc("/secret", user.Secret)
  userRouter.HandleFunc("/logout", user.Logout)
  ...
}

验证已实现的Session管理功能

编写完上面的Session管理的功能后,重启服务器,然后使用cURL分别请求URL验证一下效果。

curl -XPOST   -d 'name=Klein&password=123' \
     -c - http://localhost:8000/user/login

-c选项表示将Cookie写入到后面的文件中,完整格式是-c -<file_name>,短横线后不带文件名表示把Cookie写入到标准输出中。

我们可以在下图里看到,Cookie中的user-session存储的就是加密后的Session数据了

图片

如果请求中不携带这个Cookie访问/user/secret会直接返回HTTP 403错误

图片

那么接下来在使用cURL请求/user/secret时带上上面返回的Cookie值,看看请求是否能成功

curl --cookie "user-session=MTU4m..." http://localhost:8000/user/secret

图片

Cookie加密后的值太长了,搞得字儿好小,cURL执行的结果显示服务器成功地响应了我们的请求。你们试验的时候换成自己生成的Cookie值请求就可以啦。

你们实践时也可以用PostMan代替cURL试验,不过感觉PostMan的返回不如cURL来的明显。

Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复gohttp09获取本文源代码

前文回顾

Go Web 编程--如何确保Cookie数据的安全传输

Go Web编程--应用ORM

Go Web编程--应用ORM

五分钟用Docker快速搭建Go开发环境

深入学习用Go编写HTTP服务器

查看原文

赞 1 收藏 0 评论 0

ACChe 关注了专栏 · 2月17日

网管叨bi叨

公众号:网管叨bi叨 | Golang、PHP、Laravel、Docker等学习经验分享

关注 833

ACChe 收藏了文章 · 2月4日

golang结构体指针解析

golang指针可以这样理解:本身为一个整型常量,但由于其声明时为指针,因此拥有了特殊的能力,即在其前增加 * ,即可直接访问内存编号为该整型常量的数据。而对于某个定义的常量,在前面加 & ,也可以获得数据的内存编号。
当我们定义结构体指针时,问题出现了:

type name int8
type first struct {
    a int
    b bool
    name
}
func main(){
    var type1 = first{1, false, 2}
    var type_pointer *first = &type1
    fmt.Println(type_pointer)
    }

上面的代码打印的结果为:
&{1 false 2}
由此可推论,结构体本身并没有代表其自身的内存地址,存在内存地址的是结构体内部的数据信息,只得打印出这样的数据。

因此,我们进行如下打印:

fmt.Println(&type_pointer.a, type_pointer.a, &type_pointer, (*type_pointer).a)

输出结果为:
0x110120f8 1 0x11006128 1

可以这样解释:
&type_pointer.a:等同于&(*type_pointer).a
type_pointer.a:等同于(*type_pointer).a
上面这两种可以理解为golang语言的简便写法写法(如果不这么理解没法解释输出),不推荐这样写,
&type_pointer:指针本身的内存地址
(*type_pointer).a:指针指向的结构体,该结构体内部的a数据

此外,类似于*type_pointer.a这种写法是非法的。

查看原文

ACChe 赞了文章 · 2月4日

golang结构体指针解析

golang指针可以这样理解:本身为一个整型常量,但由于其声明时为指针,因此拥有了特殊的能力,即在其前增加 * ,即可直接访问内存编号为该整型常量的数据。而对于某个定义的常量,在前面加 & ,也可以获得数据的内存编号。
当我们定义结构体指针时,问题出现了:

type name int8
type first struct {
    a int
    b bool
    name
}
func main(){
    var type1 = first{1, false, 2}
    var type_pointer *first = &type1
    fmt.Println(type_pointer)
    }

上面的代码打印的结果为:
&{1 false 2}
由此可推论,结构体本身并没有代表其自身的内存地址,存在内存地址的是结构体内部的数据信息,只得打印出这样的数据。

因此,我们进行如下打印:

fmt.Println(&type_pointer.a, type_pointer.a, &type_pointer, (*type_pointer).a)

输出结果为:
0x110120f8 1 0x11006128 1

可以这样解释:
&type_pointer.a:等同于&(*type_pointer).a
type_pointer.a:等同于(*type_pointer).a
上面这两种可以理解为golang语言的简便写法写法(如果不这么理解没法解释输出),不推荐这样写,
&type_pointer:指针本身的内存地址
(*type_pointer).a:指针指向的结构体,该结构体内部的a数据

此外,类似于*type_pointer.a这种写法是非法的。

查看原文

赞 2 收藏 2 评论 0

ACChe 提出了问题 · 1月28日

多个项目应该怎样配置数据库的docker?

一开始写了一个项目,用docker-compose来创建了api的服务、数据库和nginx(分别三个docker容器),现在想要做另外一个项目,应该再新建一个数据库docker容器来配合现在的项目吗?可以还是用第一个项目所建的数据库容器来用吗?

关注 1 回答 0

ACChe 赞了回答 · 1月7日

docker-compose每次up之后,之前在容器中所做的修改保不保留?

你肯定是用了 容器卷 ,删除卷容器就好了

关注 6 回答 4

认证与成就

  • 获得 7 次点赞
  • 获得 116 枚徽章 获得 8 枚金徽章, 获得 51 枚银徽章, 获得 57 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-07-09
个人主页被 320 人浏览