Spring Boot实践七--API开发(RESTful API、JWT数字签名与Swagger)

1,RESTful API与单元测试

Restful API是目前比较成熟的一套互联网应用程序的API设计理念,Rest是一组架构约束条件和原则,如何Rest约束条件和原则的架构,我们就称为Restful架构,Restful架构具有结构清晰、符合标准、易于理解以及扩展方便等特点,受到越来越多网站的采用

RESTful API介绍
RESTful API接口规范

  1. RESTful API规范

1.1 URL设计

  • URL应该清晰、简洁、易于理解和记忆。

  • URL应该使用名词而不是动词,例如/users而不是/getUsers。

  • URL应该使用小写字母和短横线分隔符,例如/users/123。

1.2 HTTP方法

  • GET:用于获取资源。

  • POST:用于创建资源。

  • PUT:用于更新资源。

  • DELETE:用于删除资源。

1.3 HTTP状态码

  • 200 OK:请求成功。

  • 201 Created:资源创建成功。

  • 204 No Content:请求成功,但没有返回任何内容。

  • 400 Bad Request:请求参数错误。

  • 401 Unauthorized:未授权访问。

  • 403 Forbidden:禁止访问。

  • 404 Not Found:请求的资源不存在。

  • 500 Internal Server Error:服务器内部错误。

  1. Token(令牌)

Token是一种身份验证方式,它是服务器生成的一串字符串,用于标识用户身份。当用户登录成功后,服务器会生成一个Token,并将其返回给客户端。客户端在后续的请求中需要携带该Token,以便服务器验证用户身份。

2.1 Token的生成方式

Token可以使用随机数、时间戳、用户ID等信息生成。常见的Token生成方式有JWT、OAuth等。

2.2 Token的验证方式

客户端在请求中需要携带Token,服务器在接收到请求后,需要验证Token的有效性。常见的Token验证方式有黑名单、白名单、签名等。

  1. MD5使用

MD5是一种常用的哈希算法,它将任意长度的消息压缩成一个128位的消息摘要。MD5常用于密码加密、文件校验等场景。

3.1 MD5的加密方式

MD5加密可以使用MD5算法库,也可以使用第三方库。

3.2 MD5的应用场景

  • 密码加密:将用户密码进行MD5加密后存储到数据库中,可以增加密码的安全性。

  • 文件校验:将文件内容进行MD5加密后,可以用于校验文件的完整性。

RESTful API 实现

首先回顾下Spring MVC的几个注解:

  • @Controller:修饰class,用来创建处理http请求的对象
  • @RestController:Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式
  • @RequestMapping:配置url映射。现在更多的也会直接用以Http Method直接关联的映射注解来定义,比如:GetMapping、PostMapping、DeleteMapping、PutMapping等.

下面我们通过使用Spring MVC来实现一组对User对象操作的RESTful API,配合注释详细说明在Spring MVC中如何映射HTTP请求、如何传参、如何编写单元测试。

RESTful API具体设计如下:
在这里插入图片描述
(1)定义User实体

package com.example.demospringboot;

import lombok.Data;

@Data
public class User {

    private Long id;
    private String name;
    private Integer age;

}

注意:相比1.x版本教程中自定义set和get函数的方式,这里使用@Data注解可以实现在编译器自动添加set和get函数的效果。该注解是lombok提供的,只需要在pom中引入加入下面的依赖就可以支持:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

(2)实现对User对象的操作接口

package com.example.demospringboot;

import org.springframework.web.bind.annotation.*;
import java.util.*;

@RestController
@RequestMapping(value = "/users")     // 通过这里配置使下面的映射都在/users下
public class UserController {

    // 创建线程安全的Map,模拟users信息的存储
    static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());

    /**
     * 处理"/users/"的GET请求,用来获取用户列表
     *
     * @return
     */
    @GetMapping("/")
    public List<User> getUserList() {
        // 还可以通过@RequestParam从页面中传递参数来进行查询条件或者翻页信息的传递
        List<User> r = new ArrayList<User>(users.values());
        return r;
    }

    /**
     * 处理"/users/"的POST请求,用来创建User
     *
     * @param user
     * @return
     */
    @PostMapping("/")
    public String postUser(@RequestBody User user) {
        // @RequestBody注解用来绑定通过http请求中application/json类型上传的数据
        users.put(user.getId(), user);
        return "success";
    }

    /**
     * 处理"/users/{id}"的GET请求,用来获取url中id值的User信息
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // url中的id可通过@PathVariable绑定到函数的参数中
        return users.get(id);
    }

    /**
     * 处理"/users/{id}"的PUT请求,用来更新User信息
     *
     * @param id
     * @param user
     * @return
     */
    @PutMapping("/{id}")
    public String putUser(@PathVariable Long id, @RequestBody User user) {
        User u = users.get(id);
        u.setName(user.getName());
        u.setAge(user.getAge());
        users.put(id, u);
        return "success";
    }

    /**
     * 处理"/users/{id}"的DELETE请求,用来删除User
     *
     * @param id
     * @return
     */
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        users.remove(id);
        return "success";
    }
}

MockMvc单元测试

下面针对该Controller编写测试用例验证正确性:

package com.example.demospringboot;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplication {

    private MockMvc mvc;

    @Before
    public void setUp() {
        mvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
    }

    @Test
    public void testUserController() throws Exception {
        // 测试UserController
        RequestBuilder request;

        // 1、get查一下user列表,应该为空
        request = get("/users/");
        mvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("[]")));

        // 2、post提交一个user
        request = post("/users/")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"id\":1,\"name\":\"Mike\",\"age\":20}");
        mvc.perform(request)
                .andExpect(content().string(equalTo("success")));

        // 3、get获取user列表,应该有刚才插入的数据
        request = get("/users/");
        mvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("[{\"id\":1,\"name\":\"Mike\",\"age\":20}]")));

        // 4、put修改id为1的user
        request = put("/users/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"Jack\",\"age\":30}");
        mvc.perform(request)
                .andExpect(content().string(equalTo("success")));

        // 5、get一个id为1的user
        request = get("/users/1");
        mvc.perform(request)
                .andExpect(content().string(equalTo("{\"id\":1,\"name\":\"Jack\",\"age\":30}")));

        // 6、del删除id为1的user
        request = delete("/users/1");
        mvc.perform(request)
                .andExpect(content().string(equalTo("success")));

        // 7、get查一下user列表,应该为空
        request = get("/users/");
        mvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("[]")));

    }
}

postman测试

当然也可以通过浏览器插件等进行请求提交验证。postman 可以直接在chrome 上安装插件,当然大部分的同学是没法连接到谷歌商店的,我们可以在电脑本地安装postman 客户端工具。
postman用于测试http协议接口,无论是开发, 还是测试人员, 都有必要学习使用postman来测试接口, 用起来非常方便。

Postman官网下载地址 https://www.postman.com/downloads/:

Postman for MAC https://dl.pstmn.io/download/latest/osx
Postman for windows 64 https://dl.pstmn.io/download/latest/win64
Postman for windows X86 https://dl.pstmn.io/download/latest/win32
Postman for linux https://dl.pstmn.io/download/latest/linux64

用法也比较简单:
在这里插入图片描述

示例如下:
创建User:
在这里插入图片描述
查询User:
在这里插入图片描述

2,web应用中的身份认证:cookie、session、token

(1) cookie和session

首先,要思考的是为啥有这三个东西,因为HTTP协议是无状态的特性导致的,比如客户端和服务端进行了一次请求和响应,协议只负责处理请求,拒绝响应之类的,但是不负责记忆,这样就有问题产生了,协议的拒绝或者响应肯定是需要验证的,但是协议本身没有记忆的能力,那么每次请求都去做验证显然是不合理的,就好比你逛淘宝每次去到一个新的页面都需要你进行登录验证,正常人都会心态爆炸啦,所以为了维持会话的进行,cookie和session就应运而生了,这也就是他的用途了,当然token也是同理,只是手段有些许差别。

①我们需要记住的是cookie是存储在客户端的,而session是存储在服务器的,这里这么理解,cookie就是钥匙放在客户的手上,服务端就是一个酒店,而session就是每一个门的门锁,门里面都有个服务员可以和客户对话,存贮位置不同是他们一个重要的区别,比如我们要进行一个会话,那肯定需要用钥匙打开对应的门才能继续会话。

②cookie是有过期时间的,如果不设置,cookie就是当前游览器会话时间的话,游览器关了,cookie就过期了,如果设置了过期时间,cookie会被存贮在硬盘上,游览器关闭,下次打开cookie依旧存在

③cookie有大小的限制,单个cookie的大小是4kb,多数游览器限制一个站点最多可以存储20个cookie。

④这里还有个需要注意的点就是cookie是以明文的形式存储的,存储方式为键值对,这样就暴露一个问题那就是隐私数据比如账号密码是不能存在于cookie的,不然很轻易就会被他人窃取,同样cookie也比较容易伪造,可以轻易修改cookie的数值,所以如果重要信息以明文的形式存储非常容易泄露被人利用。

所以,这里我将cookie和session验证的缺点总结一下:
①session需要将每一个会话存储会话信息存到内存中,这样会占用大量的内存,增加服务器的压力
②session有局限性,如果是单服务器不会有问题,因为账单都在总店,但是如果是分布式服务就会面临无法验证的问题,因为session是存储在服务器A中的。
③安全性不高,由于是服务器将cookie发给客户端,客户端只要发送cookie即可通过验证。客户端容易伪造出示cookie,服务端就验证通过了,将返回的信息以明文的形式返回给客户端进行保存。
④app等移动端应用会使得session失效
⑤cookie不允许跨域访问
⑥cookie有大小限制4kb

(2) token

token是服务端生成的一串字符串,作为客户请求的一个令牌,当第一次登录后,服务器就会生成一个token给客户端,以后的请求带上token就可以无需登录了,其实和cookie非常相似,但是token多了一个验证的步骤,不再是出示即可通过了。

相较于cookie,token减轻了服务端的压力,不需要就session这样将大量的会话记录保存起来了,而是通过token,服务器向redis中查询用户的信息。有了redis,服务器只需要去redis查询用户信息即可,并且token并不限制大小,可以允许跨域的请求,这也是不同与cookie的,也解决了app等移动端应用会使得session失效的问题。由此可见,token在多方面是有优于cookie的。

(3) JWT和数字签名

token除了去redis中查询还有一种JWT(JSon Web Token)的形式,因为频繁的查询redis同样会加强redis的压力,所以JWT相较于查询redis也有他的一定优点,他是将数据交给了游览器存储,下次需要继续请求的时候携带JWT给服务器进行验证就可以了,这样可以避免查询redis这个步骤,在一定程度上降低redis的压力:
在这里插入图片描述

另外,token和cookie不同的是token加入了签名机制,有了签名机制就加强了数据的安全性。JWT一般是一个字符串类似于adb.123ccc.666ad,以.为分隔符,由Header、Payload和Signature三部分组成。

  • Header:
{
"alg": "HS256", "typ": "JWT"
}

主要有两个属性,typ属性统一写JWT,alg是表示加密使用的算法,这里的HS256就是对称加密,还有一个是RS256是非对称加密.

  • payload

这里存放的就是一些类似于cookie的数据,类似于:

{
"iss""zs",
"sub":"123",
"sex""男" //同样允许写入自定义的字段
}

有七个默认字段可供选择:
iss (issuer):签发人/发行人
sub (subject):主题
aud (audience):用户
exp (expiration time):过期时间
nbf (Not Before):生效时间,在此之前是无效的
iat (Issued At):签发时间
jti (JWT ID):用于标识该 JWT

值得注意的是,JWT的json对象是采用Base64的方式简单加密的,由于这种加密是可逆,是可以通过反向编码得到原文,所以敏感数据依旧不能放在JWT中

  • Signature

签名是将前面两个部分(header和payload)的信息先进行base64加密成字符串的形式再使用头部声明的加密方式(如HS256)进行加密后得到的一个数据。

可以这么理解,当数据不被篡改的时候,将secret解密后的数据通过base64反向解析后得到的和header和payload是一样的数据;如果服务器解密Signature部分发现payload和header与发来的明文payload和header对不上,那就证明信息已经被篡改了。

这里有个可以散发的点,为什么使用HS256这种对称加密的方法?因为对称加密需要对密钥进行妥善保管,这个过程客户端都没有参与,是服务器对信息进行了加密解密,也就是说类似于cookie用户登录的信息被服务器加密后,返回给用户,下次使用的时候发给服务器,服务器再通过密钥解密确认信息是否被篡改过。自始至终都是服务器在做加密和解密验证的工作,密钥也只有服务器知道,密钥不需要在客户端和服务器中传输,也就不存在安全性的问题。不考虑传输安全性的情况,对称加密的解密更快,肯定是优于非对称加密的,这也是为什么JWT大多采用HS256,而不是我们认为更安全的RS256(非对称加密RSA),主要就是因为不需要传递密钥,所以业务场景很重要。有了签名之后,即使 JWT 被泄露或者截获,如果服务端的秘钥不被泄露的话,黑客也没办法同时篡改 Signature、Header、Payload。所以密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。

补充说明:
对称加密其实就是加密和解密用的都是一个钥匙,但是由于加解密用的一个钥匙,密钥的保存和传输就是个问题,如果一旦别人截获密钥的信息就能得到全部数据,同时密钥的保存也是一个问题。

非对称加密RSA数字签名算法会生成公钥和私钥,这里对于公钥和私钥的理解需要注意一下,公私的区分是公钥是发布在互联网中,或者说对外公布,而私钥是个人保存的,比如一个数据可以进行公钥加密,那么拥有私钥的人通过私钥解密就可以获得原文了;而私钥加密的前提下,如果只知道公钥是无法获得原文的。因此,非对称公钥无需保存,只保存私钥即可,但是加解密过程慢;对称加密,加解密同一密钥,密钥保存难度大,但是加解密过程快,所有优势互补,就有了以下场景:

A公司拥有自己的公钥A和私钥A,同样B公司拥有公钥B和私钥B。 A公司如果想想B公司发送一段重要隐私信息,那么可以通过B公司的公钥进行加密,将需要传输的数据进行对称加密,将加密的密钥一块发送给B公司,那么只有拥有对应私钥的B公司才能解密获得信息的内容,再通过获取的密钥将对称加密的信息进行解密,这样在保证安全性的同时,大大提升了解密的速度。

(4) JWT 实践

1,引入JWT依赖,由于是基于Java,所以需要的是java-jwt

<dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>3.4.0</version>
</dependency>

2,自定义两个注解

(1)用来跳过验证的PassToken:

package com.example.demospringboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

(2)用来使用验证的UseToken :

package com.example.demospringboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

自定义注解参数说明:

@Target:注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包

@Retention:注解的保留位置
RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,class字节码文件中不包含。
RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,class文件中存在,JVM将会忽略,运行时无法获得。
RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解

3,在Controller中使用token

(1)在postUser注册接口中获取token并返回;
(2)在getUser查询接口增加@UseToken注解验证token

   /**
     * 处理"/users/"的POST请求,用来创建User,并获取token值
     *
     * @param user
     * @return
     */
    @PostMapping("/")
    public JSONObject postUser(@RequestBody User user) {
        // @RequestBody注解用来绑定通过http请求中application/json类型上传的数据
        users.put(user.getId(), user);

        JSONObject jsonObject=new JSONObject();
        String token = tokenService.getToken(user);
        jsonObject.put("token", token);
        jsonObject.put("user", user);
        return jsonObject;
    }

    /**
     * 处理"/users/{id}"的GET请求,用来获取url中id值的User信息
     * @UseToken:该接口必须在请求头中加上token并通过验证才可以访问
     */
    @UseToken
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // url中的id可通过@PathVariable绑定到函数的参数中
        return users.get(id);
    }

获取token的方法实现如下:

package com.example.demospringboot;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.stereotype.Service;

@Service("TokenService")
public class TokenService {
    public String getToken(User user) {
        String token="";
        // withAudience()存入需要保存在token的信息,这里我把用户ID存入token中
        // Algorithm.HMAC256():使用HS256加密生成token,这里密钥用了用户age,唯一密钥的话可以保存在服务端。
        token= JWT.create().withAudience(String.valueOf(user.getId()))// 将 user id 保存到 token 里面
                .sign(Algorithm.HMAC256(String.valueOf(user.getAge())));// 以 age 作为 token 的密钥
        return token;
    }
}

4,实现一个拦截器去获取token并验证token:

SpringMVC 中的Interceptor 拦截请求是通过HandlerInterceptor 来实现的。HandlerInterceptor接口是Spring框架中用于拦截HTTP请求的接口,它提供了三个方法,分别在请求处理之前、请求处理之后和请求处理完成之后被调用。

package com.example.demospringboot;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

import static com.example.demospringboot.UserController.users;


public class AuthenticationInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        //检查有没有需要用token的注解
        if (method.isAnnotationPresent(UseToken.class)) {
            UseToken userLoginToken = method.getAnnotation(UseToken.class);
            if (userLoginToken.required()) {
                // 执行认证
                String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
                if (token == null) {
                    throw new RuntimeException("无token!");
                }
                // 获取 token 中的 user id
                String userId;
                try {
                    userId = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {
                    throw new RuntimeException("401");
                }
                User user = users.get(Long.parseLong(userId));
                if (user == null) {
                    throw new RuntimeException("用户不存在,请重新登录");
                }
                // 验证 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(String.valueOf(user.getAge()))).build();
                try {
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e) {
                    throw new RuntimeException("401");
                }
                return true;
            }
        }
        return true;
    }

    @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,最后配置拦截器

创建拦截器配置类:InterceptorConfig------必须搭配的类,实现WebMvcConfigurer(建议使用)并且添加@Configuration注解;另一种继承WebMvcConfigurerAdapter不建议使用

@Configuration
public class InterceptorConfig implements  WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");    // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}

6,post验证:

(1),注册时获取token:
在这里插入图片描述

(2)未使用token或错误token:
在这里插入图片描述
在服务侧可以看到抛出对应异常。

(3)正确使用token:
在这里插入图片描述

github:https://github.com/JackyZhang888/springboot/tree/main/useTokenJWT

3,使用Swagger2构建强大的API文档

本章将介绍RESTful API的重磅好伙伴Swagger2,它可以轻松的整合到Spring Boot中,并与Spring MVC程序配合组织出强大RESTful API文档。它既可以减少我们创建文档的工作量,同时说明内容又整合入实现代码中,让维护文档和修改代码整合为一体,可以让我们在修改代码逻辑的同时方便的修改文档说明。另外Swagger2也提供了强大的页面测试功能来调试每个RESTful API。

第一步:添加swagger-spring-boot-starter依赖

在pom.xml中加入依赖,具体如下:

<dependency>
    <groupId>com.spring4all</groupId>
    <artifactId>swagger-spring-boot-starter</artifactId>
    <version>1.9.0.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

第二步:应用主类中添加@EnableSwagger2Doc注解,具体如下

package com.example.demospringboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.spring4all.swagger.EnableSwagger2Doc;

@SpringBootApplication
@EnableSwagger2Doc
public class DemospringbootApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemospringbootApplication.class, args);
	}
}

第三步:application.properties中配置文档相关内容,比如

swagger.title=spring-boot-starter-swagger
swagger.description=Starter for swagger 2.x
swagger.version=1.4.0.RELEASE
swagger.license=Apache License, Version 2.0
swagger.licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.html
swagger.termsOfServiceUrl=https://github.com/xxx/spring-boot-starter-swagger
swagger.contact.name=didi
swagger.contact.url=http://blog.xxx.com
swagger.contact.email=xxx.com
swagger.base-package=com.example.demospringboot
swagger.base-path=/**

spring.mvc.pathmatch.matching-strategy=ant_path_matcher

各参数配置含义如下:

  • swagger.title:标题
  • swagger.description:描述
  • swagger.version:版本
  • swagger.license:许可证
  • swagger.licenseUrl:许可证URL
  • swagger.termsOfServiceUrl:服务条款URL
  • swagger.contact.name:维护人
  • swagger.contact.url:维护人URL
  • swagger.contact.email:维护人email
  • swagger.base-package:swagger扫描的基础包,默认:全扫描
  • swagger.base-path:需要处理的基础URL规则,默认:/**

更多配置说明可见官方说明:https://github.com/SpringForAll/spring-boot-starter-swagger

最后配置的spring.mvc.pathmatch.matching-strategy=ant_path_matcher是因为Springboot 版本与 Swagger 版本不匹配:由于Spring Boot 2.6.x 请求路径与 Spring MVC 处理映射匹配的默认策略从AntPathMatcher更改为PathPatternParser。所以需要设置spring.mvc.pathmatch.matching-strategy为ant-path-matcher来改变它。

第四步:启动应用,访问:http://localhost:8080/swagger-ui.html,就可以看到如下的接口文档页面:
在这里插入图片描述
点击user-controller可以看到具体接口:
在这里插入图片描述

添加Api文档内容

在整合完Swagger之后,在http://localhost:8080/swagger-ui.html页面中可以看到,关于各个接口的描述还都是英文或遵循代码定义的名称产生的。这些内容对用户并不友好,所以我们需要自己增加一些说明来丰富文档内容。如下所示,我们通过@Api,@ApiOperation注解来给API增加说明、通过@ApiImplicitParam、@ApiModel、@ApiModelProperty注解来给参数增加说明。

比如下面的例子:

@Data
@ApiModel(description="用户实体")
public class User {

    @ApiModelProperty("用户编号")
    private Long id;
    @ApiModelProperty("用户姓名")
    private String name;
    @ApiModelProperty("用户年龄")
    private Integer age;
}
@Api(tags = "用户管理")
@RestController
@RequestMapping(value = "/users")     // 通过这里配置使下面的映射都在/users下
public class UserController {

    // 创建线程安全的Map,模拟users信息的存储
    static Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());

    @GetMapping("/")
    @ApiOperation(value = "获取用户列表")
    public List<User> getUserList() {
        List<User> r = new ArrayList<>(users.values());
        return r;
    }

    @PostMapping("/")
    @ApiOperation(value = "创建用户", notes = "根据User对象创建用户")
    public String postUser(@RequestBody User user) {
        users.put(user.getId(), user);
        return "success";
    }

    @GetMapping("/{id}")
    @ApiOperation(value = "获取用户详细信息", notes = "根据url的id来获取用户详细信息")
    public User getUser(@PathVariable Long id) {
        return users.get(id);
    }

    @PutMapping("/{id}")
    @ApiImplicitParam(paramType = "path", dataType = "Long", name = "id", value = "用户编号", required = true, example = "1")
    @ApiOperation(value = "更新用户详细信息", notes = "根据url的id来指定更新对象,并根据传过来的user信息来更新用户详细信息")
    public String putUser(@PathVariable Long id, @RequestBody User user) {
        User u = users.get(id);
        u.setName(user.getName());
        u.setAge(user.getAge());
        users.put(id, u);
        return "success";
    }

    @DeleteMapping("/{id}")
    @ApiOperation(value = "删除用户", notes = "根据url的id来指定删除对象")
    public String deleteUser(@PathVariable Long id) {
        users.remove(id);
        return "success";
    }

}

完成上述代码添加后,启动Spring Boot程序,访问:http://localhost:8080/swagger-ui.html,就能看到下面这样带中文说明的文档了(其中标出了各个注解与文档元素的对应关系以供参考):
在这里插入图片描述
在这里插入图片描述

API文档访问与调试

在上图请求的页面中,我们看到user的Value是个输入框。因为Swagger除了查看接口功能外,还提供了调试测试功能,我们可以点击上图中点击下方"Try it out!“(“Cancal”)按钮,再执行"Execure”,即可完成了一次请求调用!
在这里插入图片描述

3,JSR-303实现请求参数校验

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

Bean Validation中内置的constraint:
在这里插入图片描述
Hibernate Validator附加的constraint:
在这里插入图片描述
在JSR-303的标准之下,我们可以通过上面这些注解,优雅的定义各个请求参数的校验。比如:校验字符串是否为空、检验字符串的长度、校验数字的大小、校验字符串格式是否为邮箱等。

第一步:在要校验的字段上添加上@NotNull、@Size、@MAx、@Min注解,具体如下:

package com.example.demospringboot;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.*;

@Data
@ApiModel(description="用户实体")
public class User {

    @ApiModelProperty("用户编号")
    private Long id;

    @NotNull(message="姓名不能为空")
    @Size(min = 2, max = 5)
    @ApiModelProperty("用户姓名")
    private String name;

    @NotNull(message="年龄不能为空")
    @Max(100)
    @Min(10)
    @ApiModelProperty("用户年龄")
    private Integer age;
}

第二步:在需要校验的参数实体前添加@Valid注解,具体如下:

@PostMapping("/")
@ApiOperation(value = "创建用户", notes = "根据User对象创建用户")
public String postUser(@Valid @RequestBody User user) {
    users.put(user.getId(), user);
    return "success";
}

完成上面配置之后,启动应用,并用POST请求访问localhost:8080/users/接口,body使用一个空对象{}。用Postman等测试工具发起,或使用curl发起:

curl -X POST \
  http://localhost:8080/users/ \
  -H 'Content-Type: application/json' \
  -H 'Postman-Token: 72745d04-caa5-44a1-be84-ba9c115f4dfb' \
  -H 'cache-control: no-cache' \
  -d '{
    
}'

可以看到输出的信息:
在这里插入图片描述

可以看到前端已经报错,但是并没有收到我们返回的校验信息,可以通过自定义捕获异常后的处理方式,简单实现:

package com.example.demospringboot;

import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;
import java.util.stream.Collectors;

@ControllerAdvice
public class CustomExceptionHandler {

    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String methodArgumentNotValidExceptionHandle(MethodArgumentNotValidException e) {
        List<ObjectError> allErrors = e.getAllErrors();
        List<String> errorsMessages = allErrors.stream()
                .map(error -> error.getDefaultMessage())
                .collect(Collectors.toList());
        return errorsMessages.toString();
    }
}

返回如下:
在这里插入图片描述

当然,为了让前端有更好的逻辑展示与页面交互处理,统一的数据返回格式必不可少。可以参考:https://www.hangge.com/blog/cache/detail_2897.html

常用的返回内容参数:

  • timestamp:请求时间
  • status:HTTP返回的状态码,这里返回400,即:请求无效、错误的请求,通常参数校验不通过均为400
  • error:HTTP返回的错误描述,这里对应的就是400状态的错误描述:Bad Request
  • errors:具体错误原因,是一个数组类型;因为错误校验可能存在多个字段的错误,比如这里因为定义了两个参数不能为Null,所以存在两条错误记录信息
  • message:概要错误消息,返回内容中很容易可以知道,这里的错误原因是对user对象的校验失败,其中错误数量为2,而具体的错误信息就定义在上面的errors数组中
  • path:请求路径

4,找回启动日志中的请求路径列表

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping类在启动的时候,通过扫描Spring MVC的@Controller、@RequestMapping等注解去发现应用提供的所有接口信息。然后在日志中打印,以方便开发者排查关于接口相关的启动是否正确。

从Spring Boot 2.1.0版本开始,将这些日志的打印级别做了调整:从原来的INFO调整为TRACE。所以,当我们希望在应用启动的时候打印这些信息的话,只需要在配置文件增增加对RequestMappingHandlerMapping类的打印级别设置即可,比如在application.properties中增加下面这行配置既可:

logging.level.org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping=tra

参考:
https://blog.didispace.com/spring-boot-learning-21-2-3/
https://blog.csdn.net/zhanghfei88/article/details/123663218
https://blog.csdn.net/weixin_54515240/article/details/129445989
https://www.jianshu.com/p/e88d3f8151db

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值