第24章 Spring Boot RESTful API安全防护:JWT认证实战

在上一篇文章【# Spring Boot新手指南:一文学会RESTful API开发】中,我们深入探讨了如何使用Spring MVC构建RESTful API,并且了解了多种注解的应用,如@RestController、@RequestMapping、@GetMapping等,它们共同帮助我们设计出了既简洁又高效的API接口。然而,在实际开发过程中,除了关注API的功能实现外,维护一份清晰、准确的API文档同样至关重要。这不仅能提升团队内部的协作效率,还能极大地便利外部用户的接入与使用。本文将带你深入探索如何在Spring Boot应用中使用JWT(JSON Web Token)来保障RESTful API的安全性。我们将从为什么API安全性至关重要讲起,继而详细介绍JWT的工作原理及其在Spring Boot项目中的具体实现。通过本教程,你将学会如何为你的API添加强大的身份验证和授权机制,确保数据的安全传输,防止恶意攻击,并提高系统的整体健壮性。无论是初学者还是有一定经验的开发者,都能从中受益,构建出既高效又安全的Web应用。

知识回顾

为何API安全性很重要?

在设计和部署RESTful API时,有如下三个核心缘由能够解释为何安全性应该是一个很重要的考虑因素。安全

1.数据保护

RESTful API是一种向外界传输价值的服务方式。所以,保护经过RESTful方式提供的数据始终应该是属于高优先级。服务器

2.DOS攻击

若是不采起正确的安全措施,(DOS)攻击可使RESTful API进入非功能状态。考虑到不少基础的RESTful API是开放给全部人使用的状况,经过这种相似开源的方式有助于它更好推广给市场,让更多人投入使用,但同时意味着若是有人选择对API执行DOS攻击时,它也可能致使灾难性的结果。restful

3.商业影响

现在有越来多的服务平台,提供着影响衣食住行的各类信息,从飞机航班时刻表到高铁余票查询,甚至只是超市里平常用品,都能给你提供价格、数量、时间等诸多信息,让你足不出户,买到最合心意的商品。在这样的大趋势下,这种利用API数据来获取更多信息,再提供给你的聚合服务平台将会愈来愈多。因而经过RESTful API传输的信息会被频繁调用,而其中的我的信息很容易被泄露。

一、如何保证接口安全?

保证RESTful API的安全性,主要包括三大方面:

a) 对客户端做身份认证

b) 对敏感的数据做加密,并且防止篡改

c) 身份认证之后的授权

恶意提交攻击

1.拦截且修改:客户端发送到服务端接口的请求被第三方拦截,然后修改数据,再提交给客户端执行

例如:

  1. 客户端发送了将username重置为"小明"的请求
  2. 第三方拦截后修改为username重置为"小红"
  3. 然后提交给服务器执行
  4. 服务器认证了用户身份后(cookie或token都是客户端发出的,因此能通过认证)
  5. 那么客户端的username就被修改为"小红"

2.拦截不修改,重复提交攻击

例如:

  1. 客户端发送一个更新user表的请求
  2. 第三方拦截
  3. 然后短时间内复制重复提交
  4. 服务器认证后不断往数据库写数据

任务描述

任务要求

使用IDEA开发工具构建一个项目多模块工程。study-springboot-chapter19学习关于Springboot开发服务接口如果保证访问安全

  1. 基于study-springboot工程,复制study-springboot-chapter19标准项目,坐标groupId(com.cbitedu)、artifactId(study-springboot-chapter19),其他默认
  2. 继承study-springboot工程依赖
  3. 构建Restful API jwt安全解决方案

任务收获

  1. Spring Boot开发Restful API服务,通过JWT保证服务安全
  2. 学会使用JUnit完成单元测试
  3. 掌握web开发原理
  4. 熟悉SpringMVC的基础配置

任务准备

环境要求

  1. JDK1.8+
  2. MySQL8.0.27+
  3. Maven 3.6.1+
  4. IDEA/VSCode

工程目录要求

study-springboot-chapter19

数据准备

sql 复制代码
-- ----------------------------
-- Table structure for jjwt
-- ----------------------------
DROP TABLE IF EXISTS `jjwt`;
CREATE TABLE `jjwt` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- ----------------------------
-- Records of jjwt
-- ----------------------------
BEGIN;
INSERT INTO `jjwt` (`id`, `username`, `password`) VALUES (1, 'admin', '123456');
COMMIT;

任务实施

JWT可以理解为一个加密的字符串,里面由三部分组成:头部 (Header)、负载 (Payload)、签名(signature)

由base64加密后的header和payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT字符串

环境搭建

第一步引入第三方依赖:主要是jjwt和knife4j

xml 复制代码
   <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.0.0</version>
        </dependency>       
    <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.plugin</groupId>
                    <artifactId>spring-plugin-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    <dependency>
        <groupId>org.springframework.plugin</groupId>
        <artifactId>spring-plugin-core</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>

第二步:jwt辅助类:JWTUtils

typescript 复制代码
package com.cbitedu.springboot.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

public class JWTUtils {
    public  static  String SIGN = "ASDFse@#w";
    /**
     * 生成Token
     * @param map 用户信息(非敏感信息)
     * @return
     */
    public static String createToken(Map<String,String> payload){
        JWTCreator.Builder builder = JWT.create();
        //遍历map 存放到payload
        payload.forEach((k,v)->{
            builder.withClaim(k,v);
        });

        HashMap<String, Object> header = new HashMap<>();
        header.put("alg", "HS256");  // 默认值就是这两个
        header.put("typ", "JWT");
        builder.withHeader(header);

        Calendar cr = Calendar.getInstance();
        cr.add(Calendar.SECOND,100);    //  过期时间 100秒
        System.out.println(cr.getTime());
        builder.withExpiresAt(cr.getTime());                // 设置有效时间(根据业务需求这是有效时间)
        return builder.sign(Algorithm.HMAC256(SIGN));     // 加签(配置私钥,防止字符串被篡改)
    }

    /**
     * token 验签
     * token 有效负荷读取
     * @param token
     */
    public static DecodedJWT verifyToken(String token){
        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);//创建验证对象
    }
}

第三步:swagger3和token拦截器

kotlin 复制代码
package com.cbitedu.springboot.config;

import io.swagger.annotations.Api;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
@EnableOpenApi
public class Swagger3Config {
    @Bean
    public Docket apiConfig(){
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo())
                .select()
                //设置通过什么方式定位到需要生成文档的接口
                //定位了方法上的ApiOperation
                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                //接口URL路径,any表示全部路径
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("API安全服务项目")
                .description("项目描述信息")
                .contact(new Contact("creatorblue","http://www.creatorblue.com","zhujianwu@creatorblue.com"))
                .version("1.0")
                .build();
    }
}
java 复制代码
package com.cbitedu.springboot.config;

import com.cbitedu.springboot.intercepter.JWTinterepter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTinterepter())
                .addPathPatterns("/user/verity")
                .excludePathPatterns("/user/login");
    }
}
java 复制代码
package com.cbitedu.springboot.intercepter;

import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.cbitedu.springboot.util.JWTUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

public class JWTinterepter implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String,Object> map = new HashMap<>();

        //获取请求头中的令牌
        String token = request.getHeader("token");
        System.out.println("token:"+token);
        try {
            JWTUtils.verifyToken(token);
            return true;
        } catch (TokenExpiredException tokenExpiredException){
            map.put("msg","token过期");
        }catch (AlgorithmMismatchException algorithmMismatchException){
            map.put("msg","token算法不一致");
        }catch (Exception e){
            map.put("msg","token无效");
        }
        map.put("state",false);

        String s = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charest=UTF-8");
        response.getWriter().println(s);
        return false;
    }
}

第四步:控制器:UserController

typescript 复制代码
package com.cbitedu.springboot.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.cbitedu.springboot.entity.User;
import com.cbitedu.springboot.service.UserService;
import com.cbitedu.springboot.util.JWTUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import java.util.HashMap;
import java.util.Map;


/**
 * <p>说明: API应用KEYAPI接口层</P>
 */
@RestController
@Slf4j
@Api(tags = {"用户管理"})//用在类上,表示对类的说明tags表示说明该类的作用
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping("/")
    public ModelAndView index(User user) {
        return new ModelAndView("login");

    }
    @PostMapping("/user/login")
    @ApiOperation(value = "显示用户数据")
    public Map<String, Object> login(@RequestParam(value="username", required=true) String username,@RequestParam(value="password", required=false) String password) {
        log.info("用户名:" + username);
        log.info("密码:" + password);
        Map<String, Object> map = new HashMap<>();
        try {
            User userDB = userService.getOne(new QueryWrapper<User>().eq("username", username));
            if (userDB != null) {
                map.put("msg", "登录成功");
                map.put("code", "200");
                Map<String, String> payload = new HashMap<>();
                payload.put("name", username);
                String token = JWTUtils.createToken(payload);
                map.put("token", token);
            }
        } catch (Exception ex) {
            map.put("msg", "登录失败");
        }

        return map;
    }

    @GetMapping("/user/verity")
    public Map<String, String> verityToken(String token) {
        Map<String, String> map = new HashMap<>();
        log.info("token===="+token);
        try{
            JWTUtils.verifyToken(token);
            map.put("msg", "验证成功");
            map.put("state", "true");
        }
      catch (Exception exception){
            map.put("msg","验证失败");
            exception.printStackTrace();
      }
        return map;
    }
}

四、测试API文档访问地址:http://localhost:81/doc.html

实验实训

1、认真学习Knife4j:doc.xiaominfo.com/docs/quick-...

2、了解国产API管理平台ApiFox:www.apifox.cn/

相关推荐
2402_857583491 分钟前
新闻推荐系统:Spring Boot的架构优势
数据库·spring boot·架构
2401_854391086 分钟前
新闻推荐系统:Spring Boot与大数据
java·spring boot·后端
陈序缘15 分钟前
Go语言实现长连接并发框架 - 任务管理器
linux·服务器·开发语言·后端·golang
全职计算机毕业设计43 分钟前
基于协同过滤的网络文学智能推荐平台的设计与实现(小说)springboot mysql Redis Thymeleaf
spring boot·后端·mysql
原机小子1 小时前
大学生就业桥梁:基于Spring Boot的招聘系统
服务器·数据库·spring boot
忙里偷闲的sin1 小时前
整理Maven坐标,Spring Boot集成工具依赖版本差异问题
java·spring boot·maven
泯泷1 小时前
「生产必看」在企业环境中正确使用 Node.js 的九大原则
前端·后端·node.js
Gogeof2 小时前
云原生化 - 旅程(简约版)
微服务·云原生
苹果醋32 小时前
毕业设计_基于springboot+layui+mybatisPlus的中小型仓库物流管理系统源码+SQL+教程+可运行】41004
spring boot·毕业设计·layui·课程设计
JarvisO2 小时前
SpringBoot+Redis+RabbitMQ完成增删改查
spring boot·redis·java-rabbitmq