在工作中,有这么一个需求,通过后台管理域名的证书自动申请及续签的功能;
方案有两种:
一种是通过官方的golang.org/x/crypto/acme/autocert包来实现域名的申请及续签,这个不用多说。
另一种是通过lego包来实现,重点说明一下这中方案,只实现了http的挑战,需要在路由中设置挑战路径
我这个Echo的路由

// http域名挑战
	s.e.GET("/.well-known/acme-challenge/:token", func(c echo.Context) error {
		fmt.Println("域名挑战......")
		token := c.Param("token")
		exist, value := cert.NewMemoryProviderServer().GetKeyAuth("", token)
		fmt.Printf("exist:%v, token:%v, value:%v", exist, token, value)
		if !exist {
			return errors.New("挑战失败")
		}
		c.Response().Writer.Write([]byte(value))
		return nil
	}).Name = "域名申请挑战"

接下来是对lego的封装,http-001挑战需要实现Present,CleanUp这两个方法:

var httpData *MemoryProviderServer
var once1 sync.Once

type MemoryProviderServer struct {
	data map[string]string 
	lock sync.Mutex
}

func NewMemoryProviderServer() *MemoryProviderServer {
	once1.Do(func() {
		httpData = &MemoryProviderServer{
			data: map[string]string{},
		}
	})
	return httpData
}

func (s *MemoryProviderServer) Present(domain, token, keyAuth string) error {
	s.lock.Lock()
	fmt.Printf("保存token:%v,value:%v\n", token, keyAuth)
	s.data[token] = keyAuth
	s.lock.Unlock()
	return nil
}

func (s *MemoryProviderServer) CleanUp(domain, token, keyAuth string) error {
	s.lock.Lock()
	delete(s.data, domain+token)
	fmt.Printf("清空token:%v\n", token)
	s.lock.Unlock()
	return nil
}

func (s *MemoryProviderServer) GetKeyAuth(domain, token string) (exist bool, keyAuth string) {
	s.lock.Lock()
	fmt.Printf("domain data:%+v", s.data)
	keyAuth, exist = s.data[token]
	s.lock.Unlock()
	return
}

接下来是lets encrypt账号注册,注同一个ip一定时间内注册账号是有限制的,所以需要将账号信息缓存起来,比如存redis,db等,这里是直接存的数据库,表结构如下:

type LetsEncrypt struct {
	ID           int64  `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
	Email        string `gorm:"column:email;type:varchar(191);uniqueIndex:idx_email;not null;default:'';comment:email" json:"email"`
	PrivateKey   string `gorm:"column:private_key;type:text;comment:账号key" json:"privateKey"`
	Registration string `gorm:"column:registration;type:text;comment:lets Encrypt注册信息" json:"registration"`
	CreateAt     int64  `gorm:"column:create_at;not null;autoCreateTime" json:"createAt"`
	UpdateAt     int64  `gorm:"column:update_at;not null;autoUpdateTime" json:"updateAt"`
}

缓存实现:

type DataCache interface {
	SetKey(key crypto.PrivateKey) error
	GetKey() crypto.PrivateKey
	SetRegistration(reg *registration.Resource) error
	GetRegistration() *registration.Resource
	SetEmail(email string) error
}

type DBCache struct {
	Email        string
	Key          crypto.PrivateKey
	Registration *registration.Resource
	Data         *models.LetsEncrypt
}

func (d *DBCache) SetKey(key crypto.PrivateKey) error {
	d.Key = key
	d.Data.PrivateKey = string(certcrypto.PEMEncode(d.Key))
	return d.saveData()
}

func (d *DBCache) GetKey() crypto.PrivateKey {
	return d.Key
}

func (d *DBCache) SetRegistration(reg *registration.Resource) error {
	d.Registration = reg
	marshal, err := json.Marshal(d.Registration)
	if err != nil {
		return err
	}
	d.Data.Registration = string(marshal)
	return d.saveData()
}

func (d *DBCache) GetRegistration() *registration.Resource {
	return d.Registration
}

func (d *DBCache) SetEmail(email string) error {
	d.Email = email
	// 查询数据库是否存在
	var account models.LetsEncrypt
	if err := database.GetGormDb().Where(models.LetsEncrypt{Email: d.Email}).FirstOrCreate(&account).Error; err != nil {
		log.Error(err)
		return err
	}
	d.Data = &account
	// 解析key
	if d.Data.PrivateKey != "" {
		key, err := certcrypto.ParsePEMPrivateKey([]byte(d.Data.PrivateKey))
		if err != nil {
			log.Info("初始化key错误")
			return err
		}
		d.Key = key
	}
	// 解析registration
	if d.Data.Registration != "" {
		var reg registration.Resource
		if err := json.Unmarshal([]byte(d.Data.Registration), &reg); err != nil {
			log.Info("初始化registration错误")
			return err
		}
		d.Registration = &reg
	}
	return nil
}
func (d *DBCache) saveData() error {
	return database.GetGormDb().Save(d.Data).Error
}

最后实现lego的封装

type user struct {
	//	email 邮箱
	email string
	//	registration 表示已在ACME服务器上注册的帐户信息
	registration *registration.Resource
	//	key 表示ECDSA私钥
	key crypto.PrivateKey
}

func (u *user) GetEmail() string {
	return u.email
}
func (u *user) GetRegistration() *registration.Resource {
	return u.registration
}
func (u *user) GetPrivateKey() crypto.PrivateKey {
	return u.key
}

type LetsEncrypt struct {
	User    *user
	Client  *lego.Client
	Cache   DataCache
	Account string
}

var letsEncrypt *LetsEncrypt
var once sync.Once
var defaultEmail = "xxx@163.com"
var dao DataCache

func NewLetsEncrypt(email string) *LetsEncrypt {
	once.Do(func() {
		if email == "" {
			email = defaultEmail
		}
		dao = &DBCache{}
		if err := dao.SetEmail(email); err != nil {
			log.Error(err)
			return
		}
		letsEncrypt = &LetsEncrypt{
			User: &user{
				email: email,
			},
			Cache: dao,
		}

		letsEncrypt.newKey()
		letsEncrypt.newRegistration()
	})
	return letsEncrypt
}

// 生成lets encrypt 注册账号private key
func (l *LetsEncrypt) newKey() {
	if l.Cache.GetKey() != nil {
		l.User.key = l.Cache.GetKey()
		return
	}

	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		log.Error(err)
	}
	l.User.key = key
	log.Info("设置key缓存")
	if err = l.Cache.SetKey(key); err != nil {
		log.Error(err)
	}
}

// 生成lets encrypt的注册信息
// newClient将会执行2次,第一次用于注册账号信息,第二次解决No key Id in jws的bug,/lego/client.go在45行
//
func (l *LetsEncrypt) newRegistration() {
	var err error
	l.newClient()
	if l.Cache.GetRegistration() != nil {
		l.User.registration = l.Cache.GetRegistration()
		l.newClient()
		return
	}
	reg, err := l.Client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
	if err != nil {
		log.Error(err)
		return
	}
	l.User.registration = reg
	l.newClient()
	// todo 保存信息
	if l.User.registration.Body.Status != "valid" {
		err = errors.New("Returning registration status not valid ")
		return
	}
	log.Info("保存registration")
	if err = l.Cache.SetRegistration(reg); err != nil {
		log.Error(err)
		return
	}
	return
}

const (
	//	CALetsEncryptDebug(默认) Let's Encrypt 测试地址
	CALetsEncryptDebug = "https://acme-staging-v02.api.letsencrypt.org/directory"
	//	CALetsEncrypt Let's Encrypt 正式地址
	CALetsEncrypt = "https://acme-v02.api.letsencrypt.org/directory"
)

func (l *LetsEncrypt) newClient() {
	var err error
	cfg := lego.NewConfig(l.User)
	cfg.CADirURL = CALetsEncrypt
	cfg.Certificate.KeyType = certcrypto.RSA2048
	client, err := lego.NewClient(cfg)
	if err != nil {
		log.Error(err)
		return
	}
	l.Client = client
	return
}
func (l *LetsEncrypt) SetProvider(typ challenge.Type) error {
	var err error
	switch typ {
	case challenge.DNS01:
		err = l.Client.Challenge.SetDNS01Provider(NewDNSProviderBestDNS())
	case challenge.HTTP01:
		err = l.Client.Challenge.SetHTTP01Provider(NewMemoryProviderServer())
	default:
		err = errors.New("Unknown Challenge types ")
	}

	return err
}

func (l *LetsEncrypt) Obtain(domains []string) ([]byte, []byte, error) {
	if len(domains) == 0 {
		return nil, nil, errors.New("the domain cannot be empty")
	}
	var (
		err error
		res *certificate.Resource
	)
	request := certificate.ObtainRequest{
		Domains: domains,
		Bundle:  true,
		//	可以在privateKey参数中提供自己的私钥
	}

	times := 1
	for times > 0 {
		times--
		if res, err = l.Client.Certificate.Obtain(request); err != nil {
			log.Info(err)
			if err = l.SetProvider(challenge.HTTP01); err != nil {
				continue
				//return nil, nil, err
			}
		}
		break
	}

	fmt.Printf("cert:%v,key:%v", string(res.Certificate), string(res.PrivateKey))
	return res.Certificate, res.PrivateKey, err
}

func (l *LetsEncrypt) Renew(ce, privateKey []byte) ([]byte, []byte, error) {
	//	私钥重用的话 privateKey 不为空,反之则重新生成
	var (
		res *certificate.Resource
		err error
	)

	if res, err = l.Client.Certificate.Renew(certificate.Resource{
		PrivateKey:  privateKey,
		Certificate: ce,
	}, true, true, ""); err != nil {
		return nil, nil, err
	}
	return res.Certificate, res.PrivateKey, nil
}

最后使用,c,k就是公钥和私钥

    certService := NewLetsEncrypt("")
	obmains := []string{"xxx.com","xxx2.com"}
	c, k, err := certService.Obtain(obmains);

完成!搞定
就可以把拿到的公钥和私钥去配置https了.