1 Jwt 和 Session 登录方案介绍
JSON Web Token(缩写 JWT)是目前流行的跨域认证解决方案。
原理是生存的凭证包含标题 header,有效负载 payload 和签名组成。用户信息payload中,后端接收时只验证凭证是否有效,有效就使用凭证中的用户信息。
签名是通过标题 header,有效负载 payload 和密钥(后端保存,不可泄露)生成。
JWT 介绍:https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
JWT 实现:https://learnku.com/go/t/52399
登录替代方案(session)
常规传统登录方式使用 session,登录成功后端存储 sessionId 和对应的登录用户信息,返回 sessionId 给前端。前端将 sessionId 存在 cookie 中。前端发送请求时,附带传上 cookie 中的 sesssionId,后端接受 sessionId 时,查询 sessionId 是否存在对应用户信息,存在即说明登录成功。
缺点:
- 没有分布式架构,无法支持横向扩展,后端服务是有状态的。
- Session依赖Cookie,如果客户端禁用了Cookie,那么Session无法正常工作。
- Session依赖Cookie,Cookie无法防止CSRF(Cross Site Request Forgery):跨站请求伪造。
JWT 好处
- 后端服务是无状态服务,支持横向扩展。
- 基于 json,可以在令牌中自定义丰富内容,不依赖认证服务就可以完成授权。
Jwt 缺点
- token 过长
- token 一旦发出,无法销毁
2 Jwt 实现方案
JWT 登录方案包含登录,鉴权,续期三个逻辑,包含子需求有
后端:
- 登录生成 token
- 每次请求,验证 token 是否合法
- token 续期
前端:
- 登录密码 sha 256 加密
- 获取 token 并存储
- 每次请求,携带 token
- 每次请求,发现 token 不合法错误直接跳转登录
- 每次请求,查看 token 是否需要自动续期
3 如何实现(后端 goframe,前端 acro design in vue)
登录实现
// 登录
func (C CUser) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) {
role, tokenString, err := logic.User.Login(ctx, req.Username, req.Password)
if err != nil {
return
}
res = &v1.LoginRes{
Token: tokenString,
Role: role,
}
return
}
// 登录
func (s *lUser) Login(ctx context.Context, username string, password string) (role string, token string, err error) {
// 查询用户信息
var user *entity.User
err = dao.User.Ctx(ctx).Where(do.User{
Passport: username,
Password: password,
}).Scan(&user)
if err != nil {
return
}
if user == nil {
err = errors.New("账户或密码错误")
return
}
// 生成 jwt token
token, err = MyJwt.GenerateToken(ctx, username)
if err != nil {
return
}
return
}
// GenerateToken 生成 jwt 格式 token
func (lJwt) GenerateToken(ctx context.Context, username string) (token string, err error) {
tokenHeader := jwt.NewWithClaims(jwt.SigningMethodHS256, &MyClaims{
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(ExpiresTime).Unix(),
},
})
token, err = tokenHeader.SignedString(JwtSecret)
return
}
鉴权实现
// 检测登录 token 是否合法
func Auth(r *ghttp.Request) {
valid := logic.User.IsSignedIn(r.GetCtx(), r)
if !valid {
r.SetError(gerror.NewCode(gcode.New(50008, "请重新登录", "非法令牌或还未登录")))
return
}
r.Middleware.Next()
}
// 查看是否登录
func (s *lUser) IsSignedIn(ctx context.Context, r *ghttp.Request) bool {
token, exist := s.getToken(r)
if !exist {
return false
}
valid := MyJwt.Valid(r.Context(), token)
return valid
}
// Valid 验证 token 是否合法
func (lJwt) Valid(ctx context.Context, token string) (valid bool) {
var claims *MyClaims = &MyClaims{}
tkn, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
return JwtSecret, nil
})
if err != nil {
valid = false
} else {
valid = tkn.Valid
}
return
}
后台签证续期逻辑
func (C CUser) Refresh(ctx context.Context, req *v1.RefreshReq) (res *v1.RefreshRes, err error) {
newToken, err := logic.MyJwt.GenerateToken(ctx, logic.Ctx.Get(ctx).Username)
res = &v1.RefreshRes{
Token: newToken,
}
return
}
// GenerateToken 生成 jwt 格式 token
func (lJwt) GenerateToken(ctx context.Context, username string) (token string, err error) {
tokenHeader := jwt.NewWithClaims(jwt.SigningMethodHS256, &MyClaims{
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(ExpiresTime).Unix(),
},
})
token, err = tokenHeader.SignedString(JwtSecret)
return
}
前端拦截器实现头添加 token 和续期操作
axios.interceptors.request.use(
(config: AxiosRequestConfig) => {
// let each request carry token
// this example using the JWT token
// Authorization is a custom headers key
// please modify it according to the actual situation
const token = getToken();
// const tokenData = jwt.decode(token);
// decodedHeader.exp
if (token) {
const decodedHeader = jwtDecode(token);
const now = Date.parse(new Date().toString()) / 1000;
// token 续期,如果 token 有效期小于60s,发起续期请求
if (
config.url !== '/api/user/refresh' &&
decodedHeader.exp > now &&
decodedHeader.exp - 60 < now
) {
refreshToken()
.then((res) => {
setToken(res.data.token);
})
.finally();
}
// header 头携带 token
if (!config.headers) {
config.headers = {};
}
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
// do something
return Promise.reject(error);
}
);
前端实现对返回未鉴权的错误,跳转登录页面
axios.interceptors.response.use(
(response: AxiosResponse<HttpResponse>) => {
const res = response.data;
// if the custom code is not 20000, it is judged as an error.
// if (res.code !== 20000) {
if (res.code !== 0 && res.code !== 20000) {
Message.error({
content: res.message || 'Error',
duration: 5 * 1000,
});
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (
[50008, 50012, 50014].includes(res.code) &&
response.config.url !== '/api/user/info'
) {
Modal.error({
title: 'Confirm logout',
content: '您已登出,您可以取消以留在此页面,或重新登录',
okText: '重新登陆',
async onOk() {
const userStore = useUserStore();
await userStore.logout();
window.location.reload();
},
});
}
return Promise.reject(new Error(res.message || 'Error'));
}
return res;
},
(error) => {
Message.error({
content: error.msg || 'Request Error',
duration: 5 * 1000,
});
return Promise.reject(error);
}
);
jwt token 解析页面:https://jwt.io/