JWT & token
1. 什么是token认证?
基于token的用户认证是一种服务端无状态 的认证方式,所谓服务端无状态指的token本身包含登录用户所有的相关数据,而客户端在认证后的每次请求都会携带token,因此服务器端无需存放token数据。
当用户认证后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage 等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
2. 什么是JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。
3. JWT令牌结构
JWT令牌由Header、Payload、Signature三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC、SHA256或RSA)。
// header 内容
{
"alg": "HS256",
"typ": "JWT"
}
// 将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
base64UrlEncode(header)
Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
// payload 内容
{
"sub": "1234567890",
"name": "456",
"admin": true
}
// 最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
base64UrlEncode(payload)
Signature
第三部分是签名,此部分用于防止jwt内容被篡改。 这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。
// signature 内容
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
// 签名所使用的密钥
secret
token实现单点登录
JWT 生成及校验 Token
jwt 工具类
java
public class AppJwtUtil {
// 设置token过期时间30分钟
private static final long EXPIRE_TIME = 30 * 60 * 1000; // 30分钟
// 加密KEY
private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
// 生产ID
public static String getToken(Long id) {
Map<String, Object> claimMaps = new HashMap<>();
claimMaps.put("id", id);
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(currentTime)) // 签发时间
.setSubject("system") // 说明
.setIssuer("heima") // 签发者信息
.setAudience("app") // 接收用户
.compressWith(CompressionCodecs.GZIP) // 数据压缩方式
.signWith(SignatureAlgorithm.HS512, TOKEN_ENCRY_KEY) // 加密方式
.setExpiration(new Date(currentTime + EXPIRE_TIME)) // 过期时间戳
.addClaims(claimMaps) // cla信息
.compact();
}
/**
* 获取payload body信息
*
* @param token
* @return
*/
public static Claims getClaimsBody(String token) {
try {
Jws<Claims> jwt = Jwts.parser().setSigningKey(TOKEN_ENCRY_KEY).parseClaimsJws(token);
return jwt.getBody();
} catch (Exception e) {
return null;
}
}
/**
* 获取hearder body信息
*
* @param token
* @return
*/
public static JwsHeader getHeaderBody(String token) {
Jws<Claims> jwt = Jwts.parser().setSigningKey(TOKEN_ENCRY_KEY).parseClaimsJws(token);
return jwt.getHeader();
}
/**
* 是否过期
*
* @param claims
* @return 1:有效,0:无效
*/
public static int verifyToken(Claims claims) {
if (claims == null) {
return 0;
}
// 当前时间在有效期范围内
if(new Date().before(claims.getExpiration())){
return 1;
}
return 0;
}
}
后端使用token校验登录
service层生成token代码
java
@Service
public class WmUserServiceImpl extends ServiceImpl<WmUserMapper, WmUser> implements WmUserService {
@Override
public ResponseResult login(WmLoginDto dto) {
//1.检查参数
if(StringUtils.isBlank(dto.getName()) || StringUtils.isBlank(dto.getPassword())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID,"用户名或密码为空");
}
//2.查询用户
WmUser wmUser = getOne(Wrappers.<WmUser>lambdaQuery().eq(WmUser::getName, dto.getName()));
if(wmUser == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
}
//3.比对密码
String loginPwd = dto.getPassword(); //明文密码
String dbPwd = wmUser.getPassword(); //密文密码
boolean result = BCrypt.checkpw(loginPwd, dbPwd);
if(result){
//4.返回数据 jwt
Map<String,Object> map = new HashMap<>();
map.put("token", AppJwtUtil.getToken(wmUser.getId().longValue()));
wmUser.setSalt("");
wmUser.setPassword("");
map.put("user",wmUser);
return ResponseResult.okResult(map);
}else {
return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
}
}
拦截器校验token代码
java
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//2.判断是否是登录接口,是直接放行
String path = request.getURI().getPath();
if(path.equals("/user/api/v1/login/login_auth/")){
return chain.filter(exchange);
}
//3.非登录接口,获取token
String token = request.getHeaders().getFirst("token");
//4.判断token是否存在,不存在返回401(无权访问)
if(StringUtils.isBlank(token)){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//5.判断token是否有效,无效返回401(无权访问)
Claims claimsBody = AppJwtUtil.getClaimsBody(token);
try {
int i = AppJwtUtil.verifyToken(claimsBody);
if(i==0){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
} catch (Exception e) {
e.printStackTrace();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//从token载荷里获取用户ID
String userId = String.valueOf(claimsBody.get("id"));
//将用户ID设置到请求头
request.mutate().header("userId",userId);
//6.放行
return chain.filter(exchange);
}
/**
* 优先级设置 值越小 优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
防止token伪造
- 如上代码所示,生成 token 时保存userId,再次请求时比对请求携带的 uerId 和 token 体中解析出来的 userId 是否匹配
- 生成 token 时保存请求ip地址,再次请求时比对当前请求ip地址和 token 中解析出来的ip地址是否匹配