实战篇04 05 06 1.用户输入的参数需要校验是否有效,然后才能进数据库查询,为了避免参数校验的if麻烦,引入SpringBoo Validation起步依赖,在要校验的参数前添加@**P**attern,使用正则表达式完成校验,然后再controller类上添加@Validated注解,刷新Maven ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/eb5c9b041d434871a83bde60087094ed.png) 运行一下postman测试: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ced3d0547b2c4200a213f31455e7d080.png)![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f61bf5410a2f4903a6f87f27ce46fb2c.png) 参数不符合接口文档,让后显然现在的返回内容不符合接口文档,因此还要修改,使用全局异常处理器 定义一个类然后注释@RestControllerAdvice注解-标识这个 类用于处理异常,所有的方法返回值都会被返回成json字符串,用方法来返回异常因此这个方法上要添加@ExceptionHandler(Exception.class)注解,Exception.class表示所有异常,方法返回类型是Result这样就可以保障返回类型符合接口文档要求。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/7282c5a8c48c4e45a07579460e0e673a.png)

创建包exception,和controller同一个目录,新建GlobalExceptionHandler类:
在这里插入图片描述
技巧:
return Result.error(StringUtils.hasLength(e.getMessage())?e.getMessage():“操作失败”);
在写完StringUtils的时候如果直接alt+enter可能会调错包变成import com.mysql.cj.util.StringUtils;所以删掉它全部写完再试一次就会正确:
import org.springframework.util.StringUtils;

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e){
        //输出e方便调试
        e.printStackTrace();
        //e会封装错误信息,判断长度是否大于零
        return Result.error(StringUtils.hasLength(e.getMessage())?e.getMessage():"操作失败");

    }
}

在这里插入图片描述
2.登入
在这里插入图片描述
根据用户查询用户在注册的时候已经写过了,可以复用,重点看Controller,注意MySQL返回的查询结果是根据名字返回查到的那一整行,用的是select*

@Select("select * from user where username = #{username}")
    User findByUserName(String username);
    ```
Controller代码暂时忽略JWT令牌:
```java
    @PostMapping("/login")
    public Result<String> login(@Pattern(regexp="^\\S{5,16}$") String username, @Pattern(regexp="^\\S{5,16}$")String password){
        //根据用户名查询用户
        User loginUser = userService.findByUserName(username);
        //判断用户是否存在
        if(loginUser == null) {
            return Result.error("用户不存在");
        }

        //判断秘法是否正确
        if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){

            return Result.success("jwt token ");
        }
        
        return Result.error("密码错误");
   }

在这里插入图片描述
3.JWT登入认证
要登入以后才能访问相关的服务,因此每个服务需要检查登录状态,这个就是登录认证
在Controller目录下新建ArticleController类

@RestController
@RequestMapping("/article")
public class ArticleController {

    @GetMapping("/list")
    public Result<String> list(){
        return Result.success("所有的文章");

    }
}

执行后访问 http://localhost:8080/article/list
在这里插入图片描述
登录认证原理:
生成一个令牌,登录后检查令牌是否合法
令牌的特点:
1.业务数据,减少数据库请求次数,如为记录请求来源就要查询用户信息,为了避免则在令牌上加入用户信息
2.防止篡改,保障信息合法性和有效性
4.JWT(Json Web Token)
如:
在这里插入图片描述

是前后端之间传输的字符串,通过 两个.把字符串分成三个部分:
第一个部分:alg-加密算法,type-令牌类型
第二个部分:有效载荷-业务数据(如id,username,基于Base64编码方式完成-基于64个可打印字符的二进制编码方式A-Z,a-z,0-9,+,/非加密且公开!不要存放私密数据如密码)
第三部分:数字签名,将第一部分和第二部分加密后得到
服务器拿到数据后将解密内容与前面比较
引入JWT,刷新

    <dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>4.4.0</version>
    </dependency>
    ```
在test下新建一个类JwtTest作单元测试,熟悉一下jwt工具
```java
public class JwtTest {
    @Test
    public void testGen() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", "1");
        claims.put("name", "张三");
        //生成JWT
        String token = JWT.create()
                .withClaim("user", claims)//添加载荷,claims是一个map集合
                .withExpiresAt(new Date(System.currentTimeMillis() * 1000 * 60 * 60 * 12))//添加过期时间,毫秒时间->12h
                .sign(Algorithm.HMAC256("123"));//指定算法设置加密密钥
        System.out.println(token);
    }

}
    @Test
    //测试jwt工具密钥解析
    public void testParse(){
        //定义字符串(上面的)模拟用户传递jwt令牌
        String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Im5hbWUiOiLlvKDkuIkiLCJpZCI6IjEifSwiZXhwIjozMDcwMTMwNDk1MjkyOTkzfQ.8HQcqMpMNOnVihaZE9ldkN2tgGmSOwL-i0xr4Qypx3o";//需要重新从终端复制否则会因为超时验证失败

        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("123")).build();//解密的密码

        DecodedJWT decodedJWT = jwtVerifier.verify(token);//生成一个解析后的JWT对象
        Map<String, Claim>  claims= decodedJWT.getClaims();
        System.out.println(claims.get("user"));
    }

4.JWT生成
复制02资料\04_综合案例资料\02_后台资料\02_工具类\JWTUtil.java到工具类Utils目录下

    @PostMapping("/login")
    public Result<String> login(@Pattern(regexp="^\\S{5,16}$") String username, @Pattern(regexp="^\\S{5,16}$")String password){
        //根据用户名查询用户
        User loginUser = userService.findByUserName(username);
        //判断用户是否存在
        if(loginUser == null) {
            return Result.error("用户不存在");
        }

        //判断秘法是否正确
        if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
            //登录成功-只修改这里
            Map<String,Object> claims = new HashMap<>();
            claims.put("id",loginUser.getId());
            claims.put("username",loginUser.getUsername());
            String token = JwtUtil.genToken(claims);
            return Result.success(token);
        }

        return Result.error("密码错误");
    }

在这里插入图片描述
5.JWT验证
浏览器与服务器通信的时候会携带token,在header部分,请求头位Authorization,值位登录时下发的令牌,若未登录成功返回401
如果收到的token解析失败则表示登录失败,所以这里要使用异常处理!
技巧:选中需要打包的代码,然后点击包围
在这里插入图片描述
ArticleController

    @GetMapping("/list")
   public Result<String> list(@RequestHeader(name = "Authorization") String token, HttpServletResponse response){
       //验证token
       //如果该代码异常则失败,否则成功

       try {
           Map<String, Object> claims = JwtUtil.parseToken(token);
       } catch (Exception e) {
           //http响应状态码位401-使用HTTPServletResponse
           response.setStatus(401);
           return Result.error("未登录");
       }
       return Result.success("所有的文章");
   }

未登录尝试:http://localhost:8080/article/list在这里插入图片描述
注意是GET方法,勾选请求头:在这里插入图片描述
6.拦截器
当有多个接口都需要JWT认证的时候应该使用拦截器,让所有接口直接调用
还要实现一个配置类实现WebMvcConfiger把拦截器注册进去
在这里插入图片描述
新建包interceptors\LoginInterceptor.java
需要实现HandlerInterceptor接口,添加@Component注释

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //令牌验证
        //借助request对象拿到令牌-所有请求数据都在request对象中
        String token = request.getHeader("Authorization");
        try {
            System.out.println(token);
            Map<String, Object> claims = JwtUtil.parseToken(token);
        } catch (Exception e) {
            //http响应状态码位401-使用HTTPServletResponse
            response.setStatus(401);
            //不放行
            return false;
        }
        //放行
        return true;
    }
}

新建文件夹config/WebConfig.java 实现接口WebMvcConfigerer

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //在Interceptors中已经注入了一个LoginInterceptor
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注入拦截器,不过它会拦截所有接口包括注册和登录接口,所以加入excludePathPatterns(),输入排除的接口路径
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login","/user/register");
    }
}

注册不影响:
在这里插入图片描述
登录不影响:在这里插入图片描述
获取文章列表影响:
在这里插入图片描述
打钩上Authorization,有JWT就可以正常访问:
在这里插入图片描述

1.@RestControllerAdvice和@ExceptionHandler(Exception.class)

@RestControllerAdvice

  • 自动拦截所有 @Controller 或 @RestController 中抛出的异常
  • 自动将返回值转换为 JSON(因为包含了 @ResponseBody)
    @ExceptionHandler(Exception.class)
  • 标记这个方法为异常处理器
  • Exception.class:表示处理所有类型的异常(Exception 是所有异常的父类)
    当Controller发生异常后会被捕获,然后自动匹配ExceptionHandler中的异常,调用对应的方法
@ExceptionHandler @RestControllerAdvice Spring 框架 Controller 客户端 @ExceptionHandler @RestControllerAdvice Spring 框架 Controller 客户端 发送请求 方法执行 发生异常 抛出异常 捕获异常 查找 @RestControllerAdvice 查找 @ExceptionHandler 匹配异常类型 (Exception.class 匹配所有) 调用 handleException(e) 返回 Result 对象 返回 Result Result 转 JSON 返回 JSON 响应

冲突问题:

场景 是否冲突 行为
不同异常类型(同级) ✅ 不冲突 各自处理自己的异常
父子异常关系 ✅ 不冲突 选择最精确的子类处理器
同一个异常类(同级) ❌ 冲突 报错Ambiguous @ExceptionHandler method mapped

或随机选择

多个处理器 + @Order ✅ 不冲突 按顺序执行第一个匹配的

2.实体类Resut
实体类Result中的方法都是静态方法,所以return的时候不需要new一个新对象

  • 代码简洁,语义清晰如 return Result.error();
  • 所有错误响应格式一致

设计模式 - 静态工厂方法

  • 方法名更有语义
  • 运行的时候缓存的是同一个对象,不用new重新构造
  • 可以返回继承该类的子类型
  • 使用多个方法重载可以使得参数更灵活
    3.Exception e.printStackTrace();
    e.printStackTrace() 是 Java 中打印异常堆栈信息的方法,用于调试和排查问题。
    4.Result,是泛型类
写法 含义 是否合法 赋值给变量时

类型强制转换?

Result 原始类型(Raw Type) ✅ 合法(但不推荐)忽略泛型,data 当 Object 用
Result 泛型类型(定义时用) ✅ 类型定义 是定义时使用的,不是使用时不存在强制类型转换问题
Result 参数化类型(使用时用) ✅ 指定具体类型 任何一个具体类都不用强制类型转换
5.if(Md5Util.getMD5String(password).equals(loginUser.getPassword()))
如果使用==的话会报错,因为比较的是内存地址!
==比较数值的时候是比变量值
==比字符串常亮的时候也是比变量值
==比较的对象是像字符串这样的用于表示内存地址的符号的时候是比内存地址
// 字面量写法(直接写双引号)
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);  // true(指向常量池中的同一个对象)

// new 关键字创建
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s3 == s4);  // false(堆中不同对象)

6.简洁性:
对于两个互斥的退出条件时采用if return的方法而不是if else,因为更简洁!

        if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){

            return Result.success("jwt token ");
        }
        
        return Result.error("密码错误");
   }

7.jwt-混合了静态方法和实力方法的库

// 你看的代码
String token = JWT.create()              // 1️⃣ 静态方法
        .withClaim("user", claims)       // 2️⃣ 实例方法
        .withExpiresAt(expireDate)       // 2️⃣ 实例方法
        .sign(Algorithm.HMAC256("123")); // 2️⃣ 实例方法

8.拦截器


9.注释@Component、@Autowired、@Configuration

更多推荐