目录
JWT
概述
JWT
是什么
JWT
全称 JSON Web Token
,是为了在网络应用环境间传递声明而执行的一种基于 JSON
的开放标准
它将用户信息加密到 Token
里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证 Token
的正确性,只要正确即通过验证
JWT
详细讲解请见 github
:https://github.com/jwtk/jjwt
为什么使用 JWT
随着技术的发展,分布式 web
应用的普及,通过 session
管理用户登录状态成本越来越高,因此慢慢发展成为 Token
的方式做登录身份校验,然后通过 Token
去取 redis
中的缓存的用户信息,随着之后 JWT
的出现,校验方式更加简单便捷化,无需通过 redis
缓存,而是直接根据 Token
取出保存的用户信息,以及对 Token
可用性校验,单点登录更为简单
传统的 Cookie
,Session
与 JWT
对比
传统 Cookie
和 Session
- 服务端需要存储
Session
,由于Session
经常需要快速查找,通常将其存储在内存或内存数据库中,当同时在线用户较多时会占用大量的服务器资源 - 在分布式架构下,需要考虑在多个节点间同步
Session
数据 - 由于客户端使用
Cookie
存储SessionID
,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范CSRF
攻击 - 不支持
Android,IOS
,小程序等移动端
JWT
JWT
需要服务端生成Token
,客户端保存这个Token
,每次请求携带这个Token
即可,服务端认证解析即可JWT
方式校验方式更加简单便捷化,无需通过redis
缓存,而是直接根据Token
取出保存的用户信息,以及对token
可用性校验,单点登录,验证token
更为简单
JWT
的组成
JWT
由 3
部分组成。第 1
部分为头部(Header
),第 2
部分我们称其为载荷(Payload
),第 3
部分是签证(Signature
)。结合 JWT
的格式即:Header.Payload.Signature
Header
Header
是由以下这个格式的 Json
通过 Base64
编码(编码不是加密,是可以通过反编码的方式获取到这个原来的 Json
,所以 JWT
中存放的一般是不敏感的信息)生成的字符串,Header
中存放的内容是说明编码对象是一个 JWT
以及使用 SHA-256
的算法进行加密(加密用于生成 Signature
)
{
"typ":"JWT",
"alg":"HS256"
}
Claim
:
Claim
是描述 Json
的信息的一个 Json
,将 Claim
转码之后生成 Payload
**
Claim
是一个 Json
,Claim
中存放的内容是 JWT
自身的标准属性,所有的标准属性都是可选的,可以自行添加,比如:JWT
的签发者、JWT
的接收者、JWT
的持续时间等;同时 Claim
中也可以存放一些自定义的属性,这个自定义的属性就是在用户认证中用于标明用户身份的一个属性,比如用户存放在数据库中的 id
,为了安全起见,一般不会将用户名及密码这类敏感的信息存放在 Claim
中
{
"iss":"Issuer —— 用于说明该JWT是由谁签发的",
"sub":"Subject —— 用于说明该JWT面向的对象",
"aud":"Audience —— 用于说明该JWT发送给的用户",
"exp":"Expiration Time —— 数字类型,说明该JWT过期的时间",
"nbf":"Not Before —— 数字类型,说明在该时间之前JWT不能被接受与处理",
"iat":"Issued At —— 数字类型,说明该JWT何时被签发",
"jti":"JWT ID —— 说明标明JWT的唯一ID",
"user-definde1":"自定义属性举例",
"user-definde2":"自定义属性举例"
}
Signature
Signature
是由 Header
和 Payload
组合而成,将 Header
和 Claim
这两个 Json
分别使用 Base64
方 式进行编码,生成字符串 Header
和 Payload
,然后将 Header
和 Payload
以 Header.Payload
的格式组合在一起形成一个字符串,然后使用上面定义好的加密算法和一个密匙(这个密匙存放在服务器上,用于进行验证)对这个字符串进行加密,形成一个新的字符串,这个字符串就是 Signature
JWT
实现用户认证的流程
JWT
优缺点
优点
- 可扩展性好:应用程序分布式部署的情况下,
Session
需要做多机数据共享,通常可以存在数据库或者Redis
里面。而JWT
不需要 - 无状态:
JWT
不在服务端存储任何状态。RESTful API
的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT
的载荷中可以存储一些常用信息,用于交换信息,有效地使用JWT
,可以降低服务器查询数据库的次数
缺点
- 安全性:由于
JWT
的payload
是使用Base64
编码的,并没有加密,因此JWT
中不能存储敏感数据。而Session
的信息是存在服务端的,相对来说更安全 - 性能:
JWT
太长。由于是无状态使用JWT
,所有的数据都被放到JWT
里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致JWT
非常长,Cookie
的限制大小一般是4k
,cookie
很可能放不下,所以JWT
一般放在LocalStorage
里面。并且用户在系统中的每一次Http
请求都会把JWT
携带在Header
里面,Http
请求的Header
可能比Body
还要大。而SessionId
只是很短的一个字符串,因此使用JWT
的Http
请求比使用Session
的开销大得多 - 一次性:无状态是
JWT
的特点,但也导致了这个问题,JWT
是一次性的。想修改里面的内容,就必须签发一个新的JWT
。即缺陷是一旦下发,服务后台无法拒绝携带该JWT
的请求(如踢除用户)
而正是 JWT
是一次性的,所以也产生了以下问题
- 无法废弃:通过
JWT
的验证机制可以看出来,一旦签发一个JWT
,在到期之前就会始终有效,无法中途废弃。例如你在payload
中存储了一些信息,当信息需要更新时,则重新签发一个JWT
,但是由于旧的JWT
还没过期,拿着这个旧的JWT
依旧可以登录,那登录后服务端从JWT
中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT
,那么旧的就加入黑名单(比如存到redis
里面),避免被再次使用 - 续签:如果你使用
JWT
做会话管理,传统的Cookie
续签方案一般都是框架自带的,Session
有效期30
分钟,30
分钟内如果有访问,有效期被刷新至30
分钟。一样的道理,要改变JWT
的有效时间,就要签发新的JWT
。最简单的一种方式是每次请求刷新JWT
,即每个HTTP
请求都返回一个新的JWT
。这个方法不仅暴力不优雅,而且每次请求都要做JWT
的加密解密,会带来性能问题。另一种方法是在Redis
中单独为每个JWT
设置过期时间,每次访问时刷新JWT
的过期时间
可以看出想要破解 JWT
一次性的特性,就需要在服务端存储 JWT
的状态。但是引入 redis
之后,就把无状态的 JWT
硬生生变成了有状态了,违背了 JWT
的初衷。而且这个方案和 Session
都差不多了
SpringBoot
方式整合 JWT
示例
Maven
依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
JwtTokenUtil
工具类
public class JwtTokenUtil {
private static final Logger log = LoggerFactory.getLogger(JwtTokenUtil.class);
private static String SECRET_KEY = "78944878877848fg)";
private static long ACCESS_TOKEN_EXPIRETIME = 24 * 60 * 60 * 1000;// 1:设置1小时过期
private static long REFRESH_TOKEN_EXPIRETIME = 72 * 60 * 60 * 1000;// 2:设置24小时过期
private static String ISSUER = "yingxue.com";
// 生成 access_token
public static String getAccessToken(String subject, Map<String, Object> claims) {
return generateToken(ISSUER, subject, claims, ACCESS_TOKEN_EXPIRETIME, SECRET_KEY);
}
// 生成 refresh_token,用于 JWT 续签
public static String getRefreshToken(String subject, Map<String, Object> claims) {
return generateToken(ISSUER, subject, claims, REFRESH_TOKEN_EXPIRETIME, SECRET_KEY);
}
/**
* 签发token
* @param issuer:签发人
* @param subject:代表这个JWT的主体,即它的所有人 一般是用户id
* @param claims:存储在JWT里面的信息 一般放些用户的权限/角色信息
* @param ttlMillis:有效时间(毫秒)
* @param secret
*/
public static String generateToken(String issuer, String subject, Map<String, Object> claims, long ttlMillis,
String secret) {
// HS256算法加密
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] signingKey = DatatypeConverter.parseBase64Binary(secret);
JwtBuilder builder = Jwts.builder();
if (null != claims) {
builder.setClaims(claims);
}
if (!StringUtils.isEmpty(subject)) {
builder.setSubject(subject);
}
if (!StringUtils.isEmpty(issuer)) {
builder.setIssuer(issuer);
}
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);// 计算当前时间毫秒数
builder.setIssuedAt(now);
if (ttlMillis >= 0) {
// 过期的毫秒数 = 当前时间毫秒数 + 配置文件设置的过期毫秒数
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);// 设置过期时间
}
builder.signWith(signatureAlgorithm, signingKey);
return builder.compact();
}
// 获取用户 id
public static String getUserId(String token) {
String userId = null;
try {
Claims claims = getClaimsFromToken(token);
userId = claims.getSubject();
} catch (Exception e) {
log.error("------>获取用户id失败<------");
}
return userId;
}
// 获取用户名
public static String getUserName(String token) {
String username = null;
try {
Claims claims = getClaimsFromToken(token);
username = (String) claims.get(Constant.JWT_USER_NAME);
} catch (Exception e) {
log.error("------>获取用户名失败<------");
}
return username;
}
// 从令牌中获取数据声明
public static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY)).parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
// 校验令牌
public static Boolean validateToken(String token) {
Claims claimsFromToken = getClaimsFromToken(token);
return (null != claimsFromToken && !isTokenExpired(token));
}
// 验证 token 是否过期
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
// 当当前的时间在 expiration 时间之前时,返回 false,也就是 token 还未过期
boolean before = expiration.before(new Date());
log.info("------>before的值为:" + before + "<------");
return before;
} catch (Exception e) {
log.error("------>验证token结果为:已经过期<------");
return true;
}
}
// 刷新 token
public static String refreshToken(String refreshToken, Map<String, Object> claims) {
String refreshedToken;
try {
Claims parserclaims = getClaimsFromToken(refreshToken);
// 刷新token的时候如果为空说明原先的 用户信息不变 所以就引用上个token里的内容
if (null == claims) {
claims = parserclaims;
}
refreshedToken = generateToken(parserclaims.getIssuer(), parserclaims.getSubject(), claims,
ACCESS_TOKEN_EXPIRETIME, SECRET_KEY);
} catch (Exception e) {
refreshedToken = null;
log.error("------>刷新token出现错误<------");
}
return refreshedToken;
}
// 获取 token 的剩余过期时间
public static long getRemainingTime(String token) {
long result = 0;
try {
long nowMillis = System.currentTimeMillis();
result = getClaimsFromToken(token).getExpiration().getTime() - nowMillis;
} catch (Exception e) {
log.error("------>获取token的剩余过期时间失败<------", e);
}
return result;
}
}
springboot
配置拦截器
可以配置一些需要用户进行鉴权验证的接口,从而对这些接口进行请求拦截
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
// 注册一个拦截器到 spring 中
@Override
public void addInterceptors(@NotNull InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor()) // 添加拦截器
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/error/login") // 不拦截的请求
.excludePathPatterns("/error")
.excludePathPatterns("/static/**")// 排除静态资源
.excludePathPatterns("/user/publicKey")// 获取公钥接口
.excludePathPatterns("/user/register")// 用户注册页面
.excludePathPatterns("/user/userRegister")// 用户注册
.excludePathPatterns("/user/doLogin"); // 用户登录
}
@Bean
public MyInterceptor myInterceptor() {
return new MyInterceptor();
}
@Override
public void addResourceHandlers(@NotNull ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}
自定义的拦截器
从拦截的 Http
请求的头部 Header
中获取 JWT
,进行校验。成功则放行请求,失败则提示用户
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
// 注册一个拦截器到 spring 中
@Override
public void addInterceptors(@NotNull InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor()) // 添加拦截器
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/error/login") // 不拦截的请求
.excludePathPatterns("/error")
.excludePathPatterns("/static/**")// 排除静态资源
.excludePathPatterns("/user/publicKey")// 获取公钥接口
.excludePathPatterns("/user/register")// 用户注册页面
.excludePathPatterns("/user/userRegister")// 用户注册
.excludePathPatterns("/user/doLogin"); // 用户登录
}
@Bean
public MyInterceptor myInterceptor() {
return new MyInterceptor();
}
@Override
public void addResourceHandlers(@NotNull ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}
用户登录 Service
分别获取 access_token
和 refresh_token
并返回给客户端,返回refresh_token
是为了 JWT
的续签
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public LoginResponse userDoLogin(@NotNull UserDTO userDTO) {
LoginResponse loginResponse = new LoginResponse();
// 使用PasswordEncoder工具类解析加密密码,拿到明文密码
String password = PasswordEncoder.decryptPassword(userDTO.getPassword());
String emailRegExp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$";
// 判断用户用的是登录名还是邮箱,进行登录
if (userDTO.getUserName().matches(emailRegExp)) {// 邮箱登录
User emailUser = userRepository.findByEmail(userDTO.getEmail());
if (StringUtils.isEmpty(emailUser)) {
// 该用户不存在,请先注册
throw new BusinessException(ResponseCode.USER_NOT_EXIST);
}
String decodePassword = DesUtil.getInstance(Constant.DES_KEY).getDecodeString(emailUser.getPassword());
if (!password.equals(decodePassword)) {
// 密码错误
throw new BusinessException(ResponseCode.PASSWORD_ERROR);
}
if (emailUser.getIsActive() == Constant.USER_STATUS_ERROR) {
// 该用户已被锁定,请联系运营人员
throw new BusinessException(ResponseCode.USER_IS_ACTIVE);
}
// 构建该方法的返回数据
Map<String, Object> claimsMap = new HashMap<String, Object>();
claimsMap.put(Constant.JWT_USER_ID, emailUser.getId());
// 分别获取 access_token 和 refresh_token 返回给客户端
String access_token = JwtTokenUtil.getAccessToken(emailUser.getId().toString(), claimsMap);
String refresh_token = JwtTokenUtil.getRefreshToken(emailUser.getId().toString(), claimsMap);
loginResponse.setAccessToken(access_token);
loginResponse.setRefreshToken(refresh_token);
loginResponse.setId(emailUser.getId());
return loginResponse;
} else {// 登录名登录
User loginUsername = userRepository.findByUserName(userDTO.getUserName());
if (StringUtils.isEmpty(loginUsername)) {
// 该用户不存在,请先注册
throw new BusinessException(ResponseCode.USER_NOT_EXIST);
}
String decodePassword = DesUtil.getInstance(Constant.DES_KEY).getDecodeString(loginUsername.getPassword());
if (!password.equals(decodePassword)) {
// 密码错误
throw new BusinessException(ResponseCode.PASSWORD_ERROR);
}
if (loginUsername.getIsActive() == Constant.USER_STATUS_ERROR) {
// 该用户已被锁定,请联系运营人员
throw new BusinessException(ResponseCode.USER_IS_ACTIVE);
}
// 构建该方法的返回数据
Map<String, Object> claimsMap = new HashMap<String, Object>();
claimsMap.put(Constant.JWT_USER_ID, loginUsername.getId());
// 分别获取 access_token 和 refresh_token 返回给客户端
String access_token = JwtTokenUtil.getAccessToken(loginUsername.getId().toString(), claimsMap);
String refresh_token = JwtTokenUtil.getRefreshToken(loginUsername.getId().toString(), claimsMap);
loginResponse.setAccessToken(access_token);
loginResponse.setRefreshToken(refresh_token);
loginResponse.setId(loginUsername.getId());
return loginResponse;
}
}
}
用户登录 Controller
@Controller
@RequestMapping(path = { "/user" })
public class UserController {
@Autowired
private UserService userService;
// 用户登录
@RequestMapping(path = { "/doLogin" }, method = { RequestMethod.POST })
@ResponseBody
public Map<String, Object> doLogin(UserDTO userDTO) {
LoginResponse loginResponse = userService.userDoLogin(userDTO);
return new ResponseMap().success().message("登录成功").data(loginResponse);
}
}
用户登录页面
将服务器返回的 access_token
和 refresh_token
保存到 localStorage
中,在之后的每一次用户 Http
请求的 Header
中需要携带 access_token
和 refresh_token
,完成用户的鉴权验证
$.ajax({// 用户登录
type : 'post',
url : '/user/doLogin',
dataType : 'json',
data : ({
'userName' : username,
'password' : rsa_password
}),
success : function(resp) {
if(resp.code !== 200) {
layer.msg(resp.message,function(){});
} else if(resp.code == 200) {
layer.msg('登录成功');
// 将获取到的 access_token 和 refresh_token 保存到 localStorage 中
localStorage.setItem('authorization', resp.data.accessToken);
localStorage.setItem('refresh_token', resp.data.accessToken);
var id = localStorage.getItem('itemId');
window.location.href = 'http://127.0.0.1:8080/itemKill/info?id=' + id;
}
},
});
参考:https://blog.csdn.net/AkiraNicky/article/details/99307713
原文链接:https://blog.csdn.net/weixin_38192427/article/details/115154600