您当前的位置: 首页 >  spring

wu@55555

暂无认证

  • 1浏览

    0关注

    201博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

springcloud:token生成组件JWT详解(七)

wu@55555 发布时间:2022-04-10 01:14:50 ,浏览量:1

0. 引言

无论是C端还是B端系统, 用户登陆都是基础必要的功能,我们需要记住其登陆状态,这样在登陆之后再次操作其他功能时就能直接操作。否则在未登陆或者长期未操作后访问其他页面或者功能,应该跳转到登陆页面要求用户登陆

那么我们就需要记住用户的登陆状态,并且在用户每次访问功能时校验其登陆状态是否过期。

在单机架构下,我们可以使用session来实现存储用户登陆信息,而在分布式系统中时,session就不能再满足我们的需求了。

有可能我们第一次登陆时session存储在server1上,用户再发起第二个请求时,打到server2上,结果发现没有session,就会又跳转到登陆页面,用户这时一定会发出大大的问号:怎么又要登陆? 在这里插入图片描述 所以我们引入了token来解决这个问题

1. 什么是token?

token是登陆时服务端结合密钥生成一串加密字符串,会返回给客户端

客户端将其存储在cookie中,下一次访问时会将其添加到请求的header中。

服务端会通过密钥+算法验证token是否合法,如果合法就通过校验,如果不合法就要求重新登陆。

密钥是写在服务端的一串字符串,拿不到这个密钥,是无法解密出token的,因此保证了token的安全性。

可以看到token是不会保存到服务端的,服务端只需要通过算法解密后校验即可。所以保证了即使在分布式系统中依然可用。 在这里插入图片描述

2. 什么是JWT?

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

关注
打赏
1664985904
查看更多评论
立即登录/注册

微信扫码登录

0.0676s