登录验证方案
session认证
众所周知,http 协议本身是无状态的协议,那就意味着当有用户向系统使用账户名称和密码进行用户认证之后,下一次请求还要再一次用户认证才行。因为我们不能通过 http 协议知道是哪个用户发出的请求,所以如果要知道是哪个用户发出的请求,那就需要在服务器保存一份用户信息(保存至 session ),然后在认证成功后返回 cookie 值传递给浏览器,那么用户在下一次请求时就可以带上 cookie 值,服务器就可以识别是哪个用户发送的请求,是否已认证,是否登录过期等等。这就是传统的 session 认证方式。
session 认证的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。于是乎为了解决 session 共享的问题,又引入了 redis,接着往下看。
token认证
这种方式跟 session 的方式流程差不多,不同的地方在于保存的是一个 token 值到 redis,token 一般是一串随机的字符(比如UUID),value 一般是用户ID,并且设置一个过期时间。每次请求服务的时候带上 token 在请求头,后端接收到token 则根据 token 查一下 redis 是否存在,如果存在则表示用户已认证,如果 token 不存在则跳到登录界面让用户重新登录,登录成功后返回一个 token 值给客户端。
优点是多台服务器都是使用 redis 来存取 token,不存在不共享的问题,所以容易扩展。缺点是每次请求都需要查一下redis,会造成 redis 的压力,还有增加了请求的耗时,每个已登录的用户都要保存一个 token 在 redis,也会消耗 redis 的存储空间。
那能不能不存储token了?其实我们只是验证token的合法性,只要能验证这个token是我们签发的,就可以不存储。看看下面jwt的方案。
1、jwt是什么
jwt是json web token的缩写,常用于登录身份校验,它将用户信息加密保存到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。
2、jwt验证过程
- 用户首次登录,发送用户名和密码请求服务端
- 服务端接收到用户名和密码,去数据库校验用户合法性,合法就会生成一个token字符串给前端
- 前端收到token后,把token保存到自己本地(可以是cookie或者localstorage)
- 前端每次发送请求的时候,都带上token
- 后端拦截器,每次都先校验下token的合法性,因为token是后端加密签发的,中间变化了,校验不通过
- 返回给前端结果,如果通过就返回接口数据,不通过跳转到登录页登录。
3、jwt组成
一个jwt生成后的token是下面这样的,分别有三部分组成,由"."分隔连接而成。
- Header
- Payload
- Signature
每个组成部分的数据。
Header
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等),然后对这个部分进行base64生成第一部分字符串。
{
'alg': "HS256",
'typ': "JWT"
}
PayLoad
是载荷,承载用户基本信息,可以自定义数据,正常是保存用户的用户id,用户名等信息
例如:
{
"userId":"121",
"userName":"fwf"
}
然后对这部分base64,生成第二个字符串。
Signature
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
下面这张图就可以看出header和payload,所以载荷里最好不要存敏感信息,很容易解析出原数据
4、jwt的实现
引入jar包,不需要重复造轮子了
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
jwt工具类
package com.als.api.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* @author xiangwei.li
* @version 1.0.0
* @date 2023/4/4
*/
public class JwtUtil {
private static String header = "Authorization";
private static String secret = "bda8bb94fb3746a0bd5083bb0b0e616d";
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 创建令牌
*/
public static String createToken(String userId, String userName) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("userName", userName);
claims.put("expireTime", System.currentTimeMillis());
return createJwt(claims);
}
/**
* 生成令牌
*/
public static String createJwt(Map<String, Object> claims) {
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public static Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
}
return null;
}
/**
* 获取请求token
*
* @param request
* @return token
*/
public static String getToken(HttpServletRequest request) {
String token = request.getHeader(header);
if (!StringUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) {
token = token.replace(TOKEN_PREFIX, "");
}
return token;
}
//刷新token把CLAIM_KEY_CREATED刷新了
public static String refreshToken(String token){
String refreshedToken;
try {
final Claims claims = parseToken(token);
claims.put("expireTime", System.currentTimeMillis());
refreshedToken = createJwt(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
}
配置拦截器
package com.als.api.filter;
import com.als.api.exception.ApiException;
import com.als.api.util.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author xiangwei.li
* @version 1.0.0
* @date 2023/4/4
*/
public class JwtInterceptor implements HandlerInterceptor {
@Value("${crabc.token.expireTime:36000}")
private long expireTime;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getMethod().toUpperCase().equals("OPTIONS")){
return true; // 通过OPTION请求
}
String token = JwtUtil.getToken(request);
if (token == null) {
throw new ApiException(401, "用户未登录");
}
Claims claims = JwtUtil.parseToken(token);
if (claims == null) {
throw new ApiException(401, "用户未登录");
}
long nowTime = System.currentTimeMillis();
Long expire = Long.parseLong(claims.get("expireTime").toString());
long time = nowTime - expire;
if (time/1000 > expireTime) {
throw new ApiException(401, "登录失效,请重新登录");
}
return true;
}
}
用户登录
package com.als.api.controller;
import com.als.api.dto.ApiUserDTO;
import com.als.api.response.Result;
import com.als.api.util.JwtUtil;
import org.springframework.util.Base64Utils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author xiangwei.li
* @version 1.0.0
* @date 2023/4/4
*/
@RestController
@RequestMapping("/api/user")
public class ApiUserController {
/**
* 登录
*
* @param userId
* @param pwd
* @return
*/
@PostMapping("/login")
public Result login(String userId, String pwd) throws Exception {
//模拟用户登录,这里访问数据库
ApiUserDTO userInfo = new ApiUserDTO();
userInfo.setUserId(userId);
userInfo.setPwd(pwd);
userInfo.setUserName("jack");
if (userInfo == null) {
return Result.error("账号或密码错误!");
}
Map<String, Object> user = new HashMap<>();
//签发token
String token = JwtUtil.createToken(userInfo.getUserId(), userInfo.getUserName());
user.put("expires", 3600);
user.put("access_token", token);
user.put("refresh_token", UUID.randomUUID().toString().replace("-", ""));
return Result.success(user);
}
}
最后讲讲 JWT 的缺点,因为任何技术都不是完美的,所以我们得用辩证思维去看待任何一项技术。
- 安全性没法保证,所以 jwt 里不能存储敏感数据。因为 jwt 的 payload 并没有加密,只是用 Base64 编码而已。
- 无法中途废弃。因为一旦签发了一个 jwt,在到期之前始终都是有效的,如果用户信息发生更新了,只能等旧的 jwt 过期后重新签发新的 jwt。
- 续签问题。当签发的 jwt 保存在客户端,客户端一直在操作页面,按道理应该一直为客户端续长有效时间,否则当 jwt有效期到了就会导致用户需要重新登录。那么怎么为 jwt 续签呢?最简单粗暴就是每次签发新的 jwt,但是由于过于暴力,会影响性能。如果要优雅一点,又要引入 Redis 解决,但是这又把无状态的 jw t硬生生变成了有状态的,违背了初衷