基于JWT验证原理解决分布式系统session一致性问题

session一致性问题

在集群或者分布式系统中,用户登录后的,由于服务端是集群环境或者分布式环境,如何保证用户每次与服务器交互都是使用原来的session或者实现单点登录,这里就涉及session一一致性的问题。

解决session一致性问题可以从两种思路实现,一种是session服务端存储,一种是session客户端存储。

session服务端存储

服务端存储session,需要保证用户请求过来,能够使用到同一个session进行交互,有以下的实现方式

  1. session sticky,这种方式通过对同一个用户请求进行定向,用户每次请求,都访问同一台服务器,这样就能完成每次访问都是同一个服务器。
  2. session复制,这种方式就是在服务器集群中对session进行复制存储,每个服务端都存储同一份session,这样也能保证用户每次访问都能使用同一份session。
  3. session统一存储,使用第三方中间件存储session,每次用户访问时,去中间件进行session查找对应的session,保证每次请求使用同一个session。

session客户端存储

session客户端存储的话,实现思路是服务端不保存任何session信息,session信息附加在客户端存储,每次请求把session信息传递到服务端,服务端需要做的就是验证客户端的session是否是正确有效的。jwt的实现思路就是其中的一种实现方式,也是这篇文章介绍的点。

jwt简介

jwt中的概念

首先,根据jwt官网中的信息,先了解一下jwt中的一些常见概念。

token:返回给客户端用来登录验证的凭证,token有效表示用户已经登录,token中的信息有效可信。token的生成公式如下所示。token=base64(header).base64(payload).sinatrue

header:组成token中的一部分,主要用于定义签名sinatrue的生成算法,例如HS256,以及token类型JWT。例如下面

{
   "alg": "HS256",
   "typ": "JWT"
 }

payload:同样也是生成token的一部分,payload中的信息,有部分key是jwt中已经声明使用含义的,例如exp表示token有效期等等。iss (issuer), exp (expiration time), sub (subject), aud (audience)。除了这些已经声明使用含义的信息,还可以附加我们自定义的key的信息到payload中,作为token的一部分,验证token后,我们加入大token中的信息可以使用,效果类似于存储在session中,例如把登录用户名放入token,验证token后,可以根据token中的用户名信息判断当前登录的用户。例如下面

{
   "sub": "1234567890",
   "name": "John Doe",
   "admin": true,
   "exp": 1544872606128
   }

sinatrue:token的签名,也是token的组成部分,用来验证token中的header和payload信息是否被修I改过并且是否已经过期等,如果token没被修改并且没过期,表示token可用,用户当前依然属于登录状态。key是服务端定义的一个密钥用来保证在header和payload都被知晓的情况下,key作为混淆值,保证token不被伪造。

根据header定义的签名短发,签名格式如下:

 sinatrue=SignatureAlgorithm.HS256( base64(header) + "." + base64(payload) , base64(key))

token验证

验证token的过程其实跟重新生成一个token的过程是一样的

根据传递过来的header、payload以及服务端存储的密钥,重新生成一个token对比,如果token相同,那么标志token有效,没被修改。同时如果token没被修改,还可以进一步同bease64解密payload,然后根据payload中的exp有效期信息,判断token是否已经过期。

jwt基本工作流程

下面看一张jwt的基本工作流程图:

在这里插入图片描述

根据上图以及了解的基本概念,应该就能了解token的生成、验证和使用原理。验证完token之后,我们就可以使用token中的payload中的信息。

jwt实践

下面看一下jwt的api是如何使用的。

引入pom依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

新建了以下一个jwt生成token和验证token的工具类。

public class JwtTokenUtils {

    /**
     * token默认过期时间
     */
    public static final Integer  DEFAULT_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;

    public static final String DEFAULT_TOKEN_KEY = "123456";

    public static void main(String[] args) {
        //获取secretkey
        System.out.println(generatorSecretkey("123456"));
		//用户名加入到token中,并生成一个新的token
        String tolen = generateTokenBy("1admin");
        System.out.println(tolen);
		//验证token并且获取token中的payload信息(json结构)
        System.out.println(pharseToken(tolen));
    }
    private static String generatorSecretkey(String key){
        return BaseEncoding.base64().encode(key.getBytes());
    }

    //seperator--------------------------------------------------------------------------

    /**
     * key-val格式获取payload中信息
     * @param token
     * @param secretkey
     * @return Claims 一个继承map的结构对象
     */
    public static Claims pharseToken(String token, String secretkey){
        Jws<Claims> claimsJwt=Jwts.parser()
                                .setSigningKey(secretkey)
                                .parseClaimsJws(token);
        return claimsJwt.getBody();
    }

    public static Claims pharseToken(String token){
        Jws<Claims> claimsJwt=Jwts.parser()
                .setSigningKey(generatorSecretkey(DEFAULT_TOKEN_KEY))
                .parseClaimsJws(token);
        return claimsJwt.getBody();
    }

    /**
     * 生成token信息
     * @param header like
     *               {
     *                  "alg": "HS256",
     *                  "typ": "JWT"
     *                }
     * @param payload like
     *                {
     *                   "sub": "1234567890",
     *                   "name": "John Doe",
     *                   "admin": true,
     *                   "exp": ${timestamp}
     *                 }
     *
     * @param secretkey 加盐
     * @return token=base64(header).base64(payload).sinatrue ,
     *         sinatrue=SignatureAlgorithm.HS256( base64(header) + "." + base64(payload) , base64(key))
     *         secretKey=base64(key)
     *         JWT规范是以上实现
     */
    public static String generateTokenBy(Map<String, Object> header, Map<String, Object> payload, String secretkey){
         return Jwts.builder()
                .setHeader(header)
                .setPayload(JsonUtils.beanToJson(payload))
                .signWith(SignatureAlgorithm.HS256, secretkey).compact();
    }

    public static String generateTokenBy(String userName, long exp, String secretkey){
        Map<String, Object> defaultHeader = Maps.newHashMap();
        defaultHeader.put("alg", "HS256");
        defaultHeader.put("typ", "JWT");

        Map<String, Object> payload = Maps.newHashMap();
        payload.put("userName", userName);
        payload.put("exp", exp);
        return generateTokenBy(defaultHeader, payload, secretkey);
    }

    public static String generateTokenBy(String userName, String secretkey){
        return generateTokenBy(userName, getDefaultTokenExpireTime(), secretkey);
    }

    public static String generateTokenBy(String userName){
        return generateTokenBy(userName, generatorSecretkey(DEFAULT_TOKEN_KEY));
    }

    public static Long getDefaultTokenExpireTime(){
        Date curDate = new Date();
        return curDate.getTime() + DEFAULT_TOKEN_EXPIRE_TIME;
    }

}

有了以上的工具之后,我们就可以实现登录时生成token,请求时验证token来保证session一致性。

在登录方法做这样的处理。这样token就会存储在客户端cookie中,方便下次请求时验证。

@PostMapping("/login")
@ResponseBody
public ResponseData doLogin(String username, String password,
                            HttpServletResponse response) {
    ResponseData data = new ResponseData();
    if (hasUser(username, password)) {
        //如果验证成功,生成token
        String token = JwtTokenUtils.generateTokenBy(username);
        //response设置cookie
        response.addHeader("Set-Cookie", "access_token=" + token + ";Path=/;HttpOnly");
        data.setCode(SUCCESS);
        return data;
    }
    data.setCode(FAIL);
    return data;
}

验证token。验证时也好办,只需要在http请求时,拦截请求,获取request中的cookie信息,并且获取token,然后调用验证方法。可以通过继承HandlerInterceptorAdapter,拦截器方式实现验证逻辑。下面是一个简单实现

public class TokenHandlerInterceptorAdapter extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token= null;
        Cookie[] cookies = request.getCookies();
        if (ArrayUtils.isNotEmpty(cookies)) {
            for (Cookie cookie : cookies) {
                if ("access_token".equals(cookie.getName())) {
                    token= cookie.getValue();
                }
            }
        }
        if(StringUtils.isEmpty(token)){
            response.sendRedirect(LOGIN_PAGE);
            return false;
        }
        try{
            Claims claims=JwtTokenUtils.pharseToken(token);
            String userName= (String) claims.get("userName");
            request.setAttribute("userName", userName);
            return super.preHandle(request, response, handler);
        }catch (ExpiredJwtException e){
            //token'过期了
            response.sendRedirect(LOGIN_PAGE);
        }catch (SignatureException e1){
            //token验签不通过
            response.sendRedirect(LOGIN_PAGE);
        }catch (Exception e){

        }finally {

        }
        return false;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值