说明

在服务端编程时,有时候需要一个很重要的功能便是客户身份验证,而一种通常的验证方法是白名单:只有在白名单列表中的客户端才可以被允许访问。

因为服务端提供的API很多,因此,必须在每个API实现中都需要去检查客户端权限,这种实现方法有以下问题:

  • 违背了DRY原则,所有的白名单检查代码其实都是一模一样;
  • 将身份认证和业务逻辑耦合的比较紧密了,如果我们需要将白名单变成另外一种实现,修改的地方会很多

一种新的实现方式

为了避免代码中的这种紧耦合,我们最好把身份认证和业务逻辑解耦,白名单不是业务逻辑核心范畴,同时也不是唯一身份认证方案,因此,这种解耦合情合理。

以下实现是一种比较优雅的解耦实现:

func NewMasterServer(r *mux.Router, port int, metaFolder string,
    ......
) *MasterServer {
    ms := &MasterServer{
        port:                    port,
        volumeSizeLimitMB:       volumeSizeLimitMB,
        pulseSeconds:            pulseSeconds,
        ......
    }
    ms.bounedLeaderChan = make(chan int, 16)
    seq := sequence.NewMemorySequencer()
    var e error
    if ms.Topo, e = topology.NewTopology("topo", confFile, seq,
        uint64(volumeSizeLimitMB)*1024*1024, pulseSeconds); e != nil {
        glog.Fatalf("cannot create topology:%s", e)
    }
    ms.vg = topology.NewDefaultVolumeGrowth()

    // 使用Guard来包装每个业务Handler
    ms.guard = security.NewGuard(whiteList, secureKey)
    r.HandleFunc("/", ms.uiStatusHandler)
    r.HandleFunc("/ui/index.html", ms.uiStatusHandler)
    r.HandleFunc("/dir/assign", ms.proxyToLeader(ms.guard.WhiteList(ms.dirAssignHandler)))
    r.HandleFunc("/dir/lookup", ms.proxyToLeader(ms.guard.WhiteList(ms.dirLookupHandler)))
    r.HandleFunc("/dir/join", ms.proxyToLeader(ms.guard.WhiteList(ms.dirJoinHandler)))
    r.HandleFunc("/dir/status", ms.proxyToLeader(ms.guard.WhiteList(ms.dirStatusHandler)))
    r.HandleFunc("/col/delete", ms.proxyToLeader(ms.guard.WhiteList(ms.collectionDeleteHandler)))
    r.HandleFunc("/vol/lookup", ms.proxyToLeader(ms.guard.WhiteList(ms.volumeLookupHandler)))
    r.HandleFunc("/vol/grow", ms.proxyToLeader(ms.guard.WhiteList(ms.volumeGrowHandler)))
    r.HandleFunc("/vol/status", ms.proxyToLeader(ms.guard.WhiteList(ms.volumeStatusHandler)))
    r.HandleFunc("/vol/vacuum", ms.proxyToLeader(ms.guard.WhiteList(ms.volumeVacuumHandler)))
    r.HandleFunc("/submit", ms.guard.WhiteList(ms.submitFromMasterServerHandler))
    r.HandleFunc("/delete", ms.guard.WhiteList(ms.deleteFromMasterServerHandler))
    r.HandleFunc("/{fileId}", ms.proxyToLeader(ms.redirectHandler))
    r.HandleFunc("/stats/counter", ms.guard.WhiteList(statsCounterHandler))
    r.HandleFunc("/stats/memory", ms.guard.WhiteList(statsMemoryHandler))

    ms.Topo.StartRefreshWritableVolumes(garbageThreshold)

    return ms
}

上面的代码启动了一个HTTPServer,并且注册了每个uri对应的处理handler,即系统业务代码。

但是上述实现并不是在每个handler内部写白名单检查,而是将白名单检查功能封装成Guard,来包装handler:

ms.guard.WhiteList(statsCounterHandler))

我们来看看Guard的实现

func (g *Guard) WhiteList(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
    if !g.isActive {
        //if no security needed, just skip all checkings
        return f
    }
    return func(w http.ResponseWriter, r *http.Request) {
        if err := g.checkWhiteList(w, r); err != nil {
            w.WriteHeader(http.StatusUnauthorized)
            return
        }
        f(w, r)
    }
}

由于使用了WhiteList来包装业务逻辑,每个uri在处理时首先会进入WhiteList处理,在checkWhiteList()检查通过后方才会进入核心业务处理。

总结

这里的WhiteList只是一个简单示例,其实现实编程中还有很多类似的场景,遇到类似需求时,我们一定得三思而后写,不妨尝试下合理、适度的封装,定会让你的代码变得更加优美。