Springboot简单功能示例-5 使用JWT进行授权认证

发布时间 2023-09-18 20:02:12作者: 超级修理工

springboot-sample

介绍

  springboot简单示例-使用JWT进行授权认证 跳转到发行版 查看发行版说明

软件架构(当前发行版)

  • Springboot3.1.3
  • hutool
  • bcprov-jdk18on

安装教程

git clone --branch 自定义加密进行登录验证 git@gitee.com:simen_net/springboot-sample.git

主要功能

使用JWT认证

    • WebSecurityConfig.java中注册JwtAuthenticationSuccessHandler.javaJwtAuthenticationFailureHandler.java验证处理器

      // 注册验证成功处理器
      httpSecurityFormLoginConfigurer.successHandler(authenticationSuccessHandler);
      // 注册验证失败处理器
      httpSecurityFormLoginConfigurer.failureHandler(authenticationFailureHandler);
       
    • WebSecurityConfig.java中加入异常处理器JwtAuthenticationEntryPoint

      // 加入异常处理器
      httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
              httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(authenticationEntryPoint)
      );
       
    • WebSecurityConfig.java中强制session无效

      // 强制session无效,使用jwt认证时建议禁用,正常登录不能禁用session
      httpSecurity.sessionManagement(httpSecuritySessionManagementConfigurer->
              httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
      );
       

代码逻辑说明

    1. JwtUserDetails.java中增加private Map<String, Object> mapProperties,用于保存登录用户的扩展信息,录入用户分组、用户单位等等

    2. JwtUserDetailsService.java中模拟注入用户权限及扩展信息

      listGrantedAuthority.add(new SimpleGrantedAuthority("file_write"));
      mapProperties.put("扩展属性", username + " file_write");
      log.info("读取到已有用户[{}],默认密码123456,file_write权限,扩展属性:[{}]", username, mapProperties);
      
      return new JwtUserDetails(username, SM2_OBJ.signHex("123456", SecurityUtils.STR_UUID), listGrantedAuthority, mapProperties);
       
    3. SecurityUtils.java中定义全局常量Map<String, String> MAP_LOGIN_SUCCESS,用于保存用户登录时的token必,可以在服务器通过简单的匹配防止伪造签名,如不需要可以取消

    4. 登录成功处理器JwtAuthenticationSuccessHandler.java,返回包含jwt编码的标准化json。其中使用Sm2JwtSigner.java进行签名和校验

      @Override
      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
          if (!response.isCommitted() && authentication != null && authentication.getPrincipal() != null
                  // 获取登录用户信息对象
                  && authentication.getPrincipal() instanceof JwtUserDetails userDetails) {
      
              // 获取30分钟有效的token编码
              String strToken = jwtTokenUtils.getToken30Minute(
                      userDetails.getUsername(),
                      CollUtil.join(userDetails.getAuthorities(), ","),
                      userDetails.getMapProperties()
              );
      
              // 在全局登录成功的map中放入当前用户登录的token
              MAP_LOGIN_SUCCESS.put(userDetails.getUsername(), strToken);
      
              // 包装返回的JWT对象
              ReplyVO<JwtResponseData> replyVO = new ReplyVO<>(
                      new JwtResponseData(strToken, DateUtil.date()), "用户登录成功");
      
              // 将返回字符串写入response
              SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK, replyVO);
          }
      }
       
    5. 登录失败处理器JwtAuthenticationFailureHandler,根据抛出的异常返回对应的json

      @Override
      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
          String strData = LOGIN_ERROR_UNKNOWN;
          String strMessage = "LOGIN_ERROR_UNKNOWN";
      
          if (exception instanceof LockedException) {
              strData = LOGIN_ERROR_ACCOUNT_LOCKING;
              strMessage = exception.getMessage();
          } else if (exception instanceof CredentialsExpiredException) {
              strData = LOGIN_ERROR_PASSWORD_EXPIRED;
              strMessage = exception.getMessage();
          } else if (exception instanceof AccountExpiredException) {
              strData = LOGIN_ERROR_OVERDUE_ACCOUNT;
              strMessage = exception.getMessage();
          } else if (exception instanceof DisabledException) {
              strData = LOGIN_ERROR_ACCOUNT_BANNED;
              strMessage = exception.getMessage();
          } else if (exception instanceof BadCredentialsException) {
              strData = LOGIN_ERROR_USER_CREDENTIAL_EXCEPTION;
              strMessage = exception.getMessage();
          } else if (exception instanceof UsernameNotFoundException) {
              strData = LOGIN_ERROR_USER_NAME_NOT_FOUND;
              strMessage = exception.getMessage();
          }
      
          // exception.printStackTrace();
          SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK,
                  new ReplyVO<>(strData, strMessage, ReplyEnum.ERROR_USER_HAS_NO_PERMISSIONS.getCode()));
      }
       
    6. 异常处理器JwtAuthenticationEntryPoint中根据request头Accept判断请求类型是html还是json,html请求跳转到登录页面,json请求返回异常接送代码

      @Override
      public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
          // 从request头中获取Accept
          String strAccept = request.getHeader("Accept");
          if (StrUtil.isNotBlank(strAccept)) {
              // 对Accept分组为字符串数组
              String[] strsAccept = StrUtil.splitToArray(strAccept, ",");
              // 判断Accept数组中是否存在"text/html"
              if (ArrayUtil.contains(strsAccept, "text/html")) {
                  // 存在"text/html",判断为html访问,则跳转到登录界面
                  response.sendRedirect(STR_URL_LOGIN_URL);
              } else {
                  // 不存在"text/html",判断为json访问,则返回未授权的json
                  SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK,
                          new ReplyVO<>("未授权或登录已超时", ReplyEnum.ERROR_TOKEN_EXPIRED));
              }
          }
      }
       
    7. 登出成功处理器JwtLogoutSuccessHandler

      jwtTokenUtils.verifyToken(strJwtToken);
      // 从token中获取用户名
      String strUserName = jwtTokenUtils.getAudience(strJwtToken);
      // 断言用户名非空
      Assert.notBlank(strUserName, "当前用户不存在");
      // 从全局登录信息Map中移除该用户信息
      MAP_LOGIN_SUCCESS.remove(strUserName);
      
      log.info("[{}]登出成功", strUserName);
       
    8. Sm2JwtSigner.java签名和校验时,将headerBase64payloadBase64使用STR_JWT_SIGN_SPLIT组合成字符串进行签名和校验

      /**
       * 返回签名的Base64代码
       *
       * @param headerBase64  JWT头的JSON字符串的Base64表示
       * @param payloadBase64 JWT载荷的JSON字符串Base64表示
       * @return 签名结果Base64,即JWT的第三部分
       */
      @Override
      public String sign(String headerBase64, String payloadBase64) {
          StringBuilder sbContent = new StringBuilder();
          sbContent.append(headerBase64).append(STR_JWT_SIGN_SPLIT).append(payloadBase64);
          return Base64Encoder.encode(SM2_OBJ.sign(StrUtil.utf8Bytes(sbContent)));
      }
      
      /**
       * 验签
       *
       * @param headerBase64  JWT头的JSON字符串Base64表示
       * @param payloadBase64 JWT载荷的JSON字符串Base64表示
       * @param signBase64    被验证的签名Base64表示
       * @return 签名是否一致
       */
      @Override
      public boolean verify(String headerBase64, String payloadBase64, String signBase64) {
          StringBuilder sbContent = new StringBuilder();
          sbContent.append(headerBase64).append(STR_JWT_SIGN_SPLIT).append(payloadBase64);
          return SM2_OBJ.verify(StrUtil.utf8Bytes(sbContent), Base64Decoder.decode(signBase64));
      }
       
    9. 生成的JWT代码和解密内容

      • JWT Tokens 编码

        eyJ0eXAiOiJKV1QiLCJhbGciOiLlm73lr4ZTTTLpnZ7lr7nnp7Dnrpfms5XvvIzln7rkuo5CQ-W6kyJ9.eyJhdWQiOlsic2ltZW4iXSwiaWF0IjoxNjk1MDIwMzUzLCJleHAiOjE2OTUwMzgzNTMsIlVTRVJfQVVUSE9SSVRZIjoiZmlsZV9yZWFkIiwiTUFQX1VTRVJfUFJPUEVSVElFUyI6eyLmianlsZXlsZ7mgKciOiJzaW1lbiBmaWxlX3JlYWQifX0.MEQCIBr7QHoMdgqt53AM+hlVJfDfSrj8Pdi+dAJ9hg3QMBQuAiAhcFbV26ESehhylWewr467GNWncKruz86NfD68CU105Q==
         
      • Decode 解码后HEADER

        {
            "typ": "JWT",
            "alg": "国密SM2非对称算法,基于BC库"
        }
         
      • Decode 解码后PAYLOAD

        {
           "aud": [
              "simen"
           ],
           "iat": 1695020353,
           "exp": 1695038353,
           "USER_AUTHORITY": "file_read",
           "MAP_USER_PROPERTIES": {
              "扩展属性": "simen file_read"
           }
        }