一:认证

在了解JWT之前先来回顾一下传统session认证和基于token认证。

1.1 传统session认证

http协议是一种无状态协议,即浏览器发送请求到服务器,服务器是不知道这个请求是哪个用户发来的。为了让服务器知道请求是哪个用户发来的,需要让用户提供用户名和密码来进行认证。当浏览器第一次访问服务器(假设是登录接口),服务器验证用户名和密码之后,服务器会生成一个sessionid(只有第一次会生成,其它会使用同一个sessionid),并将该session和用户信息关联起来,然后将sessionid返回给浏览器,浏览器收到sessionid保存到Cookie中,当用户第二次访问服务器是就会携带Cookie值,服务器获取到Cookie值,进而获取到sessionid,根据sessionid获取关联的用户信息。

public User login(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = userService.login(username, password);
if (user == null) {
throw new AuthenticationException("用户名或密码错误");
}
// 返回这次请求关联的当前会话,如果没有会话则创建一个新的
// 需要在服务器端记录该session
HttpSession session = request.getSession();
session.setAttribute("user", user);
// 让浏览器保存sessionid到cookie中
// Cookie cookie = new Cookie("sessionid", session.getId());
// cookie.setPath("/");
// response.addCookie(cookie);
return user;
}
public Object getUserInfo(HttpServletRequest request){
// 从request中获取Cookie
// 从Cookie中获取sessionid
// 根据sessionid获取对应的Session对象
// 从session中获取关联的用户信息
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
return user;
}

session的缺点:

  • Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
  • 扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。即不能满足单点登陆
  • CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

1.2 基于token认证

token原理:

  1. 使用用户名和密码请求登录接口
  2. 登录接口验证用户名和密码
  3. 登录接口生成一个uuid作为token,将用户信息作为值,然后保存到redis缓存中jedis.set(token, user);
  4. 登录接口返回用户信息和token
  5. 浏览器将token保存到本地
  6. 当请求其它接口时就携带token值
  7. 接口根据token去缓存中查,如果找到了就调用接口,如果找不到报token错误(一般通过拦截器来实现检查)
public String auth(String username, String password) throws AuthenticationException {
User user = userService.login(username, password);
if (user == null) {
throw new AuthenticationException("用户名或密码错误");
}
String token = UUID.randomUUID().toString();
redisClient.set(token, user);
return token;
}
public Object getUserInfo(@RequestHeader("token") String token) throws AuthenticationException {
User user = redisClient.get(token);
if (user == null) {
throw new AuthenticationException("token不可用");
}
return user;
}

session和token的区别:

此种方式原理上和session方式差不多,都是客户端调用接口时携带一个值,服务器通过该值来获取用户的信息。

不同的是session是将信息保存到本机内存中,对负载均衡有限制(只能负载到同一台机器),token是保存到缓存服务器(redis)中,对负载均衡没有限制,如果使用同一个redis服务器还可以保证单点登录。session一般用在PC上,token即可用在PC上也可以用在APP上。

1.3 JWT

1.3.1 简介

JSON Web Token(JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519),它定义了一种紧凑(Compact)且自包含(Self-contained)的方式,用于在各方之间以JSON对象安全传输信息。 这些信息可以通过数字签名进行验证和信任。 可以使用秘密(使用HMAC算法)或使用RSA的公钥/私钥对对JWT进行签名。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。是目前最流行的跨域认证解决方案。

  • Compact(紧凑): 由于它们尺寸较小,JWT可以通过URL,POST参数或HTTP标头内发送。 另外,尺寸越小意味着传输速度越快。
  • Self-contained(自包含): 有效载荷(Playload)包含有关用户的所有必需信息,避免了多次查询数据库。

1.3.2 应用场景

  • Authentication(鉴权): 这是使用JWT最常见的情况。 一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。 单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,并且能够轻松地跨不同域使用。
  • 分布式站点的单点登录(SSO)
  • Information Exchange(信息交换): JSON Web Tokens是在各方之间安全传输信息的好方式。 因为JWT可以签名:例如使用公钥/私钥对,所以可以确定发件人是他们自称的人。 此外,由于使用标头和有效载荷计算签名,因此您还可以验证内容是否未被篡改。

1.3.3 语法

jwt有3个组成部分,每部分通过点号来分割 header.payload.signature

  • 头部(header) 是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子
  • 载荷(payload) 是一个 JSON 对象,用来存放实际需要传递的数据
  • 签证(signature) 对header和payload使用密钥进行签名,防止数据篡改。

① 头部header

Jwt的头部是一个JSON,然后使用Base64URL编码,承载两部分信息:

  • 声明类型typ,表示这个令牌(token)的类型(type),JWT令牌统一写为JWT
  • 声明加密的算法alg,通常直接使用HMACSHA256,就是HS256了,也可以使用RSA,支持很多算法(HS256、HS384、HS512、RS256、RS384、RS512、ES256、ES384、ES512、PS256、PS384)

var header = Base64URL({ "alg": "HS256", "typ": "JWT"})

Base64URL:Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。

② 载荷payload

payload也是一个JSON字符串,是承载消息具体内容的地方,也需要使用Base64URL编码,payload中可以包含预定义的7个可用,它们不是强制性的,但推荐使用,也可以添加任意自定义的key

  1. iss(issuer): jwt签发者
  2. sub(subject): jwt所面向的用户
  3. aud(audience): 接收jwt的一方, 受众
  4. exp(expiration time): jwt的过期时间,这个过期时间必须要大于签发时间
  5. nbf(Not Before): 生效时间,定义在什么时间之前.
  6. iat(Issued At): jwt的签发时间
  7. jti(JWT ID): jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

// 该token签发给1234567890,姓名为John Doe(自定义的字段),签发时间为1516239022

var payload = Base64URL( {"sub": "1234567890", "name": "John Doe", "iat": 1516239022})

注意,JWT中payload是不加密的,只是Base64URL编码一下,任何人拿到都可以进行解码,所以不要把敏感信息放到里面。

③ signature

Signature 部分是对前两部分的签名,防止数据篡改。

var header = Base64URL({ "alg": "HS256", "typ": "JWT"});
var payload = Base64URL( {"sub": "", "name": "John Doe", "iat": });
var secret = "私钥";
var signature = HMACSHA256(header + "." + payload, secret);
var jwt = header + "." + payload + "." + signature;

我们可以使用jwt.io调试器来解码,验证和生成JWT:

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

1.3.4 jwt的特点

  • 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
  • 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
  • 它不需要在服务端保存会话信息, 所以它易于应用的扩展

JWT 的几个特点

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将敏感数据(如密码)写入 JWT,除非对payload进行加密。保护好secret私钥,该私钥非常重要。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

1.3.5 JWT的优点:

  • 体积小,因而传输速度更快
  • 多样化的传输方式,可以通过URL传输、POST传输、请求头Header传输(常用)
  • 简单方便,服务端拿到jwt后无需再次查询数据库校验token可用性,也无需进行redis缓存校验
  • 在分布式系统中,很好地解决了单点登录问题
  • 很方便的解决了跨域授权问题,因为跨域无法共享cookie

1.3.6 JWT的缺点:

  • 因为JWT是无状态的,因此服务端无法控制已经生成的Token失效,是不可控的,这一点对于是否使用jwt是需要重点考量的
  • 获取到jwt也就拥有了登录权限,因此jwt是不可泄露的,网站最好使用https,防止中间攻击偷取jwt
  • 在退出登录 / 修改密码时怎样实现JWT Token失效https://segmentfault.com/q/1010000010043871

1.3.7 JWT安全性:

JWT被确实存在被窃取的问题,但是如果能得到别人的token,其实也就相当于能窃取别人的密码,这其实已经不是JWT安全性的问题。网络是存在多种不安全性的,对于传统的session登录的方式,如果别人能窃取登录后的sessionID,也就能模拟登录状态,这和JWT是类似的。为了安全,https加密非常有必要,对于JWT有效时间最好设置短一点。

1.3.8 JWT常见问题

① JWT 安全吗?

Base64编码方式是可逆的,也就是透过编码后发放的Token内容是可以被解析的。一般而言,不建议在有效载荷内放敏感信息,比如使用者的密码。

② JWT Payload 內容可以被伪造吗?

JWT其中的一个组成内容为Signature,可以防止通过Base64可逆方法回推有效载荷内容并将其修改。因为Signature是经由Header跟Payload一起Base64组成的。

③ 如果我的 Cookie 被窃取了,那不就表示第三方可以做 CSRF 攻击?

是的,Cookie丢失,就表示身份就可以被伪造。故官方建议的使用方式是存放在LocalStorage中,并放在请求头中发送。

④ 空间及长度问题?

JWT Token通常长度不会太小,特别是Stateless JWT Token,把所有的数据都编在Token里,很快的就会超过Cookie的大小(4K)或者是URL长度限制。

⑤ Token失效问题?

无状态JWT令牌(Stateless JWT Token)发放出去之后,不能通过服务器端让令牌失效,必须等到过期时间过才会失去效用。

假设在这之间Token被拦截,或者有权限管理身份的差异造成授权Scope修改,都不能阻止发出去的Token失效并要求使用者重新请求新的Token。

1.3.9 JWT使用建议

  • Payload中的exp时效不要设定太长。
  • 开启Only Http预防XSS攻击。
  • 如果担心重播攻击(replay attacks )可以增加jti(JWT ID),exp(有效时间) Claim。
  • 在你的应用程序应用层中增加黑名单机制,必要的时候可以进行Block做阻挡(这是针对掉令牌被第三方使用窃取的手动防御)。

二:io.jsonwebtoken.jjwt

jwt使用流程

一般是在请求头里加入Authorization,并加上Bearer标注:

// Authorization: Bearer <token>
getToken('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})

io.jsonwebtoken是最常用的工具包。

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.</version>
</dependency>

application.properties

jwt.secret=JO6HN3NGIU25G2FIG8V7VD6CK9B6T2Z5
jwt.expire=
JwtToken类
@Configuration
public class JwtToken {
private static Logger logger = LoggerFactory.getLogger(JwtToken.class);
/** 秘钥 */
@Value("${jwt.secret}")
private String secret;
/** 过期时间(秒) */
@Value("${jwt.expire}")
private long expire;
/**
* 生成jwt token
*/
public String generateToken(Long userId) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expire * );
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId + "")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
String[] header = token.split("Bearer");
token = header[];
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
logger.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public static boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
// Getter && Setter
}
JwtController
@RestController
public class JwtController {
@Autowired
private JwtToken jwtToken;
@PostMapping("/login")
public String login(User user) {
// 1. 验证用户名和密码
// 2. 验证成功生成token
Long userId = 666L;
String token = jwtToken.generateToken(userId);
return token;
}
@GetMapping("/getUserInfo")
public String getUserInfo(@RequestHeader("Authorization") String authHeader) throws AuthenticationException {
// 黑名单token
List<String> blacklistToken = Arrays.asList("禁止访问的token");
Claims claims = jwtToken.getClaimByToken(authHeader);
if (claims == null || JwtToken.isTokenExpired(claims.getExpiration()) || blacklistToken.contains(authHeader)) {
throw new AuthenticationException("token 不可用");
}
String userId = claims.getSubject();
// 根据用户id获取接口数据返回接口
return userId;
}
}
@Configuration
public class JwtToken {
private static Logger logger = LoggerFactory.getLogger(JwtToken.class);
/** 秘钥 */
@Value("${jwt.secret}")
private String secret;
/** 过期时间(秒) */
@Value("${jwt.expire}")
private long expire;
/**
* 生成jwt token
*/
public String generateToken(Long userId) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expire * );
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId + "")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
String[] header = token.split("Bearer");
token = header[];
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
logger.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public static boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
// Getter && Setter
}
JwtController
@RestController
public class JwtController {
@Autowired
private JwtToken jwtToken;
@PostMapping("/login")
public String login(User user) {
// 1. 验证用户名和密码
// 2. 验证成功生成token
Long userId = 666L;
String token = jwtToken.generateToken(userId);
return token;
}
@GetMapping("/getUserInfo")
public String getUserInfo(@RequestHeader("Authorization") String authHeader) throws AuthenticationException {
// 黑名单token
List<String> blacklistToken = Arrays.asList("禁止访问的token");
Claims claims = jwtToken.getClaimByToken(authHeader);
if (claims == null || JwtToken.isTokenExpired(claims.getExpiration()) || blacklistToken.contains(authHeader)) {
throw new AuthenticationException("token 不可用");
}
String userId = claims.getSubject();
// 根据用户id获取接口数据返回接口
return userId;
}
}

Spring Boot集成JSON Web Token(JWT)的更多相关文章

  1. JSON WEB Token(JWT)

    最近面试被问及单点登陆怎么解决?自己的项目前后端分离,自己实现token认证,token有失效时间,token中包含用户基本的信息.且一个当用户重新登陆后,原来的token就会失效,这么安全的一个to ...

  2. JSON Web Token (JWT) 简介

    JSON Web Token (JWT) 是一种基于 token 的认证方案. JSON Web Token 的结构 一个 JWT token 看起来是这样的: eyJhbGciOiJIUzI1NiI ...

  3. JSON Web Token (JWT) 实现与使用方法

    1. JSON Web Token是什么 JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的.自包含的方式,用于作为JSON对象在各方之间安全地传输信息.该 ...

  4. Json Web Token(JWT)详解

    什么是Json Web Token Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的 ...

  5. JSON Web Token (JWT),服务端信息传输安全解决方案。

    JWT介绍 JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑独立的基于JSON对象在各方之间安全地传输信息的方式.这些信息可以被验证和信任,因为它是数字签名的 ...

  6. 漫谈JSON Web Token(JWT)

    一.背景 传统的单体应用基于cookie-session的身份验证流程一般是这样的: 用户向服务器发送账户和密码. 服务器验证账号密码成功后,相关数据(用户角色.登录时间等)都保存到当前会话中. 服务 ...

  7. Python Tornado集成JSON Web Token方式登录

    本项目github地址 前端测试模板如下: Tornado restful api 项目 项目结构如下: 项目组织类似于django,由独立的app模块构成. 登录接口设计 模式:post -> ...

  8. 基于 Token 的身份验证:JSON Web Token(JWT)

    1.传统身份验证和JWT的身份验证 传统身份验证:       HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用.这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过 ...

  9. JSON Web Token (JWT)生成Token及解密实战。

    昨天讲解了JWT的介绍.应用场景.优点及注意事项等,今天来个JWT具体的使用实践吧. 从JWT官网支持的类库来看,jjwt是Java支持的算法中最全的,推荐使用,网址如下. https://githu ...

随机推荐

  1. 一个简单的MyBatis项目

    1.log4j.properties,我们把它设为debug级别,以便于调试.生产环境可以设为INFO,本项目放在src下面: # Global logging configuration log4j ...

  2. Spring 梳理-Spring配置文件 -<context:annotation-config/>和<context:component-scan base-package=""/>和<mvc:annotation-driven /> 的区别

    <context:annotation-config/> 在基于主机方式配置Spring时,Spring配置文件applicationContext.xml,你可能会见<contex ...

  3. Docker系列(二):通过Docker安装使用 Kubernetes (K8s)

    Docker社区版从17.12版本开始已经提供了对Kubernetes的支持.但是由于其安装过程依赖的镜像服务在国内访问很不稳定,很多朋友都无法配置成功.我们提供了一个简单的工具帮助大家开启Docke ...

  4. JavaScript实现各种排序算法

    前言:本文主要是用JavaScript实现数据结构中的各种排序算法,例如:插入排序.希尔排序.合并排序等. 冒泡排序 function bubbleSort(arr) { console.time(& ...

  5. Kafka 学习笔记之 Consumer API

    Kafka提供了两种Consumer API High Level Consumer API Low Level Consumer API(Kafka诡异的称之为Simple Consumer API ...

  6. Golang的反射reflect深入理解和示例

    编程语言中反射的概念 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制.也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examin ...

  7. 报错fatal: refusing to merge unrelated histories

    提交到远程仓库的时候报错如下 是因为远程仓库有东西更新,但本地仓库没有更新造成提交失败 需要先把远程仓库给拉取下来,执行命令git pull origin master,又报错了如下 是因为两个仓库提 ...

  8. Android OkHttp + Retrofit 下载文件与进度监听

    本文链接 下载文件是一个比较常见的需求.给定一个url,我们可以使用URLConnection下载文件. 使用OkHttp也可以通过流来下载文件. 给OkHttp中添加拦截器,即可实现下载进度的监听功 ...

  9. Redis实现分布式文件夹锁

    缘起 最近做一个项目,类似某度云盘,另外附加定制功能,本人负责云盘相关功能实现,这个项目跟云盘不同的是,以项目为分配权限的单位,同一个项目及子目录所有有权限的用户可以同时操作所有文件,这样就很容易出现 ...

  10. 子网掩码!如何划分子网掩码,计算IP地址

    作者:chli1806 一.子网掩码的含义和根据子网掩码划分子网一个IP地址必然属于某一个网络,或者叫子网.子网掩码就是用来指定某个IP地址的网络地址的,换一句话说,就是用来划分子网的.例如,一个A类 ...