初识JWT
1.什么是JWT
JSON Web Token (JWT) 是一个开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,可以在客户端与服务器之间作为JSON对象安全地传输信息。
2.JWT使用场景
- 身份验证: 用户在登录以后,后续的每个请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源等。Session同样也可以实现这个功能,但是在使用Session的同时也会相应的增加服务器的压力;而JWT的开销则相对较小,因为其将存储的压力分布到各个客户端中,从而减轻了服务器的压力,并且能够在不同域的系统当中轻松的使用。单点登录(SSO)就广泛使用了JWT的功能。
- 信息交换: JWT能够在客户端与服务器之间安全地传输信息,因为其可以签名,通过签名可以验证传输信息是否被修改。
3.JWT组成
JWT就是一个字符串,经过加密处理与校验处理的字符串,由 .
分割的三个部分组成,分别是头(Header)、有效荷载(Playload)、签名(Signature),因此JWT的格式通常也是这样: header.playload.signature
(header由JWT的表头信息经过加密后得到;playload由JWT用到的身份验证信息JSON数据加密得到;signature是由header和playload加密得到,这一部分作为校验部分)。
- Header
通常是由两部分组成的:一是令牌的类型,即JWT
;二是哈希算法,比如SHA256
。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后这个JSON通过Base64
加密形成JWT的第一个部分即header
。
- Playload
JWT的第二个部分是有效荷载,其中包含了声明(Claim)。JWT提供了一组预定义的声明,这些声明都是可选的,并不是强制性的。当然你也可以自定义声明传输所需信息,比如系统用户ID。出于安全考虑,一般不会将用户的敏感信息存放在声明当中。
声明属性 | 说明 |
---|---|
iss | 发行人,JWT由谁签发 |
iat | JWT创建时间,unix时间戳格式 |
exp | JWT过期时间,unix时间戳格式 |
sub | JWT所面向的用户 |
aud | 接收方,接收JWT的一方 |
nbf | 当前时间在nbf之前,JWT不能被接收处理 |
jti | JWT唯一ID |
例如:
{
"iss": "Hilox",
"sub": "HiloxApiUser",
"iat": "1542337107",
"exp": "1542340707",
"userId": "5"
}
将上述声明(Claim)通过Base64
加密后得到payload
。
- Signature
将表头经过Base64
加密得到的header
和Claim
经过Base64
加密得到的playload
进行组合,形成一个新字符串header.playload
,对新形成的字符串使用标头当中指定的算法(例如:上述Header例子中使用HS256
算法)和自定义的密钥(例如:Hilox
)进行加密得到signature
。
最后,将字符串组合 header.playload.signature
就是生成的token了。
图1 JWT生成流程图
图1 JWT生成流程图
JWT应用
1.JWT如何使用
博主为移动端app搭建服务器,所采用的方式是将token放到http请求的请求头部当中,通常使用的是Authorization
属性字段。
移动端app使用cookie不太方便,所以暂不做考虑。
2.应用流程
图2 初次登录生成JWT流程图
图2 初次登录生成JWT流程图
图3 用户访问资源流程图
图3 用户访问资源流程图
JWT应用代码实现
下面通过代码来实现用户认证的功能,博主这里主要采用Spring Boot与JWT整合的方式实现。
关于Spring Boot项目如何搭建与使用本章不做详细介绍。
代码当中针对异常自行做处理,我这里偷点懒直接用日志在控制台打印。
1.添加JWT依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2.添加JWT相关配置
jwt:
# 发行者
name: Hilox
# 密钥, 经过Base64加密, 可自行替换
base64Secret: SGlsb3g=
#jwt中过期时间设置(分)
jwtExpires: 120
3.JWT配置实体类
/**
* jwt 相关参数
* Created by Hilox on 2018/11/16 0016.
*/
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtParam {
/**
* 发行者名
*/
private String name;
/**
* base64加密密钥
*/
private String base64Secret;
/**
* jwt中过期时间设置(分)
*/
private int jwtExpires;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBase64Secret() {
return base64Secret;
}
public void setBase64Secret(String base64Secret) {
this.base64Secret = base64Secret;
}
public int getJwtExpires() {
return jwtExpires;
}
public void setJwtExpires(int jwtExpires) {
this.jwtExpires = jwtExpires;
}
}
4.配置JWT拦截器
/**
* jwt 拦截器
* Created by Hilox on 2018/11/16 0016.
*/
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtParam jwtParam;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 忽略带JwtIgnore注解的请求, 不做后续token认证校验
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
if (jwtIgnore != null) {
return true;
}
}
if (HttpMethod.OPTIONS.equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
final String authHeader = request.getHeader(JwtConstant.AUTH_HEADER_KEY);
if (StringUtils.isEmpty(authHeader)) {
// TODO 这里自行抛出异常
log.info("===== 用户未登录, 请先登录 =====");
return false;
}
// 校验头格式校验
if (!JwtUtils.validate(authHeader)) {
// TODO 这里自行抛出异常
log.info("===== token格式异常 =====");
return false;
}
// token解析
final String authToken = JwtUtils.getRawToken(authHeader);
Claims claims = JwtUtils.parseToken(authToken, jwtParam.getBase64Secret());
if (claims == null) {
log.info("===== token解析异常 =====");
return false;
}
// 传递所需信息
request.setAttribute("CLAIMS", claims);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
}
}
5.配置MVC拦截器
/**
* mvc 配置
* Created by Hilox on 2018/11/15 0015.
*/
@Configuration
public class MyWebConfigurer extends WebMvcConfigurerAdapter {
// 这里这么做是为了提前加载, 防止过滤器中@AutoWired注入为空
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
// 自定义过滤规则
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**");
}
}
6.自定义忽略token验证注解
/**
* JWT请求忽略注解
* Created by Hilox on 2018/11/20 0020.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JwtIgnore {
}
7.JWT工具类
/**
* JWT工具类
* Created by Hilox on 2018/11/16 0016.
*/
@Slf4j
public class JwtUtils {
private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
// 构造私有
private JwtUtils() {}
/**
* 获取原始token信息
* @param authorizationHeader 授权头部信息
* @return
*/
public static String getRawToken(String authorizationHeader) {
return authorizationHeader.substring(AUTHORIZATION_HEADER_PREFIX.length());
}
/**
* 获取授权头部信息
* @param rawToken token信息
* @return
*/
public static String getAuthorizationHeader(String rawToken) {
return AUTHORIZATION_HEADER_PREFIX + rawToken;
}
/**
* 校验授权头部信息格式合法性
* @param authorizationHeader 授权头部信息
* @return
*/
public static boolean validate(String authorizationHeader) {
return StringUtils.hasText(authorizationHeader)
&& authorizationHeader.startsWith(AUTHORIZATION_HEADER_PREFIX);
}
/**
* 生成token, 只在用户登录成功以后调用
* @param userId 用户id
* @param jwtParam JWT加密所需信息
* @return
*/
public static String createToken(String userId, JwtParam jwtParam) {
return createToken(userId, null, jwtParam);
}
/**
* 生成token, 只在用户登录成功以后调用
* @param userId 用户id
* @param claim 声明
* @param jwtParam JWT加密所需信息
* @return
*/
public static String createToken(String userId, Map<String, Object> claim, JwtParam jwtParam) {
try {
// 使用HS256加密算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 生成签名密钥
byte[] apiKeySecretBytes =
DatatypeConverter.parseBase64Binary(jwtParam.getBase64Secret());
SecretKeySpec signingKey =
new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// 添加构成JWT的参数
JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
.claim(JwtConstant.USER_ID_KEY, userId)
.addClaims(claim)
.setIssuer(jwtParam.getName())
.setIssuedAt(now)
.signWith(signatureAlgorithm, signingKey);
// 添加token过期时间
long TTLMillis = jwtParam.getJwtExpires() * 60 * 1000;
if (TTLMillis >= 0) {
long expMillis = nowMillis + TTLMillis;
Date exp = new Date(expMillis);
jwtBuilder.setExpiration(exp).setNotBefore(now);
}
return jwtBuilder.compact();
} catch (Exception e) {
// TODO 这里自行抛出异常
log.error("签名失败", e);
return null;
}
}
/**
* 解析token
* @param authToken 授权头部信息
* @param base64Secret base64加密密钥
* @return
*/
public static Claims parseToken(String authToken, String base64Secret) {
try{
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(base64Secret))
.parseClaimsJws(authToken).getBody();
return claims;
} catch (SignatureException se) {
// TODO 这里自行抛出异常
log.error("===== 密钥不匹配 =====", se);
} catch (ExpiredJwtException ejw) {
// TODO 这里自行抛出异常
log.error("===== token过期 =====", ejw);
} catch (Exception e){
// TODO 这里自行抛出异常
log.error("===== token解析异常 =====", e);
}
return null;
}
}
8.编写登录验证Controller
/**
* 登录验证Controller
* Created by Hilox on 2018/11/16 0016.
*/
@Slf4j
@RestController
public class LoginController {
@Autowired
private JwtParam jwtParam;
// 登录
@PostMapping("/login")
@JwtIgnore // 加此注解, 请求不做token验证
public String login() {
// 1.用户密码验证我这里忽略, 假设用户验证成功, 取得用户id为5
Integer userId = 5;
// 2.验证通过生成token
String token = JwtUtils.createToken(userId + "", jwtParam);
if (token == null) {
log.error("===== 用户签名失败 =====");
return null;
}
log.info("===== 用户{}生成签名{} =====", userId, token);
return JwtUtils.getAuthorizationHeader(token);
}
// 验证
@PostMapping("/hilox")
public String hilox() {
return "Hello World!";
}
}
源码传送门
【源码地址】:springboot-jwt
JWT代码测试效果
启动以上项目,博主这里使用工具Postman
来模拟http请求。
1.未登录情况请求测试接口/hilox
图4 未登录情况请求测试接口效果图
图4 未登录情况请求测试接口效果图
2.请求登录接口/login
图5 请求登录接口效果图
图5 请求登录接口效果图
3.登录情况请求测试接口/hilox
这里我们需要将请求登录接口时返回的token放入请求头的Authorization
当中。
图6 登录情况请求测试接口效果图
图6 登录情况请求测试接口效果图