前言:
在当今的互联网世界中,随着应用程序和网络服务的不断增多,用户身份验证和授权变得至关重要。传统的身份验证方式,如基于会话的认证,已经逐渐显露出一些局限性和安全风险。
JSON Web Token(JWT)作为一种现代化的身份验证和授权解决方案,被广泛采用并逐渐成为业界标准。它以其简单、可靠和安全的特性,成为众多开发人员和技术团队的首选。
本博客将帮助你深入了解JWT的概念、工作原理以及如何在实际应用中使用JWT来保护你的应用程序。
JWT (JSON Web Token) 是一种用于身份验证和授权的开放标准,而传统的会话(session)是一种在服务器端存储用户状态的机制。下面是 JWT 和传统的会话机制之间的优劣势比较:
JWT 的优势:
- 无状态性:JWT 是无状态的,即服务器不需要在后端存储会话信息。每个请求都包含了自身的认证和授权信息,这使得 JWT 在分布式系统中更易于扩展和维护。
- 跨域支持:由于 JWT 是通过在请求的头部或参数中传递信息来进行身份验证和授权的,因此它能够轻松地应对跨域请求,并且不受同源策略的限制。
- 可扩展性:权JWT 的载荷部分可以自定义添加额外的信息,因此可以在令牌中携带更多的用户相关数据,如用户角色、限等。
JWT 的劣势:
- 安全性依赖于密钥管理:JWT 使用密钥对令牌进行签名,以确保其完整性和真实性。因此,密钥的安全管理变得非常重要,一旦密钥泄露,黑客就可能篡改或伪造有效的 JWT 令牌。
- 无法主动使令牌失效:一旦 JWT 令牌签发,它的有效期内都是有效的,并且服务器无法主动使其失效。如果需要吊销令牌,只能等待令牌过期或者更新密钥。
- 增加网络传输量:JWT 将用户身份信息直接存储在令牌中,因此每个请求都会携带这些信息,导致网络传输量增加。
传统的会话机制(例如session)的优势:
- 支持主动注销:传统会话机制可以通过在服务器端删除会话信息来主动注销用户。
- 较强的安全性:由于会话信息存储在服务器端,黑客很难篡改或伪造会话状态。
传统的会话机制的劣势:
- 需要在服务器端存储会话信息:每个会话都需要在服务器端维护相应的状态信息,这对于分布式系统来说可能不太适用。
- 不支持跨域请求:会话机制通常依赖于浏览器发送 Cookie 来进行身份验证,因此不适合处理跨域请求。
总的来说,JWT 在无状态、跨域支持和可扩展性方面具有优势,而传统的会话机制则在主动注销和安全性方面更为有利。选择使用哪种机制取决于具体的需求和应用场景。
有那么多的技术用于跨域身份验证我们为什么要用JWT呢?
JWT的精髓在于:“去中心化”,数据是保存在客户端的。
如果使用传统的session的话:
按之前的方法:
1、用户向服务器发送用户名和密码
2、服务器验证后,相关的数据会保存在会话中。
3、服务器向用户返回session_id,session信息写入用户的cookie中。
4、用户的每个后续的请求都将在cookie中取出session_id传给服务器。
5、服务器接收后与之前存储的数据进行对比,确认用户身份。
对于这种方法优化的话,应该持久化session数据,写入数据库或者文件持久层等。收到请求后,验证服务从持久层请求数据。
但是这种方法工作量大,由于依赖持久的数据库,会有风险。
JWT(JSON Web Token)由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
头部(Header):包含了关于该JWT的元数据信息,以 JSON 对象的形式表示。通常包含两个字段:
- "alg"(算法):指定用于签名 JWT 的算法,例如 HMAC SHA256 或 RSA。
- "typ"(类型):指定令牌的类型,一般都是 "JWT"。
载荷(Payload):存储实际的信息数据,也是以 JSON 对象的形式表示。载荷可以包含一些预定义的声明(Claims),或者自定义的声明。预定义的声明包括:
- "iss"(Issuer):令牌的发行者。
- "sub"(Subject):令牌所面向的用户。
- "aud"(Audience):令牌的接收者。
- "exp"(Expiration Time):令牌的过期时间。
- "nbf"(Not Before):令牌的生效时间。
- "iat"(Issued At):令牌的发行时间。
- "jti"(JWT ID):令牌的唯一标识符。
签名(Signature):使用密钥通过选定的算法对头部和载荷进行签名生成的字符串。签名用于验证 JWT 的完整性和真实性。具体的签名算法根据头部的 "alg" 字段确定。
JWT 的基本结构如下:
header.payload.signature
,使用 Base64 编码进行序列化。最终的 JWT 是一个字符串,可以通过在两个点(.)处分隔三个部分来解析和验证。
1、JWT默认不加密,但可以加密。生成原始令牌后,可以使用改令牌再次对其进行加密。
2、当JWT未加密方法是,一些私密数据无法通过JWT传输。
3、JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
4、JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
5、JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
6、为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
用户通过提供有效的身份信息,例如用户名和密码,向服务器发送请求。
服务器验证用户身份信息,并生成一个JWT。
服务器将生成的JWT作为响应返回给客户端。
客户端收到JWT后,将其存储在本地,通常使用浏览器的本地存储或者Cookie。
客户端在后续的请求中,将JWT添加到请求的头部或其他适当位置进行传递。
服务器接收到请求后,使用密钥验证JWT的有效性和完整性,如果验证通过,则对请求进行处理。
客户端和服务器之间通过JWT实现无状态的身份验证和授权。
对于JWT(JSON Web Token)的工具类,可以使用现有的第三方库来简化操作。以下是一个示例工具类的概述:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtils {
private static final String SECRET_KEY = "your-secret-key";
private static final long EXPIRATION_TIME = 86400000; // 24小时
// 创建JWT
public static String createJwt(String subject, Map claims) {
Date now = new Date();
Date expirationDate = new Date(now.getTime() + EXPIRATION_TIME);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 解析JWT
public static Claims parseJwt(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
使用该工具类,你可以创建和解析JWT。
创建JWT的示例代码:
Map claims = new HashMap<>();
claims.put("userId", 123456);
claims.put("role", "admin");
String jwt = JwtUtils.createJwt("[email protected]", claims);
解析JWT的示例代码:
Claims claims = JwtUtils.parseJwt(jwt);
String userId = claims.get("userId").toString();
String role = claims.get("role").toString();
请注意,在实际使用中,应该根据具体情况自定义更多的方法和逻辑,比如验证JWT的有效性、获取特定的声明等。此外,SECRET_KEY 应该保密存储,并且可以根据需要进行更改。
JWTFilter:
package com.zking.vue.util;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import io.jsonwebtoken.Claims;
/**
* * JWT验证过滤器,配置顺序 :CorsFilte-->JwtFilter-->struts2中央控制器
*
* @author Administrator
*
*/
public class JwtFilter implements Filter {
// 排除的URL,一般为登陆的URL(请改成自己登陆的URL)
private static String EXCLUDE = "^/vue/userAction_login\\.action?.*$";
private static Pattern PATTERN = Pattern.compile(EXCLUDE);
private boolean OFF = true;// true关闭jwt令牌验证功能
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String path = req.getServletPath();
if (OFF || isExcludeUrl(path)) {// 登陆直接放行
chain.doFilter(request, response);
return;
}
// 从客户端请求头中获得令牌并验证
String jwt = req.getHeader(JwtUtils.JWT_HEADER_KEY);
Claims claims = this.validateJwtToken(jwt);
if (null == claims) {
// resp.setCharacterEncoding("UTF-8");
resp.sendError(403, "JWT令牌已过期或已失效");
return;
} else {
String newJwt = JwtUtils.copyJwt(jwt, JwtUtils.JWT_WEB_TTL);
resp.setHeader(JwtUtils.JWT_HEADER_KEY, newJwt);
chain.doFilter(request, response);
}
}
/**
* 验证jwt令牌,验证通过返回声明(包括公有和私有),返回null则表示验证失败
*/
private Claims validateJwtToken(String jwt) {
Claims claims = null;
try {
if (null != jwt) {
claims = JwtUtils.parseJwt(jwt);
}
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 是否为排除的URL
*
* @param path
* @return
*/
private boolean isExcludeUrl(String path) {
Matcher matcher = PATTERN.matcher(path);
return matcher.matches();
}
// public static void main(String[] args) {
// String path = "/sys/userAction_doLogin.action?username=zs&password=123";
// Matcher matcher = PATTERN.matcher(path);
// boolean b = matcher.matches();
// System.out.println(b);
// }
}
在没开启JWT之前我们可以通过地址栏输入的方式跳过登录步骤直接进入页面。
在开启JWT之后我们直接通过地址栏进行页面跳转是不成功的。
UserAction:
package com.zking.vue.web;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.opensymphony.xwork2.ModelDriven;
import com.zking.base.web.BaseAction;
import com.zking.vue.biz.UserBiz;
import com.zking.vue.entity.User;
import com.zking.vue.util.JsonData;
import com.zking.vue.util.JwtUtils;
import com.zking.vue.util.PageBean;
import com.zking.vue.util.ResponseUtil;
import com.zking.vue.util.StringUtils;
public class UserAction extends BaseAction implements ModelDriven{
private UserBiz userBiz;
private User user = new User();
public UserBiz getUserBiz() {
return userBiz;
}
public void setUserBiz(UserBiz userBiz) {
this.userBiz = userBiz;
}
public String login() {
ObjectMapper om = new ObjectMapper();
JsonData jsonData = null;
try {
if(StringUtils.isBlank(user.getUname()) || StringUtils.isBlank(user.getPwd())) {
jsonData = new JsonData(0, "用户或者密码为空", user);
}else {
User u = this.userBiz.login(user);
Map claims = new HashMap();
claims.put("uname",user.getUname());
claims.put("pwd", user.getPwd());
String jwt = JwtUtils.createJwt(claims, JwtUtils.JWT_WEB_TTL);
response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);
jsonData = new JsonData(1, "登录成功", u);
}
} catch (Exception e) {
e.printStackTrace();
jsonData = new JsonData(0, "用户或者密码错误", user);
}finally {
try {
ResponseUtil.write(response, om.writeValueAsString(jsonData));
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
public String getAsyncData() {
ObjectMapper om = new ObjectMapper();
try {
Thread.sleep(6000);
ResponseUtil.write(response, om.writeValueAsString("http://www.javaxl.com"));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public User getModel() {
return user;
}
}
mutations.js:
export default {
// type(事件类型): 其值为setEduName
// payload:官方给它还取了一个高大上的名字:载荷,其实就是一个保存要传递参数的容器
setEduName: (state, payload) => {
state.eduName = payload.eduName;
},
setJwt:(state,payload)=>{
state.jwt=payload.jwt;
}
}
state.js:
export default{
address:'默认值',
jwt:''
}
http.js:
/**
* vue项目对axios的全局配置
*/
import axios from 'axios'
import qs from 'qs'
//引入action模块,并添加至axios的类属性urls上
import action from '@/api/action'
axios.urls = action
// axios默认配置
axios.defaults.timeout = 10000; // 超时时间
// axios.defaults.baseURL = 'http://localhost:8080/j2ee15'; // 默认地址
axios.defaults.baseURL = action.SERVER;
//整理数据
// 只适用于 POST,PUT,PATCH,transformRequest` 允许在向服务器发送前,修改请求数据
axios.defaults.transformRequest = function(data) {
data = qs.stringify(data);
return data;
};
// 请求拦截器
axios.interceptors.request.use(function(config) {
var jwt = window.vm.$store.getters.getJwt;
config.headers['jwt'] = jwt;
return config;
}, function(error) {
return Promise.reject(error);
});
export default axios;
整体的思路就是:1.我们通过登录,发送请求到后台JwtDemo中。2.在JwtDemo中会生成Jwt的串。3.将串放到响应头中去,通过控制台的网络中的标头可以查看到Jwt的串。4.请求头会被http.js中的响应拦截器拦截。5.拦截后会将Jwt的串放到state.js中。6.第二次发送queryRootNode请求,会被请求拦截器拦截,就会取出Jwt串,放到请求拦截器中,通过请求头可以查看到串,可以查看的原因是vuex,http.js.。7.请求会被提交到后台,通过JwtFilter类中获取请求头(更具校验获取登录信息)