一些声明:
- 这是我的第一篇博文,以下主要以我的笔记为主,希望大家能看懂
- 这事实上是个很简单的东西,只不过自己踏了各种错路弄了好几天,网上有没有一步到位的教程,这里就记录一下。
注意:
- 这里不是指浏览器使用SSL/TLS访问Angular,而是Angular使用SSL/TLS访问Golang
- 如果你的服务器没有域名,请寻找其他CA机构签发证书,Let‘s Encrypt无法签发IP证书
使用场景:
- Angular将登录信息以HTTPS的方式POST到Golang后端服务器
我的环境:
- CentOS 7.3
- Angular和Golang运行在服务器上
- 服务器和域名都交由Cloudflare CDN管理
- Cloudflare 开启SSL:FULL
目录:
- 生成Let's Encrypt密钥
- 配置 Golang的TLS
- Angular进行http.post
- 后端运行结果图
- 一些坑的说明
正文:
-
生成Let's Encrypt密钥
- 安装Certbot-auto
Certbot也可以,只不过certbot-auto会自动处理依赖,何乐而不为
//以下抄自官网
# wget https://dl.eff.org/certbot-auto
# sudo mv certbot-auto /usr/local/bin/certbot-auto
# sudo chown root /usr/local/bin/certbot-auto
# chmod 0755 /usr/local/bin/certbot-auto
# /usr/local/bin/certbot-auto --help
- 生成密钥
这个教程很详细,我就不化简为繁了
此时你得到了需要的文件:
//作为server.crt使用的证书文件
/etc/letsencrypt/live/你的域名/fullchain.pem
//作为server.key使用的密钥文件
/etc/letsencrypt/live/你的域名/privkey.pem
-
配置 Golang的TLS
- 下面以我的登陆代码为例
import {
//HTTPS相关包
"net/http"
"crypto/tls"
"encoding/json"
//Postgresql数据库
"database/sql"
_ "github.com/lib/pq"
//其他
"fmt"
"io/ioutil"
"log"
}
var serverPem string = "/etc/letsencrypt/live/服务器域名/fullchain.pem"
var serverKey string = "/etc/letsencrypt/live/服务器域名/privkey.pem"
var tls_config *tls.Config //最重要的证书信息都存这了
type USER struct{ //用户信息
Username string `json:"username"`
Password string `json:"password"`
}
type FeedBack struct{ // 登录反馈
Code int `json:"code"`
Ok bool `json:"ok"`
}
const( // 数据库配置
host = "localhost"
port = 5432
user = "数据库用户名"
sqlpassword = "数据库密码"
dbname = "数据库名"
)
func main() {
InitTLS()
fmt.Print("开始服务.\n-----------------\n")
//HTTPS.server 监听
mux := http.NewServeMux()
mux.HandleFunc("/api/login", loginHandle)
mux.HandleFunc("/api/register", registerHandle)
//http.ListenAndServe(":2083", mux)
//一个被抛弃的HTTP靓仔默默路过
//http.ListenAndServeTLS(serverPem, serverKey)
//这句HTTPS也不行,可能默认状态有些奇怪的参数在搞怪
//当然你用Golang做客户端的话就没有这些问题了,主要是这里要用Angular
srv := &http.Server{
Addr: ":服务器端口",
Handler: mux,
TLSConfig: tls_config,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
}
log.Fatal(srv.ListenAndServeTLS(serverPem, serverKey))
//一个HTTPS靓仔高调路过
}
func InitTLS() {
fmt.Print("开启TLS认证.\n")
//导入Server的cert和key
cert, err := tls.LoadX509KeyPair(serverPem, serverKey)
if err != nil {
log.Println(err)
return
}
//配置TLS
tls_config = &tls.Config{
Certificates: []tls.Certificate{cert}, //引入cert和key
//ClientAuth: tls.RequireAndVerifyClientCert, //要求验证客户端密钥
//如果你用Angular进行连接,切勿启用该选项,详见下方论述
}
}
/*
HTTPS相关内容上面已全部包含
下面是一些其他无关紧要的东西
可以忽略
*/
// 初始化数据库
func InitDB(){
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable",
host, port, user, sqlpassword, dbname)
db,err := sql.Open("postgres", psqlInfo) // 打开数据库连接
if err != nil{
panic(err)
}
defer db.Close()
err = db.Ping()
if err != nil{
panic(err)
}
fmt.Print("数据库连接成功.\n")
sqlStatement := `CREATE TABLE IF NOT EXISTS "userinfo"(
username VARCHAR(25) not null primary key,
password VARCHAR(25)
);`
_, err = db.Exec(sqlStatement)
if err != nil{
panic(err)
}
fmt.Print("数据库初始化成功.\n")
}
// 验证数据库是否存在用户,在控制台输出结果,没有做进一步处理
func loginHandle(res http.ResponseWriter, req *http.Request){ // 登录处理函数
fmt.Print("##开始登陆...")
res.Header().Set("Access-Control-Allow-Origin", "*") // 设置跨域:允许访问所有域
res.Header().Set("Access-Control-Allow-Headers", "Content-Type") //header的类型
res.Header().Set("content-type", "application/x-www-form-urlencoded")
//application/json会请求两次,第一次为空,引发一次报错后再登陆成功,这里就不用了
body, err := ioutil.ReadAll(req.Body)
if err != nil{
fmt.Print(err)
return
}
var userInfo USER
err = json.Unmarshal(body, &userInfo) // 解析json数据
if err != nil{
fmt.Print(err)
return
}
var fb FeedBack
ok, err := checkLogin(userInfo.Username, userInfo.Password)
if err != nil {
fmt.Print(err.Error())
return
}
if ok {
fb.Code = 200
fb.Ok = true
fbData,err := json.Marshal(fb)
fmt.Print("用户" + userInfo.Username + "登陆成功...")
//fmt.Print(string(fbData))
if err != nil {
fmt.Print("反馈数据解析失败...")
return
}
fmt.Fprintln(res, string(fbData))
} else {
fb.Code = 200
fb.Ok = false
fbData,err := json.Marshal(fb)
fmt.Print(string(fbData))
if err != nil {
fmt.Print("反馈数据解析失败...")
return
}
fmt.Fprintln(res, string(fbData))
}
defer req.Body.Close()
fmt.Print("\n")
}
//将用户信息存入Postgresql数据库
func registerHandle(res http.ResponseWriter, req *http.Request) {
fmt.Print("##开始注册...")
res.Header().Set("Access-Control-Allow-Origin", "*")
res.Header().Set("Access-Control-Allow-Headers", "Content-Type")
res.Header().Set("content-type", "application/x-www-form-urlencoded")
body, err := ioutil.ReadAll(req.Body)
defer req.Body.Close()
if err!= nil{
fmt.Print("获取请求参数失败...", err.Error())
return
}
var userInfo USER
err = json.Unmarshal(body, &userInfo)
if err != nil {
fmt.Print("解析数据失败...", err.Error())
return
}
exist, err := checkDatabase(userInfo.Username)
var fb FeedBack
if exist {
fmt.Print("用户" + userInfo.Username + "已存在...")
fb.Code = 200
fb.Ok = false
} else {
ok, err := checkRegister(userInfo.Username, userInfo.Password)
if err != nil {
fmt.Print("注册失败...", err.Error())
fb.Code = 200
fb.Ok = false
}
if ok {
fb.Code = 200
fb.Ok = true
} else {
fb.Code = 200
fb.Ok = false
}
}
fbData,err := json.Marshal(fb)
if err != nil {
fmt.Print("解析反馈信息错误...", err.Error())
return
}
fmt.Fprintln(res, string(fbData))
fmt.Print("\n")
}
func checkDatabase(username string)(bool, error){
fmt.Print("检查数据库状态...")
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable",
host, port, user, sqlpassword, dbname)
db,err := sql.Open("postgres", psqlInfo)
if err != nil{
fmt.Print("连接数据库错误...")
return false, errors.New("连接数据库出错...")
}
sqlStat := "SELECT from userInfo WHERE username = $1"
stmt, err := db.Prepare(sqlStat)
if err != nil {
fmt.Print("准备语句失败...", err.Error())
return false, errors.New("准备语句失败...")
}
rows ,err := stmt.Query(username)
if rows.Next(){
return true, nil
}
return false, nil
}
func checkLogin(username string, password string) (bool, error){
fmt.Print("检查登录信息...")
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable",
host, port, user, sqlpassword, dbname)
db,err := sql.Open("postgres", psqlInfo)
if err != nil{
fmt.Print("连接数据库错误...")
return false, errors.New("连接数据库出错...")
}
sqlStatement := "SELECT * FROM userinfo WHERE username = $1 AND password = $2;"
stmt, err := db.Prepare(sqlStatement)
if err != nil {
print(err.Error())
return false, errors.New("准备数据失败...")
}
rows, err := stmt.Query(username, password)
defer rows.Close()
var user USER
for rows.Next(){
rows.Scan(user.Username, user.Password)
return true, nil
fmt.Print("用户" + username + "登陆成功...")
}
fmt.Print("用户" + username + "登陆失败...")
return false, nil // 没有查询到数据
}
func checkRegister(username string , password string)(bool, error){
fmt.Print("正在检查注册信息...")
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable",
host, port, user, sqlpassword, dbname)
db,err := sql.Open("postgres", psqlInfo)
if err != nil{
fmt.Print("连接数据库错误...")
return false, errors.New("连接数据库出错...")
}
sqlStatement := "INSERT INTO userinfo(username, password) VALUES ($1, $2);"
stmt, err := db.Prepare(sqlStatement)
if err != nil{
fmt.Print("准备语句失败...", err.Error())
return false, errors.New("准备语句失败...")
}
_, err = stmt.Exec(username, password)
defer stmt.Close()
if err != nil{
fmt.Print("插入数据失败...", err.Error())
return false, errors.New("插入数据失败...")
}
fmt.Print("用户" + username + "注册成功...")
return true, nil
}
-
Angular进行http.post
- 引入HttpClient、HttpHeaders
- http.post()
//login.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class LoginService {
constructor(private http: HttpClient) { //以http为名使用HttpClient
console.log("登录服务初始化")
}
login(username, password){
const headers = new HttpHeaders({'Content-Type': 'application/x-www-form-urlencoded'});
let data = { username: username, password: password};
return this.http.post("https://服务器域名:端口/api/login",JSON.stringify(data), {headers: headers});
}
register(username, password) {
const headers = new HttpHeaders({'Content-Type': 'application/x-www-form-urlencoded'});
let data = {username: username,password: password};
return this.http.post("https://服务器域名:端口/api/register", JSON.stringify(data), {headers: headers});
}
}
- 组件里调用登陆服务
//一个帅气的组件.component.ts
...
import { LoginService } from '../../services/login.service';
...
export class NavMenuComponent implements OnInit {
constructor(public loginSvc:LoginService) { } //引入登录服务
/* 登陆控制 */
public input_username:string;
public input_password:string;
login() {
console.log("登录", this.input_username, this.input_password);
this.loginSvc.login(this.input_username, this.input_password).subscribe(
fb=>{ console.log(fb); }, // 反馈信息
err =>{ console.log(err); } // 错误信息
)
}
register(){
this.loginSvc.register(this.input_username, this.input_password).subscribe(
fb=>{ console.log(fb); },
err =>{ console.log(err) }
)
}
}
-
后端运行结果图
//实验前已注册过root passwd
- 注册 root passwd
- 登陆 root wrong
- 登陆 root passwd
- 注册 admin admin
- 登陆 admin admin
-
一些坑的说明
- 不要使用自签发证书
我用了自签发证书后,客户端一定会Certificates Unknown,即使将CA导入到系统证书库也一样,这坑了我最久
Wireshark抓包情况:
1.Client Hello
2.Server Hello
3.Client Alert: Certificates Unknown
解决方案:
1.对于没有“客户端证书认证”的Angular,勿必关闭服务器的“客户端证书认证”(如Golang的TLS_config)
2.请使用信任机构签发的密钥和证书(如Let's encrypt证书)
- 其他的坑
其他一些偏得和本文都没有关系了的坑就不写了