一、介绍
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
1、头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2、载荷(playload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
(1)标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。
(2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}
然后将其进行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
3、签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
二、实践(简单实现调用后台接口需要token验证)
1、引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
2、自定义注解
/**
* @author yang
* @date 2021年11月19日 11:09
* 这个注解的作用是用来pass掉token 的 也就是不需要token验证的需求
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
/**
* @author yang
* @date 2021年11月19日 11:10
* 这个注解时候用来使用token 的
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
3、创建获取Token的工具类
/**
* @author yang
* @date 2021年11月19日 11:12
*/
public class TokenUtil {
/**
* 生成token 的方法
* @param user
* @return
*/
public static String getToken(SysUser user){
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + 24*60*60*1000);
String token = "";
token = JWT.create().withAudience(user.getUserId().toString())
.withExpiresAt(expireDate)
.sign(Algorithm.HMAC256(user.getPassword()));
return token;
}
}
Algorithm.HMAC256()
:使用HS256
生成token
,密钥则是用户的密码,唯一密钥的话可以保存在服务端。withAudience()
存入需要保存在token
的信息,这里我把用户ID
存入token
中
4、创建拦截器,来拦截相应的请求
/**
* @author yang
* @date 2021年11月19日 11:19
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
SysUserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//进入方法之前进行的操作
//获取token
String token = request.getHeader("token");
//如果不是映射到方法直接通过
if(!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if(method.isAnnotationPresent(PassToken.class))
{
PassToken passToken = method.getAnnotation(PassToken.class);
if(passToken.required()){
return true;
}
}
String userId = null;
if(method.isAnnotationPresent(UserLoginToken.class))
{
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if(userLoginToken.required()){
if(token == null){
throw new RuntimeException("无token,请重新登录");
}
//获取token的userid
try{
userId = JWT.decode(token).getAudience().get(0);
}
catch (JWTDecodeException e){
throw new RuntimeException("token已过期,请重新请求");
}
SysUser user = userService.getUser(Long.parseLong(userId));
if(user==null){
throw new RuntimeException("用户不存在");
}
//验证token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try{
jwtVerifier.verify(token);
}catch (JWTVerificationException e){
throw new RuntimeException("token已过期,请重新请求");
}
return true;
}
}
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 {
//渲染视图之后进行的操作
}
}
method.isAnnotationPresent(PassToken.class) 这里就看到我们的注解的作用了,利用的是java 的反射机制,看是否有这样的注解,有进就行相关的操作
这里我们的token 是在请求头中被传过来的。
主要流程
1.从 http 请求头中取出 token,
2.判断是否映射到方法
3.检查是否有passtoken注释,有则跳过认证
4.检查有没有需要用户登录的注解,有则需要取出并验证
5.认证通过则可以访问,不通过会报相关错误信息
5、配置springmvc 的拦截器
/**
* @author yang
* @date 2021年11月19日 11:22
*/
@Configuration
public class WebMvcConfigration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/rest/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor(){
return new AuthenticationInterceptor();
}
}
这里我拦截的是/rest/下的所以请求,大家根据自己实际的请求路径进行修改。这里突然想到一个坑,有的博客会去用@EnableWebMvc这个注解,当你使用这个注解时候,会取消到springboot 的默认配置,所以请谨慎使用。
6、mvc阶段(主要是Controller,其他就不介绍了)
/**
* @author yang
* @date 2021年11月19日 11:27
*/
@RequestMapping("/rest")
@RestController
public class UserController extends BaseController {
@Autowired
private SysUserService sysUserService;
@PassToken
@PostMapping(value = "/login2")
public Object login(SysUser sysUser ){
//用户登录
BooleanResult booleanResult = sysUserService.login(sysUser.getLoginName(),sysUser.getPassword());
SysUser user = (SysUser) booleanResult.getObject();
String token = TokenUtil.getToken(user);
return token;
}
@UserLoginToken
@RequestMapping("/getmessage")
public String getmessage(){
return "你已经通过验证";
}
}
这里应该有两个方法,login 使用来登录验证的 加上注解 就不需要token 字段,getmessage 使用来模拟我们那些需要token的方法,加上注解后,就需要token
三、测试
1、我们先使用Postman调下/rest/getmessage接口,此时无token
2、接着调用/rest/login2接口生成token
3、再次调/rest/getmessage,这次在请求的Headers中带入刚生成的token
好,结束,收工,最后附上工程目录及请求图示