无论是C端还是B端系统, 用户登陆都是基础必要的功能,我们需要记住其登陆状态,这样在登陆之后再次操作其他功能时就能直接操作。否则在未登陆或者长期未操作后访问其他页面或者功能,应该跳转到登陆页面要求用户登陆
那么我们就需要记住用户的登陆状态,并且在用户每次访问功能时校验其登陆状态是否过期。
在单机架构下,我们可以使用session来实现存储用户登陆信息,而在分布式系统中时,session就不能再满足我们的需求了。
有可能我们第一次登陆时session存储在server1上,用户再发起第二个请求时,打到server2上,结果发现没有session,就会又跳转到登陆页面,用户这时一定会发出大大的问号:怎么又要登陆? 所以我们引入了token来解决这个问题
token是登陆时服务端结合密钥生成一串加密字符串,会返回给客户端
客户端将其存储在cookie中,下一次访问时会将其添加到请求的header中。
服务端会通过密钥+算法验证token是否合法,如果合法就通过校验,如果不合法就要求重新登陆。
密钥是写在服务端的一串字符串,拿不到这个密钥,是无法解密出token的,因此保证了token的安全性。
可以看到token是不会保存到服务端的,服务端只需要通过算法解密后校验即可。所以保证了即使在分布式系统中依然可用。
JWT(Json Web Token)是用于生成token的一种组件或者说是token中的一种。JWT是以JSON加密形式保存在客户端的,理论上所有的web端都能支持。
JWT分成三个部分,每个部分之间用点隔开。如下形式
xxx.yyy.zzz
三个部分为:
- 标头(header): 是一个json对象,包含alg和tyg两个变量,其中alg是签名使用的算法,默认为HMAC SHA256,typ表示令牌的类型,JWT生成的token统一都为JWT。最后会用Base64 URL算法将该json对象转换为字符串,也就是上述的
xxx
部分
{
"alg": "HS256",
"tyg": "JWT"
}
- 有效载荷(payload):是JWT的核心部分,也是一个JSON对象,包含需要传递的信息,JWT提供了七个变量供选择填写。
{
"iss":发行人
"exp":到期时间
"sub":主题
"aud":用户
"nbf":在此之前不可用
"iat":发布时间
"jti":JWT ID用于标识该JWT
}
除了这几个变量之外,也可以自定义变量,一般会将用户信息存储进去。比如username。需要注意的是,因为payload加密只是base64算法,所以是可以被逆向解密的,因此不要把用户密码放到JWT中
- 签名(signature):用于向上述两个部分的数据进行数据签名,对base64编码后的header和payload,结合密钥,用指定算法生成哈希,其中密钥存储在服务端,不允许公开,确保不会被解密。其生成公式如下:
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
综上所述,JWT的生成公式如下:
Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
4. 使用JWT
引入jar包
io.jsonwebtoken
jjwt
0.9.1
针对JWT的使用,主要有几个方法我们需要关注:
-
生成token token的生成方法应该是在用户登陆之后调用的,即用户输入账号密码,查询数据库正确后,根据用户ID或者其他用户属性调用token生成方法来生成token,并且将这个token返回给前端。
-
获取token中payload中的变量
-
判断token是否获取
-
判断token的合法性 针对于token合法性的判断,是需要对每个接口都进行的判断,因此我们需要在请求真正传达到服务前进行拦截并且进行校验,那么我们应该把这个方法放到网关中进行实现
详细的代码如下所示
完整的JWT工具类import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author whx
* @date 2022/4/10
*/
public class JWTUtil {
private final static Logger log = LoggerFactory.getLogger(JWTUtil.class);
// jwt 加解密类型
private static final SignatureAlgorithm ALGORITHM = SignatureAlgorithm.HS256;
// jwt 生成密钥使用的密码 私钥
private static final String SECRET = "asdkasdjad";
// jwt 添加至http head中的前缀
private static final String SEPARATOR = "Bearer ";
// 添加到PAYLOAD的签发者
private static final String ISSUE = "xxx";
// 添加至PAYLOAD的有效期(秒)
private static final int TIMEOUT = 60*60*24;
/**
* 生成token
* @param userId
* @return
*/
public static String generateToken(String userId){
// 创建PAYLOAD的私有声明(要放入token中的信息)
Map claims = new HashMap();
claims.put("userId",userId);
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString()) // 唯一id
.setIssuedAt(new Date(currentTime)) // 签发时间
.setIssuer(ISSUE) // 签发人
.signWith(ALGORITHM,SECRET) // 加密算法,私钥
.setExpiration(new Date(currentTime + TIMEOUT * 1000)) // 过期时间(毫秒)
.addClaims(claims)
.compact();
}
/**
* 解析token 获取key
* @param token
* @param key
* @return
*/
public static Object getVal(String token,String key){
return getClaimsBody(token).get(key);
}
/**
* token是否过期
* @param token
* @return
*/
public static boolean isExpiration(String token){
try{
return getClaimsBody(token).getExpiration().before(new Date());
}catch (ExpiredJwtException e){
return true;
}
}
public static Claims getClaimsBody(String token){
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
}
校验token的代码
token的校验我们需要放入到过滤器中,这样可以在请求打入之前自动进行校验。这里的校验我们需要放到作为统一入口的网关中
其判断流程应该是: (1)判断token是否为空 (2)判断token中的自定义变量是否为空,这里引入自定义变量的目的是为了自定义token的结构格式,以此校验token合规性,防止伪造 (3)判断token是否过期
import com.example.gateway2.util.JWTUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @author whx
* @date 2022/4/10
*/
@Component
public class TokenFilter implements GlobalFilter, Ordered {
private final String[] skipAuthUrls = new String[]{"/login/check","/user/register"};
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
// 跳过不需要验证的路径
if (isSkipUrl(url)) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
// 从请求头中取得token
String token = exchange.getRequest().getHeaders().getFirst("Authentication-Token");
// token是否为空
if (StringUtils.isEmpty(token)) {
return fail(response,"token为空,鉴权失败");
}
// 请求中的token是否有效
String userId = JWTUtil.getVal(token,"userId").toString();
if(StringUtils.isEmpty(userId)){
return fail(response,"token不合法");
}
// 校验token是否过期
if(JWTUtil.isExpiration(token)){
return fail(response,"token已过期");
}
//如果各种判断都通过,执行chain上的其他业务逻辑
return chain.filter(exchange);
}
private Mono fail(ServerHttpResponse response,String message){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Flux.just(buffer));
}
/**
* 判断当前访问的url是否开头URI是在配置的忽略url列表中
*
* @param url
* @return
*/
public boolean isSkipUrl(String url) {
for (String skipAuthUrl : skipAuthUrls) {
if (url.startsWith(skipAuthUrl)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
项目git地址
源码git地址
关注公众号,了解更多新鲜内容https://blog.csdn.net/weixin_45070175/article/details/118559272