一、jwt介绍

1、什么是jwt

jwt全称JSON WEB TOKEN 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

2、jwt流程

#用户使用用户名密码来请求服务器
#服务器进行验证用户的信息
#服务器通过验证发送给用户一个token
#客户端存储token,并在每次请求时附送上这个token值
#服务端验证token值,并返回数据

3、jwt组成

第一部分头部(header),第二部分为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

二、应用jwt

1、导入jwt依赖
包含两个依赖 io.jsonwebtoken + com.auth0
gradle

implementation 'io.jsonwebtoken:jjwt:0.9.1'

implementation 'com.auth0:java-jwt:3.4.0'

2、创建注解
可以通过配置注解来进行权限校验

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserLoginToken {
    boolean required() default true;
}
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PassToken {
    boolean required() default true;
}

3、创建jwt工具类
利用这个工具类来生成、解密及校验jwt

/**
 * jwt工具类 生成、解析、校验token
 */
public class JwtUtil {

    /**
     * 用户登录成功后生成jwt
     * 使用Hs256算法
     * 三部分组成 头部 + 荷载 + 签证信息
     * @param ttlMillis jwt过期时间
     * @param user      登录成功的user对象
     * @return
     */
    public static String createJwt(long ttlMillis, User user){
        // header部分,jwt已经封装好了
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // jwt生成时间 当前时间
        long nowMillis = System.currentTimeMillis();
        Date date = new Date(nowMillis);

        // payload 荷载部分(存放有效信息的地方,包含标准中注册的声明、公共声明、私有声明)
        // 创建私有声明
        Map<String,Object> claims = new HashMap<>();
        claims.put("id", user.getUserId());
        claims.put("username",user.getUserName());
        claims.put("password",user.getUserPassword());

        // 生成秘钥secret用
//        String key = user.getUserPassword();
        byte[] bytes = user.getUserPassword().getBytes();

        // 生成签发人
        String subject = user.getUserName();

        // 为payload添加标准声明和私有声明(new一个JwtBuilder,设置jwt的body)
        JwtBuilder jwtBuilder = Jwts.builder()
                // 先设置自己创建的私有声明,要是写在标准声明后面,会覆盖掉标准声明
                .setClaims(claims)
                // 设置jti(jwt id),主要用来作为一次性token,从而回避重放攻击
                .setId(UUID.randomUUID().toString())
                // 设置iat jwt签发时间
                .setIssuedAt(date)
                // 设置jwt的所有人
                .setSubject(subject)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, bytes);

        // 设置jwt的过期时间
        if(ttlMillis>= 0){
            long expMillis = ttlMillis+nowMillis;
            Date expDate = new Date(expMillis);
            jwtBuilder.setExpiration(expDate);
        }

        return jwtBuilder.compact();
    }

    /**
     * 解密jwt
     * @param token 需要被解密的token
     * @param user 用户的对象
     * @return
     */
    public static Claims parseJWT(String token,User user){
        // 签名秘钥(与生成签名的秘钥一样)
        String key = user.getUserPassword();

        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(key)
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

    /**
     * 校验jwt
     * 判断token携带的密码跟数据库里的是否一致(也可用官方的校验方法)
     * @param token
     * @param user
     * @return
     */
    public static Boolean isVerify(String token,User user){
        // 秘钥
        byte[] bytes = user.getUserPassword().getBytes();

        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(bytes)
                // 设置需要解析的jwt
                .parseClaimsJws(token)
                .getBody();

        // 判断密码是否一致
        if(claims.get("password").equals(user.getUserPassword())){
            return true;
        }

        return false;
    }
}

4、创建拦截器
配合拦截器使用,拦截前端传往后台的请求,用于生成jwt或者校验携带的jwt

public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private UserLoginService userLoginService;
    /*
    controller执行之前
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object){

        // 从http请求头中取出token
        String token = request.getHeader("token");

        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();

        // 方法是否带有UserLoginToken注释
        if(method.isAnnotationPresent(UserLoginToken.class)){
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if(userLoginToken.required()){
                return true;
            }
        }

        // 方法是否带有PassToken注释
        if(method.isAnnotationPresent(PassToken.class)){
            PassToken passToken = method.getDeclaredAnnotation(PassToken.class);
            if(passToken.required()){
                // 执行认证
                if(token == null){
                    throw new RuntimeException("无token,请重新登录!");
                }
                // 获取token中的userId
                String userId;
                try {
                    userId = JWT.decode(token).getClaim("id").asString();
                } catch (JWTDecodeException j){
                    throw new RuntimeException("访问异常!");
                }
                User user = userLoginService.findUserById(userId);
                if(StringUtils.isEmpty(user)){
                    throw new RuntimeException("当前用户不存在,请重新登录!");
                }
                Boolean verify = JwtUtil.isVerify(token, user);
                if(!verify){
                    throw new RuntimeException("非法访问!");
                }
                return true;
            }
        }
        return true;
    }
    /*
    controller执行后,页面渲染前
     */
    @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 {

    }
}

5、配置拦截器
这里介绍springboot项目配置拦截器的方式

/**
 * jwt 后台访问拦截
 * 拦截器配置文件 config
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
    // 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册TestInterceptor拦截器
        InterceptorRegistration registration = registry.addInterceptor(authenticationInterceptor());
        registration.addPathPatterns("/**");//所有路径都被拦截
        registration.excludePathPatterns("" +
                "/assets/**",             // assets文件夹里文件不拦截
                "/**/*.js",              //js静态资源不拦截
                "/**/*.css"             //css静态资源不拦截
        );
    }
}

6、使用注解
在登录方法上添加@UserLoginToken 注解,检测到该注解不进行拦截,同时生成jwt
在其他方法上添加@PassToken 注解,拦截器拦截检测jwt