mirror of
https://github.com/jeessy2/ddns-go.git
synced 2025-10-20 15:33:46 +08:00
feat: reset password (#1119)
- Encrypt password using bcrypt - Add reset password function
This commit is contained in:
@ -51,6 +51,7 @@
|
||||
- `-noweb` 不启动web服务
|
||||
- `-skipVerify` 跳过证书验证
|
||||
- `-dns` 自定义 DNS 服务器
|
||||
- `-resetPassword` 重置密码
|
||||
- [可选] 参考示例
|
||||
- 10分钟同步一次, 并指定了配置文件地址
|
||||
```bash
|
||||
@ -60,6 +61,10 @@
|
||||
```bash
|
||||
./ddns-go -s install -f 10 -cacheTimes 180
|
||||
```
|
||||
- 重置密码
|
||||
```bash
|
||||
./ddns-go -resetPassword 123456
|
||||
```
|
||||
- [可选] 使用 [Homebrew](https://brew.sh) 安装 [ddns-go](https://formulae.brew.sh/formula/ddns-go):
|
||||
|
||||
```bash
|
||||
|
@ -49,6 +49,7 @@ Automatically obtain your public IPv4 or IPv6 address and resolve it to the corr
|
||||
- `-noweb` does not start web service
|
||||
- `-skipVerify` skip certificate verification
|
||||
- `-dns` custom DNS server
|
||||
- `-resetPassword` reset password
|
||||
- [Optional] Examples
|
||||
- 10 minutes to synchronize once, and the configuration file address is specified
|
||||
```bash
|
||||
@ -58,6 +59,10 @@ Automatically obtain your public IPv4 or IPv6 address and resolve it to the corr
|
||||
```bash
|
||||
./ddns-go -s install -f 10 -cacheTimes 180
|
||||
```
|
||||
- reset password
|
||||
```bash
|
||||
./ddns-go -resetPassword 123456
|
||||
```
|
||||
- [Optional] You can use [Homebrew](https://brew.sh) to install [ddns-go](https://formulae.brew.sh/formula/ddns-go)
|
||||
|
||||
```bash
|
||||
|
@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
@ -12,6 +13,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/jeessy2/ddns-go/v6/util"
|
||||
passwordvalidator "github.com/wagslane/go-password-validator"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@ -116,13 +118,22 @@ func GetConfigCached() (conf Config, err error) {
|
||||
return *cache.ConfigSingle, err
|
||||
}
|
||||
|
||||
// CompatibleConfig 兼容v5.0.0之前的配置文件
|
||||
// CompatibleConfig 兼容之前的配置文件
|
||||
func (conf *Config) CompatibleConfig() {
|
||||
// 如配置文件不为空, 兼容之前的语言为中文
|
||||
if conf.Lang == "" {
|
||||
conf.Lang = "zh"
|
||||
}
|
||||
|
||||
// 如果之前密码不为空且不是bcrypt加密后的密码, 把密码加密并保存
|
||||
if conf.Password != "" && !util.IsHashedPassword(conf.Password) {
|
||||
hashedPwd, err := util.HashPassword(conf.Password)
|
||||
if err == nil {
|
||||
conf.Password = hashedPwd
|
||||
conf.SaveConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容v5.0.0之前的配置文件
|
||||
if len(conf.DnsConf) > 0 {
|
||||
return
|
||||
@ -177,6 +188,43 @@ func (conf *Config) SaveConfig() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
func (conf *Config) ResetPassword(newPassword string) {
|
||||
// 初始化语言
|
||||
util.InitLogLang(conf.Lang)
|
||||
|
||||
// 先检查密码是否安全
|
||||
hashedPwd, err := conf.CheckPassword(newPassword)
|
||||
if err != nil {
|
||||
util.Log(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
conf.Password = hashedPwd
|
||||
conf.SaveConfig()
|
||||
util.Log("用户名 %s 的密码已重置成功! 请重启ddns-go", conf.Username)
|
||||
}
|
||||
|
||||
// CheckPassword 检查密码
|
||||
func (conf *Config) CheckPassword(newPassword string) (hashedPwd string, err error) {
|
||||
var minEntropyBits float64 = 50
|
||||
if conf.NotAllowWanAccess {
|
||||
minEntropyBits = 25
|
||||
}
|
||||
err = passwordvalidator.Validate(newPassword, minEntropyBits)
|
||||
if err != nil {
|
||||
return "", errors.New(util.LogStr("密码不安全!尝试使用更复杂的密码"))
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPwd, err = util.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return "", errors.New(util.LogStr("异常信息: %s", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (conf *DnsConfig) getIpv4AddrFromInterface() string {
|
||||
ipv4, _, err := GetNetInterface()
|
||||
if err != nil {
|
||||
|
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ require (
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
golang.org/x/net v0.25.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
golang.org/x/crypto v0.23.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
2
go.sum
2
go.sum
@ -2,6 +2,8 @@ github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX
|
||||
github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
24
main.go
24
main.go
@ -33,16 +33,16 @@ var updateFlag = flag.Bool("u", false, "Upgrade ddns-go to the latest version")
|
||||
var listen = flag.String("l", ":9876", "Listen address")
|
||||
|
||||
// 更新频率(秒)
|
||||
var every = flag.Int("f", 300, "Sync frequency(seconds)")
|
||||
var every = flag.Int("f", 300, "Update frequency(seconds)")
|
||||
|
||||
// 缓存次数
|
||||
var ipCacheTimes = flag.Int("cacheTimes", 5, "Interval N times compared with service providers")
|
||||
var ipCacheTimes = flag.Int("cacheTimes", 5, "Cache times")
|
||||
|
||||
// 服务管理
|
||||
var serviceType = flag.String("s", "", "Service management (install|uninstall|restart)")
|
||||
|
||||
// 配置文件路径
|
||||
var configFilePath = flag.String("c", util.GetConfigFilePathDefault(), "config file path")
|
||||
var configFilePath = flag.String("c", util.GetConfigFilePathDefault(), "Custom configuration file path")
|
||||
|
||||
// Web 服务
|
||||
var noWebService = flag.Bool("noweb", false, "No web service")
|
||||
@ -51,7 +51,10 @@ var noWebService = flag.Bool("noweb", false, "No web service")
|
||||
var skipVerify = flag.Bool("skipVerify", false, "Skip certificate verification")
|
||||
|
||||
// 自定义 DNS 服务器
|
||||
var customDNS = flag.String("dns", "", "Custom DNS server, example: 8.8.8.8")
|
||||
var customDNS = flag.String("dns", "", "Custom DNS server address, example: 8.8.8.8")
|
||||
|
||||
// 重置密码
|
||||
var newPassword = flag.String("resetPassword", "", "Reset password to the one entered")
|
||||
|
||||
//go:embed static
|
||||
var staticEmbeddedFiles embed.FS
|
||||
@ -72,17 +75,28 @@ func main() {
|
||||
update.Self(version)
|
||||
return
|
||||
}
|
||||
// 检查监听地址
|
||||
if _, err := net.ResolveTCPAddr("tcp", *listen); err != nil {
|
||||
log.Fatalf("Parse listen address failed! Exception: %s", err)
|
||||
}
|
||||
// 设置版本号
|
||||
os.Setenv(web.VersionEnv, version)
|
||||
// 设置配置文件路径
|
||||
if *configFilePath != "" {
|
||||
absPath, _ := filepath.Abs(*configFilePath)
|
||||
os.Setenv(util.ConfigFilePathENV, absPath)
|
||||
}
|
||||
// 重置密码
|
||||
if *newPassword != "" {
|
||||
conf, _ := config.GetConfigCached()
|
||||
conf.ResetPassword(*newPassword)
|
||||
return
|
||||
}
|
||||
// 设置跳过证书验证
|
||||
if *skipVerify {
|
||||
util.SetInsecureSkipVerify()
|
||||
}
|
||||
// 设置自定义DNS
|
||||
if *customDNS != "" {
|
||||
util.SetDNS(*customDNS)
|
||||
}
|
||||
@ -118,7 +132,7 @@ func main() {
|
||||
}
|
||||
|
||||
func run() {
|
||||
// 兼容v5.0.0之前的配置文件
|
||||
// 兼容之前的配置文件
|
||||
conf, _ := config.GetConfigCached()
|
||||
conf.CompatibleConfig()
|
||||
// 初始化语言
|
||||
|
@ -204,6 +204,7 @@ const I18N_MAP = {
|
||||
'NotAllowWanAccessHelp': 'Default enabled, can prohibit access to this page from the public network',
|
||||
'Username': 'Username',
|
||||
'accountHelp': 'Please enter to protect your information security',
|
||||
'passwordHelp': 'If you need to change the password, please enter it here',
|
||||
'Password': 'Password',
|
||||
'WebhookURLHelp': `
|
||||
<a
|
||||
@ -264,6 +265,7 @@ const I18N_MAP = {
|
||||
'NotAllowWanAccessHelp': '默认启用, 可禁止从公网访问本页面',
|
||||
'Username': '用户名',
|
||||
'accountHelp': '为保护你的信息安全,建议输入',
|
||||
'passwordHelp': '如需修改密码,请在此处输入新密码',
|
||||
'Password': '密码',
|
||||
'WebhookURLHelp': `
|
||||
<a target="blank" href="https://github.com/jeessy2/ddns-go#webhook">点击参考官方 Webhook 说明</a>
|
||||
|
29
util/bcrypt.go
Normal file
29
util/bcrypt.go
Normal file
@ -0,0 +1,29 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HashPassword 密码哈希
|
||||
func HashPassword(password string) (string, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
// PasswordOK 检查密码
|
||||
func PasswordOK(hashedPassword, password string) bool {
|
||||
if hashedPassword == "" && password == "" {
|
||||
return true
|
||||
}
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsHashedPassword 是否是哈希密码
|
||||
func IsHashedPassword(password string) bool {
|
||||
_, err := bcrypt.Cost([]byte(password))
|
||||
return err == nil
|
||||
}
|
@ -116,6 +116,7 @@ func init() {
|
||||
message.SetString(language.English, "%q 登陆成功", "%q login successfully")
|
||||
message.SetString(language.English, "用户名或密码错误", "Username or password is incorrect")
|
||||
message.SetString(language.English, "登录失败次数过多,请等待 %d 分钟后再试", "Too many login failures, please try again after %d minutes")
|
||||
message.SetString(language.English, "用户名 %s 的密码已重置成功! 请重启ddns-go", "The password of username %s has been reset successfully! Please restart ddns-go")
|
||||
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ func LoginFunc(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := config.GetConfigCached()
|
||||
|
||||
// 登陆成功
|
||||
if data.Username == conf.Username && data.Password == conf.Password {
|
||||
if data.Username == conf.Username && util.PasswordOK(conf.Password, data.Password) {
|
||||
ld.ticker.Stop()
|
||||
ld.failedTimes = 0
|
||||
tokenInSystem = util.GenerateToken(data.Username)
|
||||
|
25
web/save.go
25
web/save.go
@ -9,7 +9,6 @@ import (
|
||||
"github.com/jeessy2/ddns-go/v6/config"
|
||||
"github.com/jeessy2/ddns-go/v6/dns"
|
||||
"github.com/jeessy2/ddns-go/v6/util"
|
||||
passwordvalidator "github.com/wagslane/go-password-validator"
|
||||
)
|
||||
|
||||
var startTime = time.Now().Unix()
|
||||
@ -67,29 +66,25 @@ func checkAndSave(request *http.Request) string {
|
||||
}
|
||||
|
||||
conf.NotAllowWanAccess = data.NotAllowWanAccess
|
||||
conf.Username = usernameNew
|
||||
conf.Password = passwordNew
|
||||
conf.WebhookURL = strings.TrimSpace(data.WebhookURL)
|
||||
conf.WebhookRequestBody = strings.TrimSpace(data.WebhookRequestBody)
|
||||
conf.WebhookHeaders = strings.TrimSpace(data.WebhookHeaders)
|
||||
|
||||
// 如果新密码不为空则检查是否够强, 内/外网要求强度不同
|
||||
conf.Username = usernameNew
|
||||
if passwordNew != "" {
|
||||
hashedPwd, err := conf.CheckPassword(passwordNew)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
conf.Password = hashedPwd
|
||||
}
|
||||
|
||||
// 帐号密码不能为空
|
||||
if conf.Username == "" || conf.Password == "" {
|
||||
return util.LogStr("必须输入登录用户名/密码")
|
||||
}
|
||||
|
||||
// 如果密码不为空则检查是否够强, 内/外网要求强度不同
|
||||
if conf.Password != "" {
|
||||
var minEntropyBits float64 = 50
|
||||
if conf.NotAllowWanAccess {
|
||||
minEntropyBits = 25
|
||||
}
|
||||
err = passwordvalidator.Validate(conf.Password, minEntropyBits)
|
||||
if err != nil {
|
||||
return util.LogStr("密码不安全!尝试使用更复杂的密码")
|
||||
}
|
||||
}
|
||||
|
||||
dnsConfFromJS := data.DnsConf
|
||||
var dnsConfArray []config.DnsConfig
|
||||
empty := dnsConf4JS{}
|
||||
|
@ -58,7 +58,7 @@ func Writing(writer http.ResponseWriter, request *http.Request) {
|
||||
err = tmpl.Execute(writer, struct {
|
||||
DnsConf template.JS
|
||||
NotAllowWanAccess bool
|
||||
config.User
|
||||
Username string
|
||||
config.Webhook
|
||||
Version string
|
||||
Ipv4 []config.NetInterface
|
||||
@ -66,7 +66,7 @@ func Writing(writer http.ResponseWriter, request *http.Request) {
|
||||
}{
|
||||
DnsConf: template.JS(getDnsConfStr(conf.DnsConf)),
|
||||
NotAllowWanAccess: conf.NotAllowWanAccess,
|
||||
User: conf.User,
|
||||
Username: conf.User.Username,
|
||||
Webhook: conf.Webhook,
|
||||
Version: os.Getenv(VersionEnv),
|
||||
Ipv4: ipv4,
|
||||
|
@ -574,12 +574,11 @@
|
||||
type="password"
|
||||
name="Password"
|
||||
id="Password"
|
||||
value="{{.Password}}"
|
||||
autocomplete="new-password"
|
||||
aria-describedby="passwordHelp"
|
||||
/>
|
||||
<small
|
||||
data-i18n_html="accountHelp"
|
||||
data-i18n_html="passwordHelp"
|
||||
id="passwordHelp"
|
||||
class="form-text text-muted"
|
||||
></small>
|
||||
|
Reference in New Issue
Block a user