@[Toc]
一、JWT简介
JSON Web Token(JWT)是一个开放的标准(RFC 7519),它定义了一个紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。
1、JWT认证
传统的session认证一般是这样的流程:
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
-
- 3、服务器向用户返回一个 session_id,写入用户的 Cookie。
- 4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
- 5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
session认证流程
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
一种解决方案是 session共享,将session持久化或者存入缓存。各种服务收到请求后,都向持久层或缓存请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层或者缓存万一挂了,就会认证失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
JWT认证流程:
- 1、 用户使用账号和密码发出post请求;
- 2、 服务器使用私钥创建一个jwt;
3、 服务器返回这个jwt给浏览器;
- 4、 浏览器将该jwt串在请求头中像服务器发送请求;
5、 服务器验证该jwt;
- 6、 返回响应的资源给浏览器。
jwt认证流程
2、JWT的组成
JWT信息
从上图可以看到,JWT含有三部分:头部(header)、载荷(payload)、签名(signature)。
2.1、头部(header)
JWT的头部有两部分信息:
- 声明类型,这里是JWT
- 声明加密的算法,通常直接使用HMAC SHA256
头部示例如下:
{
"alg": "HS256",
"typ": "JWT"
}
头部一般使用base64加密(该加密是可以对称解密的),构成了第一部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2.2、载荷(payload)
这部分一般存放一些有效的信息。JWT的标准定义包含五个字段:
- iss:该JWT的签发者
- sub: 该JWT所面向的用户
- aud: 接收该JWT的一方
- exp(expires): 什么时候过期,这里是一个Unix时间戳
- iat(issued at): 在什么时候签发的
载荷示例如下:
{
"iss": "kkjwt",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "kk"
}
2.3、签名(signature)
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
4、JWT 的使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
Authorization: Bearer <token>
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
二、SpringBoot整合JWT
1、依赖
引入jwt的依赖
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
2、工具类
Jwt工具类进行token的生成和认证:
/**
* JWT工具类
* 用于生成和校验token
*/
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
/**
* 秘钥
*/
private static final String SECRET = "my_secret";
/**
* 过期时间
**/
private static final long EXPIRATION = 1800L;//单位为秒
/**
* 生成用户token,设置token超时时间
*/
public static String createToken(User user){
//过期时间
Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
String token= JWT.create()
.withHeader(map) //添加头部
//可以把数据存在claim中
.withClaim("id",user.getId()) //userId
.withClaim("name",user.getName())
.withClaim("userName",user.getUserName())
.withExpiresAt(expireDate) //超时设置,设置过期的日期
.withIssuedAt(new Date()) //签发时间
.sign(Algorithm.HMAC256(SECRET)); //SECRET加密
return token;
}
/**
* 检验token并解析token
*/
public static Map<String, Claim> verifyToken(String token){
DecodedJWT jwt=null;
try {
JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
jwt=verifier.verify(token);
}catch (Exception e){
logger.error(e.getMessage());
logger.error("解析编码异常");
}
return jwt.getClaims();
}
}
3、过滤器
JWT过滤器中进行token的校验和判断,,token不合法直接返回,合法则解密数据并把数据放到request中供后续使用。
为了使过滤器生效,需要在启动类添加注解@ServletComponentScan(basePackages = "edu.hpu.filter"):
@WebFilter(filterName = "jwtFilter",urlPatterns = "/secure/*")
public class JwtFilter implements Filter{
private final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setCharacterEncoding("UTF-8");
//获取header里的token
String token=request.getHeader("authorization");
if ("OPTIONS".equals(request.getMethod())) { //除了 OPTIONS请求以外, 其它请求应该被JWT检查
response.setStatus(HttpServletResponse.SC_OK);
filterChain.doFilter(request, response);
}else {
if (token == null) {
response.getWriter().write("没有token!");
return;
}
}
Map<String, Claim> userData = JwtUtil.verifyToken(token);
if (userData==null){
response.getWriter().write("token不合法!");
return;
}
Integer id = userData.get("id").asInt();
String name = userData.get("name").asString();
String userName = userData.get("userName").asString();
//拦截器 拿到用户信息,放到request中
request.setAttribute("id", id);
request.setAttribute("name", name);
request.setAttribute("userName", userName);
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
4、控制层
- 登录Controller进行登录操作,登录成功后生产token并返回:
/**
* 登录Controller
*/
@RestController
public class LoginController {
private final Logger logger = LoggerFactory.getLogger(LoginController.class);
//模拟数据库
static Map<Integer, User> userMap = new HashMap<>();
static {
User user1 = new User(1, "zhangsan", "张三", "123456");
userMap.put(1, user1);
User user2 = new User(2, "lisi", "李四", "123123");
userMap.put(2, user2);
}
/**
* 模拟用户登录
*/
@RequestMapping("/login")
public String login(User user){
for (User dbUser : userMap.values()) {
if (dbUser.getName().equals(user.getName()) && dbUser.getPassword().equals(user.getPassword())) {
logger.info("登录成功!生成token!");
String token = JwtUtil.createToken(dbUser);
return token;
}
}
return "";
}
}
- SecureController中的请求会被JWT过滤器拦截,合法后才能访问:
**
* 需要登录后才可访问
*/
@RestController
public class SecureController {
private final Logger logger = LoggerFactory.getLogger(SecureController.class);
/**
* 查询用户信息,登录后才可访问
*/
@RequestMapping("/secure/getUserInfo")
public String getUserInfo(HttpServletRequest request){
Integer id = (Integer) request.getAttribute("id");
String name = request.getAttribute("name").toString();
String userName = request.getAttribute("userName").toString();
return "当前用户信息id=" + id + ",name=" + name + ",userName=" + userName;
}
}
三、测试
测试时先访问登录接口,根据用户名和密码生成token,再将token放在请求头里,去访问需要登录才能访问的接口。
1、访问登录接口
直接访问登录接口:http://localhost:8080/login?name=zhangsan&password=123456
登录成功则返回token:
2、访问需要登录的接口
访问http://localhost:8080/secure/getUserInfo,请求头里需要携带token:
成功获取用户信息。
本文为学习笔记类博客,学习资料来源见参考!
参考: 【1】:《SpringBoot Vue 全栈开发实战》 【2】:[一分钟带你了解JWT认证!](https://www.cnblogs.com/haha12/p/11796456.html) 【3】:[SpringBoot集成JWT实现权限认证](https://www.cnblogs.com/haha12/p/11818095.html) 【4】:[使用 JWT 来保护你的 SpringBoot 应用](https://zhuanlan.zhihu.com/p/110574995) 【5】:[基于jwt的token验证](https://www.cnblogs.com/better-farther-world2099/p/9146143.html) 【6】:[前后端分离之JWT用户认证 ](https://lion1ou.win/2017/01/18/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io) 【7】:[JSON Web Token 入门教程](http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) 【8】:[傻傻分不清之 Cookie、Session、Token、JWT](https://juejin.im/post/5e055d9ef265da33997a42cc#heading-4)