Spring Boot 集成 JWT

空扰寡人 提交于 2020-01-21 16:27:07
  • JWT (JSON Web Token)介绍, 官网github

    • 定义: JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于在各方之间作为JSON对象安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。JWTS可以使用秘密(使用HMAC算法)或公钥/私钥对使用RSA或ECDSA来签名。

    • JWT的主要应用场景

      • 身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。
      • 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
    • JWT由Header,Payload及Signature 3个子字符串组成

      格式为:Header.Payload.Signature
      例如:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtyp...oLL62SY
      Header : {"alg": "HS256"}
      Payload : {"sub": "Joe"}
      Signature : 对eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ 进行加密,形成一个新的字符串
      
      • Header是由以下这个格式的Json通过Base64编码(编码不是加密,是可以通过反编码的方式获取到这个原来的Json,所以JWT中存放的一般是不敏感的信息)生成的字符串,Header中存放的内容是说明编码对象是一个JWT以及使用“SHA-256”的算法进行加密(加密用于生成Signature)

      • Claim是一个Json,Claim中存放的内容是JWT自身的标准属性,所有的标准属性都是可选的,可以自行添加,比如:JWT的签发者、JWT的接收者、JWT的持续时间等;同时Claim中也可以存放一些自定义的属性,这个自定义的属性就是在用户认证中用于标明用户身份的一个属性,比如用户存放在数据库中的id,为了安全起见,一般不会将用户名及密码这类敏感的信息存放在Claim中。将Claim通过Base64转码之后生成的一串字符串称作Payload。

        {
        "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 : 是由Header和Payload组合而成,将Header和Claim这两个Json分别使用Base64方式进行编码,生成字符串Header和Payload,然后将Header和Payload以Header.Payload的格式组合在一起形成一个字符串,然后使用上面定义好的加密算法和一个密匙(这个密匙存放在服务器上,用于进行验证)对这个字符串进行加密,形成一个新的字符串,这个字符串就是Signature

    • JWT实现认证的原理
      服务器在生成一个JWT之后会将这个JWT会以Authorization : Bearer JWT 键值对的形式存放在cookies里面发送到客户端机器,在客户端再次访问收到JWT保护的资源URL链接的时候,服务器会获取到cookies中存放的JWT信息,首先将Header进行反编码获取到加密的算法,在通过存放在服务器上的密匙对Header.Payload 这个字符串进行加密,比对JWT中的Signature和实际加密出来的结果是否一致,如果一致那么说明该JWT是合法有效的,认证成功,否则认证失败

  • 登录和认证流程

  • 集成 JWT

    • 从官网上找到 Java 语言的 JWT 使用教程,我们这里使用标注有:maven: com.auth0 / java-jwt / 3.3.0 的版本 , maven: io.jsonwebtoken / jjwt / 0.9.0 这个版本比较老了

    • 添加 pom 配置 , github

      <dependency>
          <groupId>com.auth0</groupId>
          <artifactId>java-jwt</artifactId>
          <version>3.9.0</version>
      </dependency>
      
    • 定义注解

      // 在Controller的方法上使用此注解,该方法在映射时会检查用户是否登录,未登录返回401错误
      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Authorization {
          boolean required() default true;
      }
      
    • 定义 JwtUtils

      public class JwtUtils {
          // 生产 Token
          public static String getToken(User user){
              String token = "";
              try {
                  Algorithm algorithm = Algorithm.HMAC256("secret");
                  //long expire = 7 * 24 * 60 * 60 * 1000; // 7天后过期
                  Calendar calendar = Calendar.getInstance();
                  calendar.add(Calendar.MINUTE, 10);  // 10分钟后过期
      
                  token = JWT.create()
                          .withIssuer("jwtdemo")
                          .withClaim("userId", user.getUserId())
                          .withAudience(user.getUsername())
                          .withSubject(user.getUsername())
                          .withExpiresAt(calendar.getTime())
                          .sign(algorithm);
              } catch (JWTCreationException exception){
                  //Invalid Signing configuration / Couldn't convert Claims.
              }
              return token;
          }
      
          // 验证 token
          public static boolean checkToken(String token){
              
              JWTVerifier jwtVerifier = JWT.require(
                      Algorithm.HMAC256("secret")
              ).build();
      
              try {
                  jwtVerifier.verify(token);
                  return true;
              } catch (JWTVerificationException e) {
                  throw new RuntimeException("401 verify error");
              } finally {
                  return true;
              }
          }
      }
      
    • 定义拦截器

      @Configuration
      public class AuthenticationConfig implements WebMvcConfigurer {
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              // 拦截所有请求,通过判断是否有 @Authorization 注解, 决定是否需要登录;
              // 排除 swagger,alipay , wxpay
              registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**")
                .excludePathPatterns("/alipay")
                .excludePathPatterns("/wxpay/wxpay_notify");
          }
          @Bean
          public AuthenticationInterceptor authenticationInterceptor() {
              return new AuthenticationInterceptor();
          }
      }
      
      public class AuthenticationInterceptor implements HandlerInterceptor {
          @Autowired
          UserService userService;
          @Override
          public boolean preHandle(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   Object handler) throws Exception {
              // 如果不是映射到方法直接通过
              if(!(handler instanceof HandlerMethod)){
                return true;  // true表示继续流程
              }
      
              HandlerMethod handlerMethod=(HandlerMethod)handler;
              Method method=handlerMethod.getMethod();
      
              //检查是否有需要用户权限的注解
              if (method.isAnnotationPresent(Authorization.class)) {
                  Authorization auth = method.getAnnotation(Authorization.class);
                  if (auth.required()) {
                      // 从 http 请求头中取出 token
                      String token = request.getHeader("Authorization");
                      if (token == null) {
                          throw new RuntimeException("无token,请重新登录");
                      }
      
                      // 获取 token 中的 user id , user name, 验证user是否存在
                      try {
                          Long userId = JWT.decode(token).getClaim("userId").asLong();
                          String userName = JWT.decode(token).getAudience().get(0);
                          // User user = userService.findUserById(userId);
                      } catch (JWTDecodeException j) {
                          throw new RuntimeException("401 decode error");
                      }
      								// 验证 token , token 是否过期
                      JwtUtils.checkToken(token);
                  }
             }
         }
      
          @Override
          public void postHandle(HttpServletRequest request, 
                                 HttpServletResponse response, 
                                 Object handler, ModelAndView modelAndView) {
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, 
                                      HttpServletResponse response, 
                                      Object handler, Exception ex) {
          }
      }
      
    • 添加登录接口

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class User {
          Long userId;
          String username;
          String password;
          String token 
      }
      
      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class ResultModel<T> implements java.io.Serializable {
          private int status;
          private String message;
          private T data;
      }
      
      @RestController
      @RequestMapping("api")
      public class UserController {
          @PostMapping("/login")
          public Object login(@RequestBody User user){
              ResultModel<User> model = new ResultModel<>();
              if(user ==null){
                  model.setStatus(400);
                  model.setMessage("登录失败,用户不存在");
                  return model;
              } else {
                  if (!user.getPassword().equals("123456")){
                      model.setStatus(500);
                      model.setMessage("登录失败,密码错误");
                      return model;
                  } else {
                      String token = JwtUtils.getToken(user);
                      user.setToken(token);
                      model.setData(user);
                      model.setStatus(200);
                      return model;
                  }
              }
          }
        
          @Authorization
          @GetMapping("/getMessage")
          public String getMessage(){
              return "你已通过验证";
          }
      }
      
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!