JWT
JSON Web Token(JSON Web令牌)
是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。jwt可以使用秘密〈使用HNAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。
1. JWT作用
授权:一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。它的开销很小并且可以在不同的域中使用。如:单点登录。
信息交换:在各方之间安全地传输信息。JWT可进行签名(如使用公钥/私钥对),因此可确保发件人。由于签名是使用标头和有效负载计算的,因此还可验证内容是否被篡改
(1)传统的session认证
这种模式最大的问题是,没有分布式架构,无法支持横向扩展。如果使用一个服务器,该模式完全没有问题。但是,如果它是服务器群集或面向服务的跨域体系结构的话,则需要一个统一的session数据库库来保存会话数据实现共享,这样负载均衡下的每个服务器才可以正确的验证用户身份。
例如,举一个实际中常见的单点登陆的需求:站点A和站点B提供同一公司的相关服务。现在要求用户只需要登录其中一个网站,然后它就会自动登录到另一个网站。怎么做?
一种解决方案是听过持久化session数据,写入数据库或文件持久层等。收到请求后,验证服务从持久层请求数据。该解决方案的优点在于架构清晰,而缺点是架构修改比较费劲,整个服务的验证逻辑层都需要重写,工作量相对较大。而且由于依赖于持久层的数据库或者问题系统,会有单点风险,如果持久层失败,整个认证体系都会挂掉。
基于session认证所显露的问题
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
(2) 基于token的鉴权机制
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
- 用户使用用户名密码来请求服务器
- 服务器进行验证用户的信息
- 服务器通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附送上这个token值
- 服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)
策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *
。
通过客户端保存数据,而服务器根本不保存会话数据,每个请求都被发送回服务器。 JWT是这种解决方案的代表
JWT官网有一张图描述了JWT的认证过程:
2. JWT的原则
JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
{ "UserName": "Chongchong", "Role": "Admin", "Expire": "2018-08-08 20:15:56" }
之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名(有关详细信息,请参阅下文)。
服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
3. JWT的数据结构
典型的,一个JWT看起来如下图。
改对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。注意JWT对象为一个长字串,各字串之间也没有换行符,此处为了演示需要,我们特意分行并用不同颜色表示了。每一个子串表示了一个功能块,总共有以下三个部分:
JWT的三个部分如下。JWT头、有效载荷和签名,将它们写成一行如下。
3.1 JWT头
JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。
{ "alg": "HS256", "typ": "JWT" }
在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。
最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
3.2 有效载荷
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
{ "sub": "1234567890", "name": "wanglihong", "admin": true }
请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
JSON对象也使用Base64 URL算法转换为字符串保存
3.3签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret)
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。
3.4 Base64URL算法
如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。
作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法,很简单把。
4. JWT的用法
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。
Authorization: Bearer
当跨域时,也可以将JWT被放置于POST请求的数据主体中。
5. JWT问题和趋势
1、JWT默认不加密,但可以加密。生成原始令牌后,可以使用改令牌再次对其进行加密。
2、当JWT未加密方法是,一些私密数据无法通过JWT传输。
3、JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
4、JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
5、JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
6、为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
6. JWT与SpringBoot整合使用
6.1 pom.xml
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
6.2 TokenUtil : 封装了通过JWT 创建token, 签名, 获得数据信息
public class TokenUtil { //token到期时间60s private static final long EXPIRE_TIME= 600*1000; //密钥盐 private static final String TOKEN_SECRET="123456qwertyuiop789"; /** * 创建一个token * @param user * @return */ public static String sign(Customer user){ System.out.println("sign customer:" + user); String token=null; try { Date expireAt=new Date(System.currentTimeMillis()+EXPIRE_TIME); token = JWT.create() //发行人 .withIssuer("auth0") //存放数据 .withClaim("custId",user.getCustId()) .withClaim("custName",user.getCustName()) .withClaim("custPassword",user.getCustPassword()) //过期时间 .withExpiresAt(expireAt) .sign(Algorithm.HMAC256(TOKEN_SECRET)); } catch (IllegalArgumentException| JWTCreationException je) { } return token; } /** * 对token进行验证 * @param token * @return */ public static Boolean verify(String token){ try { //创建token验证器 JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build(); DecodedJWT decodedJWT=jwtVerifier.verify(token); System.out.println("认证通过:"); System.out.println("custId: " + TokenUtil.getUserId(token)); System.out.println("custName: " + TokenUtil.getUserName(token)); System.out.println("过期时间: " + decodedJWT.getExpiresAt()); } catch (IllegalArgumentException | JWTVerificationException e) { e.printStackTrace(); //抛出错误即为验证不通过 return false; } return true; } /** * 获取用户名 */ public static String getUserName(String token){ try{ DecodedJWT jwt= JWT.decode(token); String username = jwt.getClaim("custName").asString(); System.out.println("用户名:" + username); return username; }catch (JWTDecodeException e) { e.printStackTrace(); return null; } } public static Integer getUserId(String token){ try{ DecodedJWT jwt= JWT.decode(token); Integer userId = jwt.getClaim("custId").asInt(); System.out.println("用户id:" + userId); return userId ; }catch (JWTDecodeException e) { e.printStackTrace(); return null; } } }
6.3 用户验证拦截器 TokenInterceptor
@Component public class TokenInterceptor implements HandlerInterceptor { // /cart -----[拦截验证]-------controller @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //跨域请求会首先发一个option请求,直接返回正常状态并通过拦截器 if(request.getMethod().equals("OPTIONS")){ response.setStatus(HttpServletResponse.SC_OK); return true; } //获取到token String token = request.getHeader("token"); if (token!=null){ boolean result= TokenUtil.verify(token); if (result){ System.out.println("通过拦截器"); return true; } } try { JSONObject json=new JSONObject(); json.put("msg","token verify fail"); json.put("code","500"); response.getWriter().append(json.toString()); System.out.println("认证失败,未通过拦截器"); } catch (Exception e) { return false; } return false; } }
URL 拦截配置类
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) // 进行拦截,一般登录不拦截,企业都拦截 .addPathPatterns("/customer/**") .excludePathPatterns("/customer/login") ; } }
6.4 登录请求 controller
@Controller @RequestMapping("/customer") public class CustomerController { @Autowired private ICustomerService customerService; //登录 @PostMapping("login") @ResponseBody public String login(Customer customer){ ServerResponse response = customerService.login(customer); System.out.println("controller response:" + response); if(response.getCode() ==200) return "login success"; return "login fail"; }
6.5 service : 登录核心业务
@Service public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements ICustomerService { @Autowired private CustomerMapper customerMapper; @Override public ServerResponse login(Customer customer) { //1. DB 验证用户名与密码 QueryWrapper<Customer> wrapper = new QueryWrapper<>(); wrapper.eq("cust_name",customer.getCustName()); wrapper.eq("cust_password",customer.getCustPassword()); Customer loginCustomer = customerMapper.selectOne(wrapper); System.out.println("查询到的登录账户:" + loginCustomer); if(loginCustomer !=null) { //2. JWT创建token String token = TokenUtil.sign(loginCustomer); System.out.println("service token:" + token); return ServerResponse.success("登录成功", loginCustomer); }else return ServerResponse.fail("登录失败",loginCustomer); }
6.6 如在下订单业务中,想获得登录用户的信息
@PostMapping("submitOrder") public String submitOrder(){ //1. 获得页面的订单与商品信息 //2. 获得登录用户信息 custId , custName String token = null;// 请求头中获得token ?????? if(TokenUtil.verify(token)){ Integer custId = TokenUtil.getUserId(token); String custName = TokenUtil.getUserName(token); System.out.println("获得了 custId:" + custId); System.out.println("获得了 custName:" + custName); }else{ System.out.println("验证token无效"); } return "ok"; }