流程前后端分离环境下的api保护.jpg
- login 页面用户携带登陆凭证去请求后台。
- 后台收到登陆凭证表单去做用户名和密码的验证,验证成功后开始往Redis写一个30分钟有效期的Token。
- 后台增加一个中间件,可选择性的给某些API增加安全🔐认证,主要就是验证Toekn有效期,让用户重登。
// route.go
// hostinfo api
hostinfo := r.Group("/hostinfo")
{
// 单节点信息获取
hostinfo.GET("/cpuinfo", src.GetAgentCpuInfo)
hostinfo.GET("/raminfo", src.GetAgentRamInfo)
hostinfo.GET("/diskinfo", src.GetAgentDiskInfo)
// Get All Node Info
hostinfo.GET("/GetAllNodeInfo", middleware.TokenAuthMiddleware(), src.GetAllNodeInfo)
hostinfo.GET("/GetAllOfflineAgentInfo", middleware.TokenAuthMiddleware(), src.GetOfflineAgentInfo)
}
-
Axios请求拦截: 每次请求后端都要从本地localStorage.setItem get 到token值,携带这个token去请求那些有保护的API
-
Axios响应拦截: 检测每次后端返回的状态码,约定好,我这里是403,表示前端携带的登陆Toekn在Redis没查到(过期失效),200则正常显示,可以在Axios响应层增加一个全局响应。
Server login
data := make(map[string]interface{})
code := e.INVALID_PARAMS
data["token"] = token
data["username"] = username
data["LoginTimeStamp"] = time.Now().Unix()
// 专属登陆响应码
code = e.SUCCESS_LOGIN
//生成随机数,作为token存在Redis的value
value := strconv.Itoa(randomdata.Number(10, 200000000))
err := model.InitRedis().Set(ctx, token, value, time.Minute*30).Err()
if err != nil {
log.Error(err)
}
c.JSON(200, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": data,
})
验证
Server chk token Middleware
没错,是gin的middleware
package middleware
import (
"context"
"github.com/gin-gonic/gin"
"github.com/lijinghuatongxue/server-go/model"
"github.com/lijinghuatongxue/server-go/pkg/e"
"net/http"
)
var ctx = context.Background()
type LoginStatusCode struct {
Code string `json:"code"`
}
func TokenAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr := c.Request.Header.Get("token")
if tokenStr == "" {
// 尝试从链接中获取 兼容下部分特殊get请求
//tokenStr = c.Query("token")
tokenStr = c.Request.Header.Get("Authorization")
//logrus.Error(tokenStr)
if tokenStr == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": e.INVALID_PARAMS,
"msg": e.GetMsg(400),
})
c.Abort()
return
}
// key不存在检测
isexist, _ := model.InitRedis().Exists(ctx, tokenStr).Result()
if isexist != 1 {
data := LoginStatusCode{
Code: "4001",
}
c.JSON(403, gin.H{
"code": e.INVALID_PARAMS,
"data": data,
"msg": e.GetMsg(4001),
})
c.Abort()
return
}
}
c.Next()
}
}
UI login
登陆完之后,获取一个只有半小时有效期的token,存储在本地
return this.$http
.post("/api/login",
{
username: this.username,
password: this.password
},
)
.then(result => {
// 将Token、时间戳、用户名字,存储到localStorage,用户名用来右上角显示用户
localStorage.setItem("token", result.data.data.token);
localStorage.setItem("username", result.data.data.username);
// 时间戳这个存储意义不大,前期作为检验用户登陆时长用来强制重登的验证,但是localStorage可编辑,可篡改,生产不建议。
localStorage.setItem("logintimestamp", result.data.data.LoginTimeStamp);
// localStorage.setItem("username", JSON.stringify(result.data.data.username));
// 登陆失败重登
if (result.data.code !== 2001){
this.$message({
message: "🐱喵~ "+result.data.msg,
type: "error"
});
this.$router.push("/login");
}
// return
if (result.data.code === 2001){
// 获取登陆专属响应码,登录成功提醒用户
this.$message({
message: result.data.msg+" 欢迎 "+result.data.data.username+" 🐱喵~" ,
type: "success"
});
// 登录成功跳转到首页
this.$router.push("/navmenu/dashboard");
}
}
UI axios 添加请求拦截器
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// {
// headers: { Authorization: token }
// }
let token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = token
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
UI axios 添加响应拦截器
每次请求后端api,都携带对应的token去请求,这个token只有半小时的生命周期
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
if (error) {
// 请求配置发生的错误
if (!error.response) {
return console.log('Error', error.message);
}
// 获取状态码
const status = error.response.status;
// 错误状态处理
if (status === 403) {
console.log("登陆凭证失效,点击重新登陆 🐱")
// alert("登陆凭证失效,点击重新登陆 🐱")
ElementUI.Message({
message: '登陆凭证失效,重新登陆 🐱',
duration:'2000',
type: 'warning'
});
router.push('/login')
} else if (status === 400) {
router.push('/login')
} else if (status >= 404 && status < 422) {
router.push('/404')
}
}
return Promise.reject(error);
});
Conclusion / 结论
1. 🌲 前端浏览器任何可编辑的地方,不要存储明文登陆认证信息。
2. 🔒 前端浏览器要是存的话,最好采用前后端共同加密的api认证过期机制。
3. 💻 单独采用后端返回Token是否过期的话,比较死板,因为从登陆事件到Token过期重新登陆事件中,比如我们设置了30分钟过期Token,那便每次登陆,半小时都要重新登陆,再迭代一次,可以增加用户在操作页面功能,一定频率的去后台刷新Token 有效期,保证在登陆之后,不再以30分钟一次的频率,暴力跳转页面。