首先jwt其实是三个英语单词JSON Web Token的缩写。通过全名你可能就有一个基本的认知了。token一般都是用来认证的,比如我们系统中常用的用户登录token可以用来认证该用户是否登录。jwt也是经常作为一种安全的token使用。
JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
简洁(Compact) : 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
自包含(Self-contained) :负载中包含了所有用户所需要的信息,避免了多次查询数据库
授权
这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是今广泛使用JWT的一项功能。因为它的开销很小并且可以在不同的域中轻松使用
信息交换
JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。
占带宽: 正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多。
无法在服务端注销,那么就很难解决劫持问题
性能问题: JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。但是大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。听着似乎很牛逼,但是没有任何优势,为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程:
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
后端核对用户名和密码成功后,将用户的id等其他信息作为 JWT-Payload(负载),将其与头部分别进行Base64编码拼接后签名形成一个JWT。形成的JWT就是一个形同111.zzz.xxx的字符串。
后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature).
jwt的头部承载两部分信息:
完整的头部
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷就是存放有效信息的地方。这些有效信息包含三个部分:标准中注册的声明,公共的声明,私有的声明
标准中注册的声明:
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"iss": "John Wu JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "[email protected]",
"from_user": "B",
"target_user": "A"
"name": "John Doe",
"admin": true
}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:header (base64后的),payload (base64后的),secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意: secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
四.如何应用JWT
一般是在请求头里加入Authorization,并加上Bearer标注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:
引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
令牌的获取
@Test
void contextLoads() {
HashMap<String, Object> map = new HashMap<>();
//获取日历对象
Calendar instance = Calendar.getInstance();
//默认30S过期
instance.add(Calendar.SECOND,30);
String token = JWT.create()
.withHeader(map) //header,可以不写
.withClaim("userId", 21) //payload
.withClaim("username", "Garry") //payload
.withExpiresAt(instance.getTime()) //设置过期时间
.sign(Algorithm.HMAC256("!ISN!@#¥%")); //签名
System.out.println(token);
}
引入依赖
<!--JWT的依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!--引入mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--引入lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<!--引入druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<!--引入mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
配置文件
server.port=8080
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
mybatis.type-aliases-package=com.sise.bean
mybatis.mapper-locations=classpath:mapper/*.xml
JWT工具类封装
@Component
public class JwtUtil {
/** 盐值*/
private static final String SING="LIUYISHOU@Token666";
//生成令牌
public static String getToken(Map<String,String> map){
//获取日历对象
Calendar calendar=Calendar.getInstance();
//默认7天过期
calendar.add(Calendar.DATE,7);
//新建一个JWT的Builder对象
JWTCreator.Builder builder = JWT.create();
//将map集合中的数据设置进payload
map.forEach((k,v)->{
builder.withClaim(k, v);
});
//设置过期时间和签名
String sign = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SING));
return sign;
}
/**
* 验签并返回DecodedJWT
* @param token 令牌
*/
public static DecodedJWT getTokenInfo(String token){
return JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
}
}
新建实体类
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private Integer id;
private String username;
private String password;
}
dao层
@Mapper
public interface UserDao {
/*登录方法*/
User login(User user);
}
UserMapper.xml文件
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sise.dao.UserDao">
<select id="login" parameterType="com.sise.bean.User" resultType="com.sise.bean.User">
select id, username, password from user where username=#{username} and password=#{password}
</select>
</mapper>
service层
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao;
@Override
public User login(User user) {
//接受用户查询数据库
User userDB = userDao.login(user);
//查询到这个用户就返回,没有则抛出错误
if (userDB != null) {
return userDB;
}else{
throw new RuntimeException("登录失败!");
}
}
}
controller层
@RestController
@Slf4j
public class UserController {
@Resource
private UserService userService;
@Resource
private JwtUtil jwtUtil;
//登录后返回token
@GetMapping("/user/login")
public Map<String,Object> login(User user){
//打印输入的用户名和密码
log.info("用户名:[{}]",user.getUsername());
log.info("密码:[{}]",user.getPassword());
//创建map,作为返回值
HashMap<String, Object> map = new HashMap<>();
try {
User login = userService.login(user);
//存储载荷声明参数map
HashMap<String, String> plMap = new HashMap<>();
plMap.put("username", login.getUsername());
//生成JWT令牌
String token = jwtUtil.getToken(plMap);
//通过验证,将相关用户信息及token等存入map,用于返回
map.put("state", true);
map.put("msg", "认证成功");
map.put("token", token);
} catch (Exception e) {
map.put("state", false);
map.put("msg", e.getMessage());
}
return map;
}
//传入token,验证是否通过
@PostMapping("/user/test1")
public Map<String,Object> test1(String token){
//创建map,作为返回值
HashMap<String, Object> map = new HashMap<>();
//打印输入的token
log.info("当前的token为:[{}]",token);
try {
//通过验证,将相关用户信息及token等存入map,用于返回
DecodedJWT verify = jwtUtil.getTokenInfo(token); //验证令牌
map.put("state",true);
map.put("msg","请求成功!");
return map;
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg","无效签名");
}catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token过期!");
}catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","token无效!");
}catch (Exception e){
e.printStackTrace();
map.put("msg","token无效!");
}
//无法通过验证
map.put("state",false);
return map;
}
}
编写拦截器
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HashMap<String, Object> map = new HashMap<>();
//获取存放在请求头中的token
String token = request.getHeader("token");
try {
JwtUtil.getTokenInfo(token); //验证令牌,验证通过则返回true
return true;
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg","无效签名");
}catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token过期!");
}catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","token无效!");
}catch (Exception e){
e.printStackTrace();
map.put("msg","token无效!");
}
//验证令牌不通过,返回false
map.put("state",false);
//将map转化为json,相应给前端
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
注册拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/test") //拦截test接口
.excludePathPatterns("/user/login"); //放行login接口
}
}
更改/user/test里面的方法,不需要再在里面编写验证token的代码了
//添加拦截器后,不需要编写验证token的代码
@PostMapping("/user/test")
public Map<String,Object> test(HttpServletRequest request){
HashMap<String, Object> map = new HashMap<>();
//处理自己的业务逻辑
//获取请求头中携带的token
String token = request.getHeader("token");
//验证token,获取token中的相关信息
DecodedJWT verify = jwtUtil.getTokenInfo(token);
log.info("用户名:[{}]",verify.getClaim("username").asString());
log.info("密码:[{}]",verify.getClaim("password").asString());
map.put("state",true);
map.put("msg","请求成功!");
return map;
}