基于JWT的单点登录

使用cookie和session做单点登录的弊端

  1. 为了记录用户的信息方便用户下一次请求的鉴别,服务端会存一份用户的数据到session中,随着用户数量的增加服务端的开销会增大。

  1. 不同服务难以共享session中的数据,就算使用springsession的分布式session,也只能放大domin和子域名共享session而不能使不同域名的服务共享session。

  1. 因为是基于cookie进行用户识别的,cookie如果被截获,坏人就能拿着这个cookie对用户进行攻击

  1. 只有浏览器有cookie

使用jwt(Json Web Token)

jwt是啥

JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token。

结构:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。Header.Payload.Signature三段。

Header:指定签名使用的算法,令牌的类型(jwt)。

payload:中可以存储json信息,建议不要存敏感信息

Signature:使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret),该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)生成签名,在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.分隔,就构成整个JWT对象

jwt三部分的具体含义

jwt的加密

为了完成签名,除了用到header信息和payload信息外,还需要算法的密钥,也就是secretKey。加密的算法一般有2类:

  • 对称加密(HMAC【哈希消息验证码】):secretKey指加密密钥,可以生成签名与验签,签名和验证使用相同的密钥。性能高,秘钥泄露有安全问题。
  • 非对称加密(RSASSA【RSA签名算法(非对称)】):签名使用私钥,验证使用公钥,如果不是经常修改可将公钥保存到前端。安全性高,私钥只保存在服务端,token传输只需要传公钥,公钥只用于验签判断是否被篡改,并不能解密。

优势

  1. 自包含,能将用户的信息存到jwt当中,如可以存入用户的id然后根据id再到redis当中查询到用户的具体信息,可以避免大量用户将数据存到session给服务端带来的压力。

  1. 不需要在session中保存信息所以适合分布式的系统

  1. 短小精悍传输速度快

  1. jwt是以json的形式存在客户端的,所以是跨语言的

最佳实现

  1. 在登录服务登录之后给用户生成一个随机的token作为用户在reids的id(uuid),将这个随机的id作为键用户信息作为值存入redis并设置过期时间。

  1. 将生成的token作为jwt的payload生成jwt然后返回给前端,前端将这个jwt保存在客户端的localstorage。

  1. 之后每次前端发的请求都带上这个jwt放到请求头中给后端,后端定义一个拦截器,每次前端的请求经过拦截器都会解析出jwt中的token再根据token到redis中查得用户登录信息放入请求域中。

分布式商城中的应用

商城前台服务前后端不分离不使用localstorage

使用jwt+cookie,登录后在认证服务域名下存cookie,其他服务到认证服务获取cookie。

  1. 导入jwt依赖

<dependency>
     <groupId>com.auth0</groupId>
     <artifactId>java-jwt</artifactId>
     <version>3.10.3</version>
</dependency>
  1. 编写jwt工具类用户jwt的生成和解析

public class JwtUtil {
    //相当于盐
    private static final String SING = "%F$#TYF%G123";

    /**
     * 生成token
     * @param map 存入到payload的数据
     * @return
     */
    public static String getToken(Map<String,String> map){
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.HOUR,1);//设置令牌过期时间一个小时

        //创建jwt builder
        JWTCreator.Builder builder = JWT.create();

        //遍历map中的数据存入payload
        map.forEach((k,v) -> {
            builder.withClaim(k,v);
        });


        String token = builder.withExpiresAt(calendar.getTime())//指定令牌过期时间
                              .sign(Algorithm.HMAC256(SING));//指定盐
        return token;
    }

    /**
     * 验证token,如果验证通过则返回衣蛾DecodedJWT对象可以在这个对象中获取token中的数据
     * @param token
     * @return
     */
    public static DecodedJWT verity(String token){
        //创建验证对象
        JWTVerifier build = JWT.require(Algorithm.HMAC256(SING)).build();
        //验证,如果出错会报异常
        DecodedJWT verify = build.verify(token);
        return verify;

        /**
         *  获取解析后的token中的payload信息
         *     Claim userId = verify.getClaim("userId");
         *     Claim userName = verify.getClaim("userName");
         *     System.out.println(userId.asInt());
         *     System.out.println(userName.asString());
         */
    }
}
  1. 登录成功后将随机生成的token(uuid)作为键用户信息作为值存入redis,并使用JWTUtil将uuid生成JWT存入Auth-Server域名下的cookie。

  1. 给需要用户信息的服务编写拦截器,因为cookie不能跨域,所以在此服务下获取不到登录信息,因此我们在拦截器中重定向到Auth-Server服务获取jwt,获取到jwt后用JWTUtil解析出payload中的token再根据token到redis中查出用户信息放入请求域中,并且将jwt存到当前服务域名下的cookie中方便下一次该服务就不用重定向到Auth-Server中获取jwt了。

拦截器:

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

        MemberEntity loginUser = null;
        String token = null;

        //查看在当前域名是否已经有token的cookie
        Cookie[] cookies1 = request.getCookies();
        for(Cookie c : cookies1){
            if(c.getName().equals("token")){
                token = c.getValue();
            }
        }

        //如果当前域名没有token
        if(StringUtils.isEmpty(token)){
            //获取当前请求的地址
            String serverName = "http://mymall.com";
            String requestURI = request.getRequestURI();
            String url = serverName + requestURI;

            //如果当前域名没有token,就带上url去认证服务认证获取token
            token = request.getParameter("token");
            if(StringUtils.isEmpty(token)){
                response.sendRedirect("http://auth.mymall.com/auth" + "?url=" + url);
                return true;
            }
        }

        //第二次来到拦截器,如果token是“nothing”说明认证服务的域名下没有cookie即没有用户的登录信息,
        // 则直接放行这样请求域中就没有用户的登录信息
        if(!token.equals("nothing")){
            //token不是nothing说明有cookie,则把用户信息放入请求域
            try {
                DecodedJWT verity = JwtUtil.verity(token);
                //校验成功之后从jwt中获取payload中的用户id
                String userId = verity.getClaim("userId").asString();
                //再根据用户id到redis中找到对应的用户信息
                ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
                String s = ops.get(userId);
                loginUser = JSON.parseObject(s,MemberEntity.class);
                //将token存入当前域名下的cookie
                Cookie c = new Cookie("token", token);
                response.addCookie(c);
                //放入请求域中
                request.setAttribute("loginUser",loginUser);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return true;
    }

Auth-server中的认证接口:

/**
     * 认证(获取token)
     * @param request
     * @param token
     * @return
     */
    @GetMapping("/auth")
    public String login(HttpServletRequest request, @CookieValue(value = "token",required = false) String token){

        if(StringUtils.isEmpty(token)){
            token = "nothing";
        }

        String url = request.getParameter("url");

        return "redirect:" + url + "?token=" +token;
    }
  1. 在页面中获取请求域中的登录用户的昵称

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值