Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的, 特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息, 以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLnlKjmiLciLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlzcyI6IuetvuWPkeiAhSIsImlkIjoic2xtMTIzIiwiZXhwIjoxNTU1MDQ5NzI1LCJ1c2VybmFtZSI6InNsbSJ9.x5ZoTW5NS4wBCIK61v4YCGi8bsveifBwnsMNBQz8s_s
JWT共有三部分组成,第一部分称之为头部(header),第二部分称之为荷载(payload),第三部分是签名(signature)。
header:
{
'typ': 'JWT',//声明类型,这里是jwt
'alg': 'HS256'
}
header经过Base64加密后:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload:
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"sub": "123456",
"name": "admin",
"admin": true
}
Payload经过Base64加密后:
eyJzdWIiOiLnlKjmiLciLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlzcyI6IuetvuWPkeiAhSIsImlkIjoic2xtMTIzIiwiZXhwIjoxNTU1MDQ5NzI1LCJ1c2VybmFtZSI6InNsbSJ9
JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
Signature是对前两部分进行的签名防止数据被篡改
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
怎么使用JWT?
一般是在请求头里加入Authorization,并加上Bearer标注:
headers: {
'Authorization': 'Bearer ' + token
}
JWT如何应用在Springboot中?
首先引入pom依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
实现WebMvcConfigurer接口配置拦截所有URl
/**
* @Author: slm
* @CreateTime: 2019-04-11 10:07
* @Description: 拦截设置
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
我在项目中使用了全局异常处理和加入了两个自定义注解
创建AuthenticationInterceptor实现HandlerInterceptor接口对请求进行拦截
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken userLoginToken = method.getAnnotation(LoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new AppException("9999","无token,请重新登录");
}
// 获取 token 中的 user id
Boolean userId;
try {
userId = JWTUtil.verifyToken(token,"123456");
} catch (JWTDecodeException j) {
throw new AppException("401","无权操作");
}
if(userId){
return true;
}else {
throw new AppException("401","无权操作");
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
配置完成以后简单模拟一下用户数据:
创建JWTUtil类对token进行生成和校验
@Slf4j
public class JWTUtil {
private static final String EXP = "exp";
private static final String PAYLOAD = "payload";
/**
* 加密生成token
*
* @param object 载体信息
* @param maxAge 有效时长
* @param secret 服务器私钥
* @param <T>
* @return
*/
public static String createToken(User object, long maxAge, String secret) {
try {
final Algorithm signer = Algorithm.HMAC256(secret);//生成签名
String token = JWT.create()
.withIssuer("签发者")
.withSubject("用户")//主题,科目
.withClaim("username", object.getUsername())
.withClaim("id", object.getId())
.withClaim("password",object.getPassword())
.withExpiresAt(new Date(System.currentTimeMillis() + maxAge))
.sign(signer);
System.out.println(token);
return Base64.getEncoder().encodeToString(token.getBytes("utf-8"));
} catch (Exception e) {
log.error("生成token异常:", e);
return null;
}
}
/**
* 解析验证token
*
* @param token 加密后的token字符串
* @param secret 服务器私钥
* @return
*/
public static Boolean verifyToken(String token, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(new String(Base64.getDecoder().decode(token),"utf-8"));
return true;
} catch (IllegalArgumentException e) {
throw new AppException("9999",e.getMessage());
} catch (JWTVerificationException e) {
throw new AppException("9999",e.getMessage());
} catch (UnsupportedEncodingException e) {
throw new AppException("9999",e.getMessage());
}
}
}
创建接口调用接口
注意:登录接口加入了PassToken表示不进行校验
最后启动项目,调用登录接口
调用getMsg进行验证
登录成功