入门 - SpringBoot 2.x 使用 JWT
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,本文介绍它的原理和用法
一、跨域认证遇到的问题
由于多终端的出现,很多的站点通过 web api restful 的形式对外提供服务,采用了前后端分离模式进行开发,因而在身份验证的方式上可能与传统的基于 cookie 的 Session 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 位或者 128 位 token ,放入 Redis 中,然后返回给客户端并存储)。
③、用户第一次登录成功时,需要先自行生成 token,然后将 token 返回到浏览器并存储在 cookie 中,
并在 Redis 服务器上以 token 为 key,用户信息作为 value 保存。后续用户再操作,可以通过 HttpServletRequest 对象直接读取 cookie 中的 token,并在 Redis 中取得相对应的用户数据进行比较(用户每次访问都携带此 token,服务端去 Redis 中校验是否有此用户即可)。
④、 缺点:必须部署 Redis,每次必须访问 Redis,IO 开销特别大。
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。
入门代码案例:
第一:导入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.bilibili</groupId> <artifactId>token</artifactId> <version>0.0.1-SNAPSHOT</version> <name>token</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
第二步:写一个实体类
package com.bilibili.pojo; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import java.io.Serializable; import java.util.Date; 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; public User() { } public User(Integer id, String openid, String name, String headImg, String phone, String sign, Integer sex, String city, Date createTime) { this.id = id; this.openid = openid; this.name = name; this.headImg = headImg; this.phone = phone; this.sign = sign; this.sex = sex; this.city = city; this.createTime = createTime; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getHeadImg() { return headImg; } public void setHeadImg(String headImg) { this.headImg = headImg; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public Integer getSex() { return sex; } public void setSex(Integer sex) { this.sex = sex; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } @Override public String toString() { return "User{" + "id=" + id + ", openid='" + openid + '\'' + ", name='" + name + '\'' + ", headImg='" + headImg + '\'' + ", phone='" + phone + '\'' + ", sign='" + sign + '\'' + ", sex=" + sex + ", city='" + city + '\'' + ", createTime=" + createTime + '}'; } }
第三步:写一个JwtUtils工具类
package com.bilibili.utils; import com.bilibili.pojo.User; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JwtUtils { public static final String SUBJECT = "RookieLi"; public static final String SECRETKEY = "Rookie666"; // 密钥 public static final long EXPIRE = 1000 * 60 * 60 * 24 * 7; //过期时间,毫秒,一周 public static String getJsonWebToken(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; } public static Claims chekcJwt(String token) { try { final Claims claims = Jwts.parser().setSigningKey(SECRETKEY).parseClaimsJws(token).getBody(); return claims; } catch (Exception e) { e.printStackTrace(); } return null; } }
第四步:测试一下
package com.bilibili.token; import com.bilibili.pojo.User; import com.bilibili.utils.JwtUtils; import io.jsonwebtoken.Claims; import org.junit.Test; public class JwtUtilTest { @Test public void testGeneJwt() { User user = new User(); user.setId(999); user.setHeadImg("I'm busy"); user.setName("Rookie"); String token = JwtUtils.getJsonWebToken(user); System.out.println(token); } @Test public void testCheck() { // 下面此 token 字符串是上面的结果生成的,每次不一样,不是写死的 String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJSb29raWVMaSIsImlkIjo5OTksIm5hbWUiOiJSb29raWUiLCJpbWciOiJJJ20gYnVzeSIsImlhdCI6MTU5OTcyMzg1NiwiZXhwIjoxNjAwMzI4NjU2fQ.b83hvcgTClyGBnRipUMrvkZUgHPCe_jVfowMbMA__rk"; Claims claims = JwtUtils.chekcJwt(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"); } } }
此例子非常适合入门Token学习,希望能够先了解token的相关概念,然后上手写一下上面的例子。这样会对token有更进一步认识