SpringBoot 2.x 使用 JWT(JSON Web Token)

一、跨域认证遇到的问题

由于多终端的出现,很多的站点通过 web api restful 的形式对外提供服务,采用了前后端分离模式进行开发,因而在身份验证的方式上可能与传统的基于 cookieSession Id 的做法有所不同,除了面临跨域提交 cookie 的问题外,更重要的是,有些终端可能根本不支持 cookie

JWT(JSON Web Token) 是一种身份验证及授权方案,简单的说就是调用端调用 api 时,附带上一个由 api 端颁发的 token,以此来验证调用者的授权信息。

一般流程是下面这样:

1. 用户向服务器发送用户名和密码。
2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3. 服务器向用户返回一个 session_id,写入用户的 Cookie。
4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于扩展性不好。单机没有问题,如果是服务器集群、跨域的服务导向架构或者用户禁用了 cookie ,就不行了。

二、解决方案

1. 单机和分布式应用下登录校验,session 共享

  • 单机和多节点 tomcat 应用登录检验

    ①、单机 tomcat 应用登录,sesssion 保存在浏览器和应用服务器会话之间,用户登录成功后,服务端会保证一个 session,也会给客户端一个 sessionId,客户端会把 sessionId 保存在 cookie 中,用户每次请求都会携带这个 sessionId

    ②、多节点 tomcat 应用登录,开启 session 数据共享后,每台服务器都能够读取 session。缺点是每个 session 都是占用内存和资源的,每个服务器节点都需要同步用户的数据,即一个数据需要存储多份到每个服务器,当用户量到达百万、千万级别的时,占用资源就严重,用户体验特别不好!!

  • 分布式应用中 session 共享

    ①、真实的应用不可能单节点部署,所以就有个多节点登录 session 共享的问题需要解决。tomcat 支持 session 共享,但是有广播风暴;用户量大的时候,占用资源就严重,不推荐

    ②、Reids 集群,存储登陆的 token,向外提供服务接口,Redis 可设置过期时间(服务端使用 UUID生成随机 64 位或者 128token ,放入 Redis 中,然后返回给客户端并存储)。

    ③、用户第一次登录成功时,需要先自行生成 token,然后将 token 返回到浏览器并存储在 cookie 中,
    并在 Redis 服务器上以 tokenkey,用户信息作为 value 保存。后续用户再操作,可以通过 HttpServletRequest 对象直接读取 cookie 中的 token,并在 Redis 中取得相对应的用户数据进行比较(用户每次访问都携带此 token,服务端去 Redis 中校验是否有此用户即可)。

    ④、 缺点:必须部署 Redis,每次必须访问 RedisIO 开销特别大。

2. 最终解决方案:使用 JWT 实现 Token 认证

  • JWT 的原理

    服务器认证以后,生成一个 JSON 对象发回给用户,以后用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。也就是说服务器就不保存任何 session 数据了,即服务器变成无状态了,从而比较容易实现扩展。

    简单来说,就是通过一定规范来生成 token,然后可以通过解密算法逆向解密 token,这样就可以获取用户信息
    
  • 优点和缺点

    优点:生产的 token 可以包含基本信息,比如 id、用户昵称、头像等信息,避免再次查库;存储在客户端,不占用服务端的内存资源

    缺点:token 是经过 base64 编码,所以可以解码,因此 token 加密前的对象不应该包含敏感信息(如用户权限,密码等)

  • JWT 格式组成:头部+负载+签名 ( header + payload + signature )

    头部:主要是描述签名算法。

    负载:主要描述是加密对象的信息,如用户的 id 等,也可以加些规范里面的东西,如 iss 签发者,exp 过期时间,sub 面向的用户。

    签名:主要是把前面两部分进行加密,防止别人拿到 token 进行base 解密后篡改 token。

3. 案例图设计

三、代码演示案例

  • pom.xml 文件引入依赖和实体类
    <!-- 依赖可以减少实体类 getter/setter等方法书写 -->
    <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <optional>true</optional>
    </dependency>
    <!-- JWT相关 -->
    <dependency>
       <groupId>io.jsonwebtoken</groupId>
       <artifactId>jjwt</artifactId>
       <version>0.7.0</version>
    </dependency>
    
    ====================================================================================
    
    @Getter
    @Setter
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements Serializable {
    
        private Integer id;
        private String openid;
        private String name;
        private String headImg;
        private String phone;
        private String sign;
        private Integer sex;
        private String city;
        private Date createTime;
    
    }
    
  • 生成 JWT 工具类
    public class JwtUtil {
    
        // 主题
        public static final String SUBJECT = "RookieLi";
    
        // 秘钥
        public static final String SECRETKEY = "Rookie666";
    
        // 过期时间
        public static final long EXPIRE = 1000 * 60 * 60 * 24 * 7;  //过期时间,毫秒,一周
    
        // 生成 JWT
        public static String geneJsonWebToken(User user) {
    
            if (user == null ||
                    user.getId() == null ||
                    user.getName() == null ||
                    user.getHeadImg() == null) {
    
                return null;
            }
            String token = Jwts.builder()
                    .setSubject(SUBJECT)
                    .claim("id", user.getId())
                    .claim("name", user.getName())
                    .claim("img", user.getHeadImg())
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                    .signWith(SignatureAlgorithm.HS256, SECRETKEY).compact();
    
            return token;
        }
    
    
        // 校验 JWT
        public static Claims checkJWT(String token) {
    
            try {
                final Claims claims = Jwts.parser().setSigningKey(SECRETKEY).
                        parseClaimsJws(token).getBody();
                return claims;
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
    
  • 测试 JWT 工具类
    public class JwtUtilTest {
    
        @Test
        public void testGeneJwt(){
        
            User user = new User();
            user.setId(999);
            user.setHeadImg("I'm busy");
            user.setName("Rookie");
            String token = JwtUtil.geneJsonWebToken(user);
            System.out.println(token);
    
        }
    
    
        @Test
        public void testCheck(){
    
            // 下面此 token 字符串是上面的结果生成的,每次不一样,不是写死的
            String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJSb29raWVMaSIsImlkIjo5OTksIm5hbWUiOiJSb29raWUiLCJpbWciOiJJJ20gYnVzeSIsImlhdCI6MTU2NzMxNjk4NywiZXhwIjoxNTY3OTIxNzg3fQ.FJh41VwVh2gh5-_cOG0SOgoO3dR_ZcK9VWNNskWqKl0";
            Claims claims = JwtUtil.checkJWT(token);
            if(claims != null){
                String name = (String)claims.get("name");
                String img = (String)claims.get("img");
                int id =(Integer) claims.get("id");
                System.out.println(name);
                System.out.println(img);
                System.out.println(id);
            }else{
                System.out.println("非法token");
            }
        }
    }
    

    参考博客:

    http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

    https://www.cnblogs.com/jpfss/p/10929458.html

posted @ 2019-09-01 14:17  RookieMZL  阅读(2045)  评论(0编辑  收藏  举报