05-SpringBoot

1.介绍

Spring Boot是基于Spring开发的全新框架,相当于对Spring做了又一层封装。
其设计目的是用来简化Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。(自动配置)
并且对第三方依赖的添加也进行了封装简化。(起步依赖)
所以Spring能做的他都能做,并且简化了配置。
并且还提供了一些Spring所没有的比如:
  • 内嵌web容器,不再需要部署到web容器中提供准备好的特性,如指标、健康检查和外部化配置

    最大特点:自动配置起步依赖
    官网:https://spring.io/projects/spring-boot

2.入门

2.1HelloWorld案例

1.导入依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

2.编写启动类

package com.baidu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

3.编写controller类

package com.baidu.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "yjf";
    }
}


目录结构如下:
image.png
4.进入启动类,运行main方法
启动成功
image.png
5.通过浏览器访问
访问地址:
http://localhost:8080/user/hello
效果:
image.png

2.2常见问题以及解决方案

①访问时404
检查Controller是不是在启动类所在的包或者其子包下,如果不是需要进行修改。
②依赖爆红
配置阿里云镜像后刷新maven项目让其下载。

2.3打包运行

我们可以把springboot的项目打成jar包直接去运行。
①添加maven插件

    <build>
        <plugins>
            <!--springboot打包插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

②maven打包
image.png
③运行jar包
在jar包所在目录执行命令

java -jar jar包名称

即可运行。

2.4快速构建

https://start.spring.io/

3.起步依赖

SpringBoot依靠父项目中的版本锁定和starter机制让我们能更轻松的实现对依赖的管理

3.1依赖冲突

一般程序在运行时发生类似于 java.lang.ClassNotFoundException,Method not found: ‘……’,或者莫名其妙的异常信息,这种情况一般很大可能就是 jar包依赖冲突的问题引起的了。
一般在是A依赖C(低版本),B也依赖C(高版本)。 都是他们依赖的又是不同版本的C的时候会出现。

常见问题提示与解决方案:
如果出现了类似于 java.lang.ClassNotFoundException,Method not found: 这些异常检查相关的依赖冲突问题,排除掉低版本的依赖,留下高版本的依赖。

3.2版本锁定

我们的SpringBoot模块都需要继承一个父工程:spring-boot-starter-parent。在spring-boot-starter-parent的父工程spring-boot-dependencies中对常用的依赖进行了版本锁定。这样我们在添加依赖时,很多时候都不需要添加依赖的版本号了。

我们也可以采用覆盖properties配置或者直接指定版本号的方式修改依赖的版本。

例如:
直接指定版本号

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.49</version>
    </dependency>

覆盖properties配置

    <properties>
      <mysql.version>5.1.49</mysql.version>
    </properties>

3.3starter机制

当我们需要使用某种功能时只需要引入对应的starter即可。一个starter针对一种特定的场景,其内部引入了该场景所需的依赖。这样我们就不需要单独引入多个依赖了。
命名规律
  • 官方starter都是以 spring-boot-starter开头后面跟上场景名称。例如:spring-boot-starter-data-jpa
  • 非官方starter则是以 场景名-spring-boot-starter的格式,例如:mybatis-spring-boot-starter

4.自动配置

SpringBoot中最重要的特性就是自动配置。
Springboot遵循“**约定优于配置**”的原则,自动进行了默认配置。这样我们就不需要做大量的配置。
当我们需要使用什么场景时,就会自动配置这个场景相关的配置。
如果他的默认配置不符合我们的需求时修改这部分配置即可。

5.配置文件

5.1YML配置

5.1.1YML是什么
YAML (YAML Ain't a Markup Language)YAML不是一种标记语言,通常以.yml为后缀的文件,是一种直观的能够被电脑识别的数据序列化格式,并且容易被人类阅读,容易和脚本语言交互的,可以被支持YAML库的不同的编程语言程序导入,一种专门用来写配置文件的语言。

YAML试图用一种比XML更敏捷的方式,来完成XML所完成的任务。
例如:

student:
    name: ZS
    age: 15
5.1.2优点
  1. YAML易于人们阅读。
  2. 更加简洁明了

5.2语法

5.2.1约定
  • k: v 表示键值对关系,冒号后面必须有一个空格
  • 使用空格的缩进表示层级关系,空格数目不重要,只要是左对齐的一列数据,都是同一个层级的
  • 大小写敏感
  • 缩进时不允许使用Tab键,只允许使用空格
  • java中对于驼峰命名法,可用原名或使用-代替驼峰,如java中的lastName属性,在yml中使用lastName或 last-name都可正确映射。
  • yml中注释前面要加#
5.2.2键值关系
普通值(字面量)

k: v:字面量直接写;
字符串默认不用加上单引号或者双绰号;
“”: 双引号;转意字符能够起作用
name: “zs \n lis”:输出;zs 换行 ls
‘’:单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据

name1: zs 
name2: 'ZS  \n ls'
name3: "zs  \n ls"
age: 15
flag: true
日期
date: 2023/01/01
对象(属性和值)、Map(键值对)

多行写法:
在下一行来写对象的属性和值的关系,注意缩进

student:
  name: ZS
  age: 20

行内写法:

student: {name: zhangsan,age: 20}
数组、list、set

用- 值表示数组中的一个元素
多行写法:

pets:
  - dog
  - pig
  - cat

行内写法:

pets: [dog,pig,cat]
对象数组、对象list、对象set
students:
 - name: zhangsan
   age: 22
 - name: lisi
   age: 20
 - {name: wangwu,age: 18}
5.2.3占位符赋值

可以使用 ${key:defaultValue} 的方式来赋值,若key不存在,则会使用defaultValue来赋值。
例如

server:
  port: ${myPort:8080}

myPort: 80   

5.3SpringBoot读取YML

5.3.1 @Value注解

注意使用此注解只能获取简单类型的值(8种基本数据类型及其包装类,String,Date)

student:
  lastName: zs
@Controller
public class TestController {
    @Value("${student.lastName}")
    private String lastName;
    @RequestMapping("/test")
    public String test(){
        System.out.println(lastName);
        return "yjf";
    }
    
}

注意:加了@Value的类必须是交由Spring容器管理的

5.3.2 @ConfigurationProperties
yml配置
student:
  lastName: zs
  age: 17
student2:
  lastName: zs2
  age: 15

在类上添加注解@Component 和@ConfigurationProperties(prefix = “配置前缀”)

@Data
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
    private String lastName;
    private Integer age;
}
从spring容器中获取Student对象
@Controller
public class TestController {

    @Autowired
    private Student student;
    @RequestMapping("/test")
    public String test(){
        System.out.println(student);
        return "yjf";
    }
}

**注意事项:要求对应的属性要有set/get方法,并且key要和成员变量名一致才可以对应的上。**

5.4练习

要求把下面实体类中的各个属性在yml文件中进行赋值。然后想办法读取yml配置的属性值,进行输出测试。

@Data
public class Student {
    private String lastName;
    private Integer age;
    private Boolean boss;

    private Date birthday;
    private Map<String,String> maps;
    private Map<String,String> maps2;
    private List<Dog> list;

    private Dog dog;
    private String[] arr;
    private String[] arr2;

    private Map<String,Dog> dogMap;
}

@Data
class Dog {
    private String name;
    private Integer age;
}
答案
# 练习
student:
  lastName: zs
  age: 15
  boss: true
  birthday: 2006/2/3
  maps:
    name: zs
    age: 11
  maps2: {name: ww,age: 199}
  list:
    - name: 小白
      age: 3
    - name: 小黄
      age: 4
    - {name: 小黑,age: 1}
  dog:
    name: 小红
    age: 5
  arr:
    - zs
    - ls

  arr2: [zs,ls]
  dogMap:
    xb: {name: 小白,age: 9}
    xh:
      name: 小红
      age: 6

@Data
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
    private String lastName;
    private Integer age;
    private Boolean boss;

    private Date birthday;
    private Map<String,String> maps;
    private Map<String,String> maps2;
    private List<Dog> list;

    private Dog dog;
    private String[] arr;
    private String[] arr2;

    private Map<String,Dog> dogMap;
}
@Data
class Dog {
    private String name;
    private Integer age;
}

5.5 YML和properties配置的相互转换

我们可以使用一些网站非常方便的实现YML和properties格式配置的相互转换。

转换网站:https://www.toyaml.com/index.html

5.6 配置提示

如果使用了@ConfigurationProperties注解,可以增加以下依赖,让我们在书写配置时有相应的提示。
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
**注意:添加完依赖加完注解后要运行一次程序才会有相应的提示。**

6.单元测试

我们可以使用SpringBoot整合Junit进行单元测试。
**Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库**。

2.1 使用

①添加依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
②编写测试类
@SpringBootTest
public class ApplicationTest {

    @Autowired
    private HelloController helloController;

    @Test
    public void testJunit(){
        System.out.println(1);
        System.out.println(helloController);
    }
}

注意:测试类所在的包需要和启动类是在同一个包下。否则就要使用如下写法指定启动类。

//classes属性来指定启动类
@SpringBootTest(classes = UserApplication.class)
public class ApplicationTest {

    @Autowired
    private UserController userController;

    @Test
    public void testJunit(){
        System.out.println(1);
        System.out.println(userController);
    }
}

7.整合mybatis

3.1 准备工作

①数据准备

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `user_info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, 'zs', '123', 'xx');
INSERT INTO `t_user` VALUES (2, 'ls', '456', 'yy');
INSERT INTO `t_user` VALUES (6, 'xxx', '55', '56');
INSERT INTO `t_user` VALUES (7, '55', '55', '5');

SET FOREIGN_KEY_CHECKS = 1;

image.png

②实体类

package com.baidu.domain;

import lombok.Data;

@Data
public class User {

    private Long id;
    private String username;
    private String password;
    private String userInfo;
}

3.2 整合步骤

github: [https://github.com/mybatis/spring-boot-starter/](https://github.com/mybatis/spring-boot-starter/)
①依赖
   <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--mybatis启动器-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

    </dependencies>
②配置数据库信息
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_db?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root
③配置mybatis相关配置
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml # mapper映射文件路径
  type-aliases-package: com.baidu.domain   # 配置哪个包下的类有默认的别名
  configuration:
    map-underscore-to-camel-case: true #开启驼峰标识转下划线
④编写Mapper接口

注意在接口上加上@Mapper注解

package com.baidu.mapper;

import com.baidu.domain.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {

    List<User> selectAll();
}

⑤编写mapper接口对应的xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.baidu.mapper.UserMapper">

    <select id="selectAll" resultType="com.baidu.domain.User">
        select * from t_user
    </select>

</mapper>
⑥测试
package com.baidu;

import com.baidu.domain.User;
import com.baidu.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelectAll() {
        List<User> userList = userMapper.selectAll();
        for (User user : userList) {
            System.out.println(user);
        }
    }
}

8.案例

注册登录案例

1.注册案例
1.1环境准备

SQL

CREATE TABLE `tbl_user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  `is_lock` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--mybatis启动器-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.21</version>
        </dependency>

    </dependencies>
1.2编写登录页面

注意,该页面需要先在resources文件夹下创建一个static目录(名字需要叫static)
在static目录下创建登录页面
目录结构如下
image.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>register</title>
</head>
<body>
    <form action="user/register" method="post">
        用户名:<input type="text" name="username"><br/>
        密 码:<input type="password" name="password"><br/>
        邮箱:<input type="text" name="email"><br/>
        <input type="submit" value="注册">
    </form>

</body>
</html>
1.3编写springboot配置文件
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_db?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml # mapper映射文件路径
  type-aliases-package: com.baidu.domain   # 配置哪个包下的类有默认的别名
  configuration:
    map-underscore-to-camel-case: true #开启驼峰标识转下划线

#记录日志
logging:
  level:
    com.baidu.mapper: debug


1.4编写mapper接口
package com.baidu.mapper;

import com.baidu.domain.User;
import org.apache.ibatis.annotations.Mapper;


@Mapper
public interface UserMapper {


    int insert(User user);
}

1.5编写mybatis映射文件

在resources文件夹下创建mapper目录
在mapper目录下创建UserMapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.baidu.mapper.UserMapper">
    <insert id="insert">
        insert into tbl_user(username, password, salt, email, is_lock)
        values (#{username}, #{password}, #{salt}, #{email}, #{isLock})
    </insert>

  

</mapper>
1.6编写service接口与实现类

接口

package com.baidu.service;

import com.baidu.domain.User;

public interface UserService {

    boolean register(User user);
}

实现类

package com.baidu.service.impl;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.MD5;
import com.baidu.domain.User;
import com.baidu.mapper.UserMapper;
import com.baidu.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import cn.hutool.core.util.RandomUtil;


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public boolean register(User user) {
        String password = user.getPassword();
        String salt = RandomUtil.randomString(16);
        String md5Password = SecureUtil.md5(password + salt);

        user.setSalt(salt);
        user.setPassword(md5Password);
        user.setIsLock(0);
        return userMapper.insert(user) == 1;
    }
}

1.7编写controller
package com.baidu.controller;

import com.baidu.domain.User;
import com.baidu.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;



    @RequestMapping("/register")
    @ResponseBody
    public boolean register(User user) {
        return userService.register(user);
    }
}

1.8测试

访问地址:http://localhost:8080/register.html进行测试

2.发送激活邮件案例
  • 邮件发送的基本过程与概念
    • 邮件服务器 :类似于现实生活中的邮局,它主要负责接收用户投递过来的邮件,并把邮件投递到邮件接收者的电子邮箱中
    • 电子邮箱 :用户在邮件服务器上申请的一个账户
      • from:xxx@xx.com  ----发件人
      • to:xxx@xx.com   ----收件人
      • subject:hello     ----主题
      • body: 欢迎来到压机否代码中心 -----内容体
  • 邮件传输协议
    • SMTP协议:全称为 Simple Mail Transfer Protocol,简单邮件传输协议。它定义了邮件客户端软件和SMTP邮件服务器之间,以及两台SMTP邮件服务器之间的通信规则
    • POP3协议:全称为 Post Office Protocol,邮局协议。它定义了邮件客户端软件和POP3邮件服务器的通信 规则
    • IMAP协议:全称为 Internet Message Access Protocol,Internet消息访问协议,它是对POP3协议一种扩展,也是定义了邮件客户端软件和IMAP邮件服务器的通信规则
  • 账号准备和配置

项目配置添加依赖

<!--发送邮件-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

配置文件

  #邮箱配置
  mail:
    host: smtp.163.com #发送邮件服务器
    username: 你的邮箱用户名 #发送邮件的邮箱地址
    password: WYLKDGJFXSCGOREL #客户端授权码,不是邮箱密码,网易的是自己设置的
    from: 你的邮箱用户名 # 发送邮件的地址,和上面username一致
    properties.mail.smtp.starttls.enable: true
    properties.mail.smtp.starttls.required: true
    properties.mail.smtp.ssl.enable: true
    default-encoding: utf-8

image.png
编写MailService以及实现

package com.baidu.component;

public interface MailService {

    /**
     * 发送邮件
     * @param to
     * @param subject
     * @param content
     */
    void sendMail(String to,String subject, String content);
}

具体实现

@Service
public class MailServiceImpl implements MailService {

    /**
     * Spring Boot 提供了一个发送邮件的简单抽象,直接注入即可使用
     */
    @Autowired
    private JavaMailSender mailSender;

    /**
     * 配置文件中的发送邮箱
     */
    @Value("${spring.mail.from}")
    private String from;

    @Override
    public void sendSimpleMail(String to, String subject, String content) {
        //创建SimpleMailMessage对象
        SimpleMailMessage message = new SimpleMailMessage();
        //邮件发送人
        message.setFrom(from);
        //邮件接收人
        message.setTo(to);
        //邮件主题
        message.setSubject(subject);
        //邮件内容
        message.setText(content);
        //发送邮件
        mailSender.send(message);   
    }

}

9.Web开发

9.1 静态资源访问

由于SpringBoot的项目是打成jar包的所以没有之前web项目的那些web资源目录(webapps)。
那么我们的静态资源要放到哪里呢?
从SpringBoot官方文档中我们可以知道,我们可以把静态资源放到 `resources/static`   (或者 `resources/public` 或者`resources/resources` 或者 `resources/META-INF/resources`) 中即可。
静态资源放完后,
例如我们想访问文件:resources/static/index.html  只需要在访问时资源路径写成/index.html即可。
例如我们想访问文件:resources/static/pages/login.html  访问的资源路径写成: /pages/login.html
9.1.1 修改静态资源访问路径
SpringBoot默认的静态资源路径匹配为/** 。如果想要修改可以通过 `spring.mvc.static-path-pattern` 这个配置进行修改。
例如想让访问静态资源的url必须前缀有/res。例如/res/index.html 才能访问到static目录中的。我们可以修改如下:

在application.yml中

spring:
  mvc:
    static-path-pattern: /res/** #修改静态资源访问路径
9.1.2 修改静态资源存放目录
我们可以修改 spring.web.resources.static-locations 这个配置来修改静态资源的存放目录。
例如:
spring:
  web:
    resources:
      static-locations:
        - classpath:/yjfstatic/ 
        - classpath:/static/

9.2 设置请求映射规则@RequestMapping

该注解可以加到方法上或者是类上。
我们可以用其来设定所能匹配请求的要求。只有符合了设置的要求,请求才能被加了该注解的方法或类处理。
9.2.1 指定请求路径
path或者value属性都可以用来指定请求路径。

例如:
我们期望让请求的资源路径为**/test/testPath的请求能够被testPath**方法处理则可以写如下代码

@RestController
@RequestMapping("/test")
public class HelloController {
    @RequestMapping("/testPath")
    public String testPath(){
        return "testPath";
    }
}
@RestController
public class HelloController {

    @RequestMapping("/test/testPath")
    public String testPath(){
        return "testPath";
    }
}
9.2.2 指定请求方式
method属性可以用来指定可处理的请求方式。

例如:
我们期望让请求的资源路径为**/test/testMethodPOST请求能够被testMethod**方法处理。则可以写如下代码

@RestController
@RequestMapping("/test")
public class TestController {

    @RequestMapping(value = "/testMethod",method = RequestMethod.POST)
    public String testMethod(){
        System.out.println("testMethod处理了请求");
        return "testMethod";
    }
}

注意:我们可以也可以运用如下注解来进行替换

  • @PostMapping    等价于   @RequestMapping(method = RequestMethod.POST)
  • @GetMapping     等价于   @RequestMapping(method  = RequestMethod.GET) 
    
  • @PutMapping     等价于   @RequestMapping(method  = RequestMethod.PUT) 
    
  • @DeleteMapping   等价于 @RequestMapping(method  = RequestMethod.DELETE) 
    

例如:
上面的需求我们可以使用下面的写法实现

@RestController
@RequestMapping("/test")
public class TestController {

    @PostMapping(value = "/testMethod")
    public String testMethod(){
        System.out.println("testMethod处理了请求");
        return "testMethod";
    }
}
9.2.3 指定请求参数
我们可以使用**params**属性来对请求参数进行一些限制。可以要求必须具有某些参数,或者是某些参数必须是某个值,或者是某些参数必须不是某个值。

例如:

我们期望让请求的资源路径为**/test/testParams**的**GET**请求,并且请求参数中**具有code参数**的请求能够被testParams方法处理。则可以写如下代码
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping(value = "/testParams",method = RequestMethod.GET,params = "code")
    public String testParams(){
        System.out.println("testParams处理了请求");
        return "testParams";
    }
}
如果是要求**不能有code**这个参数可以把改成如下形式
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping(value = "/testParams",method = RequestMethod.GET,params = "!code")
    public String testParams(){
        System.out.println("testParams处理了请求");
        return "testParams";
    }
}
如果要求有code这参数,并且这参数值必须**是某个值**可以改成如下形式
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping(value = "/testParams",method = RequestMethod.GET,params = "code=abc")
    public String testParams(){
        System.out.println("testParams处理了请求");
        return "testParams";
    }
}
如果要求有code这参数,并且这参数值必须**不是某个值**可以改成如下形式
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping(value = "/testParams",method = RequestMethod.GET,params = "code!=abc")
    public String testParams(){
        System.out.println("testParams处理了请求");
        return "testParams";
    }
}
9.2.4 指定请求头
我们可以使用**headers**属性来对请求头进行一些限制。

例如:

我们期望让请求的资源路径为**/test/testHeaders的**GET**请求,并且请求头中**具有**deviceType**的请求能够被testHeaders方法处理。则可以写如下代码
@RestController
@RequestMapping("/test")
public class TestController {
    
    @RequestMapping(value = "/testHeaders",method = RequestMethod.GET,headers = "deviceType")
    public String testHeaders(){
        System.out.println("testHeaders处理了请求");
        return "testHeaders";
    }
}
如果是要求不能有**deviceType**这个请求头可以把改成如下形式
@RestController
@RequestMapping("/test")
public class TestController {
    
    @RequestMapping(value = "/testHeaders",method = RequestMethod.GET,headers = "!deviceType")
    public String testHeaders(){
        System.out.println("testHeaders处理了请求");
        return "testHeaders";
    }
}
如果要求有deviceType这个请求头,并且其值必须**是某个值**可以改成如下形式
@RestController
@RequestMapping("/test")
public class TestController {
    
    @RequestMapping(value = "/testHeaders",method = RequestMethod.GET,headers = "deviceType=ios")
    public String testHeaders(){
        System.out.println("testHeaders处理了请求");
        return "testHeaders";
    }
}
如果要求有deviceType这个请求头,并且其值必须**不是某个值**可以改成如下形式
@RestController
@RequestMapping("/test")
public class TestController {
    
    @RequestMapping(value = "/testHeaders",method = RequestMethod.GET,headers = "deviceType!=ios")
    public String testHeaders(){
        System.out.println("testHeaders处理了请求");
        return "testHeaders";
    }
}
9.2.5 指定请求头Content-Type
我们可以使用**consumes**属性来对**Content-Type**这个请求头进行一些限制。
范例一
我们期望让请求的资源路径为**/test/testConsumes**的POST请求,并且请求头中的Content-Type头必须为 **multipart/from-data** 的请求能够被testConsumes方法处理。则可以写如下代码
    @RequestMapping(value = "/testConsumes",method = RequestMethod.POST,consumes = "multipart/from-data")
    public String testConsumes(){
        System.out.println("testConsumes处理了请求");
        return "testConsumes";
    }
范例二
如果我们要求请求头Content-Type的值必须**不能为某个multipart/from-data**则可以改成如下形式:
    @RequestMapping(value = "/testConsumes",method = RequestMethod.POST,consumes = "!multipart/from-data")
    public String testConsumes(){
        System.out.println("testConsumes处理了请求");
        return "testConsumes";
    }

9.3 获取请求参数

9.3.1 获取路径参数
RestFul风格的接口一些参数是在请求路径上的。类似: /user/1  这里的1就是id。

如果我们想获取这种格式的数据可以使用**@PathVariable  **来实现。
范例一
要求定义个RestFul风格的接口,该接口可以用来根据id查询用户。请求路径要求为  /user  ,请求方式要求为GET。

而请求参数id要写在请求路径上,例如  /user/1   这里的1就是id。

我们可以定义如下方法,通过如下方式来获取路径参数:
@RestController
public class UserController {

    @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
    public String findUserById( @PathVariable("id")Integer id){
        System.out.println("findUserById");
        System.out.println(id);
        return "findUserById";
    }
}
范例二
如果这个接口,想根据id和username查询用户。请求路径要求为  /user  ,请求方式要求为GET。

而请求参数id和name要写在请求路径上,例如  /user/1/zs   这里的1就是id,zs是name

我们可以定义如下方法,通过如下方式来获取路径参数:
@RestController
public class UserController {
    @RequestMapping(value = "/user/{id}/{name}",method = RequestMethod.GET)
    public String findUser(@PathVariable("id") Integer id,@PathVariable("name") String name){
        System.out.println("findUser");
        System.out.println(id);
        System.out.println(name);
        return "findUser";
    }
}
9.3.2 获取请求体中的Json格式参数
RestFul风格的接口一些比较复杂的参数会转换成Json通过请求体传递过来。这种时候我们可以使用**@RequestBody  **注解获取请求体中的数据。
9.3.2.1 配置
SpringBoot的web启动器已经默认导入了jackson的依赖,不需要再额外导入依赖了。
9.3.2.2 使用
范例一
要求定义个RestFul风格的接口,该接口可以用来新建用户。请求路径要求为  /user  ,请求方式要求为POST。

用户数据会转换成json通过请求体传递。
请求体数据

{"name":"啊哈","age":15}

1.获取参数封装成实体对象

如果我们想把Json数据获取出来封装User对象,我们可以这样定义方法:
@RestController
public class UserController {
    @RequestMapping(value = "/user",method = RequestMethod.POST)
    public String insertUser(@RequestBody User user){
        System.out.println("insertUser");
        System.out.println(user);
        return "insertUser";
    }
}
User实体类如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Integer age;
}

2.获取参数封装成Map集合

也可以把该数据获取出来封装成Map集合:
    @RequestMapping(value = "/user",method = RequestMethod.POST)
    public String insertUser(@RequestBody Map map){
        System.out.println("insertUser");
        System.out.println(map);
        return "insertUser";
    }
范例二
如果请求体传递过来的数据是一个User集合转换成的json,Json数据可以这样定义:
[{"name":"啊哈1","age":14},{"name":"啊哈2","age":15},{"name":"啊哈3","age":16}]
方法定义:
    @RequestMapping(value = "/users",method = RequestMethod.POST)
    public String insertUsers(@RequestBody List<User> users){
        System.out.println("insertUsers");
        System.out.println(users);
        return "insertUser";
    }
9.3.2.3 注意事项
如果需要使用**@RequestBody  **来获取请求体中Json并且进行转换,要求请求头 Content-Type 的值要为: application/json 。
9.3.3 获取QueryString格式参数
如果接口的参数是使用QueryString的格式的话,我们也可以使用SpringMVC快速获取参数。

我们可以使用**@RequestParam  **来获取QueryString格式的参数。
9.3.3.1 使用
范例一
要求定义个接口,该接口请求路径要求为  /testRequestParam,请求方式无要求。参数为id和name和likes。使用QueryString的格式传递。

1.参数单独的获取

如果我们想把id,name,likes单独获取出来可以使用如下写法:

在方法中定义方法参数,方法参数名要和请求参数名一致,这种情况下我们可以省略**@RequestParam  **注解。
    @RequestMapping("/testRquestParam")
    public String testRquestParam(Integer id, String name, String[] likes){
        System.out.println("testRquestParam");
        System.out.println(id);
        System.out.println(name);
        System.out.println(Arrays.toString(likes));
        return "testRquestParam";
    }
如果方法参数名和请求参数名不一致,我们可以加上**@RequestParam  **注解例如:
    @RequestMapping("/testRquestParam")
    public String testRquestParam(@RequestParam("id") Integer uid,@RequestParam("name") String name, @RequestParam("likes")String[] likes){
        System.out.println("testRquestParam");
        System.out.println(uid);
        System.out.println(name);
        System.out.println(Arrays.toString(likes));
        return "testRquestParam";
    }

2.获取参数封装成实体对象

如果我们想把这些参数封装到一个User对象中可以使用如下写法:
    @RequestMapping("/testRquestParam")
    public String testRquestParam(User user){
        System.out.println("testRquestParam");
        System.out.println(user);
        return "testRquestParam";
    }
User类定义如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Integer age;
    private String[] likes;
}
测试时请求url如下:
http://localhost:8080/testRquestParam?id=1&name=压机否&likes=编程&likes=旅游&likes=看书
**注意:实体类中的成员变量要和请求参数名对应上。并且要提供对应的set/get方法。**
9.3.4 相关注解其他属性
9.3.4.1 required
代表是否必须,默认值为true也就是必须要有对应的参数。如果没有就会报错。

如果对应的参数可传可不传则可以把其设置为fasle

例如:

    @RequestMapping("/testRquestParam")
    public String testRquestParam(@RequestParam(value = "id",required = false) Integer uid,@RequestParam("name") String name, @RequestParam("likes")String[] likes){
        System.out.println("testRquestParam");
        System.out.println(uid);
        System.out.println(name);
        System.out.println(Arrays.toString(likes));
        return "testRquestParam";
    }
9.3.4.2 defaultValue
如果对应的参数没有,我们可以用defaultValue属性设置默认值。

例如:

    @RequestMapping("/testRquestParam")
    public String testRquestParam(@RequestParam(value = "id",required = false,defaultValue = "777") Integer uid,@RequestParam("name") String name, @RequestParam("likes")String[] likes){
        System.out.println("testRquestParam");
        System.out.println(uid);
        System.out.println(name);
        System.out.println(Arrays.toString(likes));
        return "testRquestParam";
    }

9.4 响应体响应数据

无论是RestFul风格还是我们之前web阶段接触过的异步请求,都需要把数据转换成Json放入响应体中。
9.4.1 数据放到响应体
我们的SpringMVC为我们提供了**@ResponseBody  **来非常方便的把Json放到响应体中。

**@ResponseBody  **可以加在哪些东西上面?类上和方法上

具体代码请参考范例。
9.4.2 数据转换成Json
9.4.2.1 配置
SpringBoot项目中使用了web的start后,不需要进行额外的依赖和配置
9.4.2.2 使用
只要把要转换的数据直接作为方法的返回值返回即可。SpringMVC会帮我们把返回值转换成json。具体代码请参考范例。
9.4.3 范例
范例一
要求定义个RestFul风格的接口,该接口可以用来根据id查询用户。请求路径要求为  /response/user  ,请求方式要求为GET。

而请求参数id要写在请求路径上,例如   /response/user/1   这里的1就是id。

要求获取参数id,去查询对应id的用户信息(模拟查询即可,可以选择直接new一个User对象),并且转换成json响应到响应体中。
@Controller
@RequestMapping("/response")
public class ResponseController {

    @RequestMapping("/user/{id}")
    @ResponseBody
    public User findById(@PathVariable("id") Integer id){
        User user = new User(id, "压机否", 15, null);
        return user;
    }
}

9.5.Thymeleaf

1.入门案例

导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

建议关闭thymeleaf缓存

spring:
  thymeleaf:
    cache: false

编controller写代码

package com.baidu.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/thymeleaf")
public class ThymeleafController {

    @RequestMapping("/test01")
    public String test01(HttpServletRequest request){
        request.setAttribute("name","yjf");
        return "test";
    }
}

在src/main/resources 的 templates(注意:templates这单词别写错)下新建一个test.html页面用于展示数据
image.png
在html页面中引入
xmlns:th=“http://www.thymeleaf.org”
image.png

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    两个[]是thymeleaf的表达式之一,可以用来展示作用域中的值<br/>
    在Thymeleaf中存储到作用域中的值是<br/>
    [[${name}]]
    <br/>
    <br/>
    <br/>
    这种方式也可以展示作用域中的值
    <br/>
    <span th:text="${name}"></span>
</body>
</html>
2.变量表达式

语法:${} 或 *{}
作用:取出作用域中存放的变量的内容
用法:1.必须依赖于html标签的存在,不能单独写在页面上
2.引入thymeleaf名称空间后,配合th:进行使用
3.需要动态的获取后台的值的时候使用
案例:
controller代码

    @RequestMapping("/test02")
    public String test02(HttpServletRequest request){
        User user = new User();
        user.setId(1001L);
        user.setUsername("zs");
        user.setEmail("123@qq.com");
        request.setAttribute ("user",user);
        return "test";
    }

页面代码

   id:<span th:text="*{user.id}"></span>
   姓名:<span th:text="${user.username}"></span>
   邮箱:<input type="text" th:value="${user.email}">
3.内联表达式

语法:[[…]]
作用:取出作用域中存放的变量的内容

@RequestMapping("/test04")
public String test04(HttpServletRequest request){
List<User> userList = new ArrayList<>();
for(int i=1;i<=3;i++){
    User user = new User();
    user.setId((long)(1001+i));
    user.setUsername("zs"+i);
    user.setEmail("123"+i+"@qq.com");
    userList.add(user);
}
request.setAttribute ("userList",userList);
return "test02";
}

用法:不依赖于标签,可以直接写在页面上
案例:
controller代码

    @RequestMapping("/test03")
    public String test03(HttpServletRequest request){
        User user = new User();
        user.setId(1001L);
        user.setUsername("zs");
        user.setEmail("123@qq.com");
        request.setAttribute ("user",user);
        return "test";
    }

html代码

    [[${user.id}]]
    [[${user.username}]]
    [[${user.email}]]
4.循环

controller代码

    @RequestMapping("/test04")
    public String test04(HttpServletRequest request){
        List<User> userList = new ArrayList<>();
        for(int i=1;i<=3;i++){
            User user = new User();
            user.setId((long)(1001+i));
            user.setUsername("zs"+i);
            user.setEmail("123"+i+"@qq.com");
            userList.add(user);
        }
        request.setAttribute ("userList",userList);
        return "test02";
    }

html代码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>test01</title>
</head>
<body>
<table border="1">
    <thead>
    <tr>
        <td>序号</td>
        <td>id</td>
        <td>username</td>
        <td>email</td>
    </tr>
    </thead>

    <tr th:each="user,indexName:${userList}">
        <td th:text="${indexName.index+1}"></td>
        <td th:text="${user.id}"></td>
        <td th:text="${user.username}"></td>
        <td th:text="${user.email}"></td>
    </tr>

</table>

</body>
</html>
5.URL表达式

语法:@{…}
作用:表达式可用于

<a th:href="@{'delete?id='+${user.id}}">删除</a>
<a th:href="@{'edit?id='+${user.id}}">修改</a>

10.多环境配置

image.png
image.png

11.热部署

1、在pom.xml文件中添加devtools依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>

2、修改Settings -->如下
image.png
image.png
3、重启项目即可。
个人感觉自动编译不是那么好用,最好每次修改完代码之后自己手动编译一下
快捷键ctrl+F9或者点击,如果使用手动编译的方式,步骤2可以省略
image.png

12.拦截器

1.编写拦截器

package com.baidu.interceptor;

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

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("前置方法:MyInterceptor.preHandle执行");
        return false;// 默认false,表示不放行。true表示放行。
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("后置方法:MyInterceptor.postHandle执行");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("完成时方法:MyInterceptor.afterCompletion执行");
    }
}

2.注册拦截器

package com.baidu.config;

import com.baidu.interceptor.MyInterceptor;
import org.springframework.context.annotation.Bean;
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 MvcConfig implements WebMvcConfigurer {
    // 注册拦截器
    @Bean
    public MyInterceptor myInterceptor() {
        return new MyInterceptor();//创建一个新的MyInterceptor实例并返回
    }

    // 将拦截器添加到拦截器链,指定拦截规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor())  // 添加拦截器到registry中
                .addPathPatterns("/**")  // 拦截所有路径
                .excludePathPatterns("/index.html");  // 不拦截/index路径
    }
}

生命周期方法
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 是拦截器中的另一个方法,它的执行时机和主要用途如下:

  1. preHandle 是在请求被处理之前执行的,即在控制器(Controller)方法执行之前。这使得您可以在请求进入控制器之前对其进行一些处理,例如检查用户的身份认证状态,携带的令牌有效性,或者对传入的请求数据进行一些预处理。

postHandleafterCompletion 相比,preHandle 的主要区别是它的执行时机:preHandle 是在请求被处理之前执行的,而 postHandleafterCompletion 分别在控制器方法执行之后,生成视图之前和整个请求完成之后执行。
preHandle 方法中,可以通过返回 truefalse 来决定是否继续处理请求。如果返回 true,请求将继续被处理,接下来会执行控制器方法和后续的拦截器方法;如果返回 false,请求处理将停止,可以在该方法中设置响应的状态码或者添加响应头来告知客户端请求被拒绝的原因。
2.postHandleafterCompletion 都是拦截器的方法,但它们在请求处理周期中的执行时机和主要用途是不同的。

  1. postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView): 这个方法是在控制器(Controller)的方法执行之后,生成视图之前执行的。在 postHandle 方法中,您可以对控制器的方法执行结果进行一些额外的处理,例如添加额外的模型数据到视图,或者改变要渲染的视图。需要注意的是,当控制器方法抛出异常时,这个方法不会被执行。
  2. afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex): 这个方法是在整个请求完成之后执行的,也就是在视图渲染之后。这是Spring MVC生命周期中的最后一步,适合用于清理工作,如清理资源,记录日志,处理请求过程中产生的异常等。需要注意的是,不论之前的操作结果如何(成功,异常),只要进入到了 DispatcherServlet,这个方法都会被调用。

总结一下,postHandleafterCompletion 的主要区别在于它们的执行时机和主要用途:postHandle 是在控制器的方法执行之后,生成视图之前执行的,适合进行某些额外的处理;而 afterCompletion 是在整个请求完成之后执行的,适合进行资源清理和异常处理。

13.同源策略与跨域

13.1架构演进

13.1.1单体架构
  • 前后端都部署在同一台服务器上(前后端代码都在同一个应用中)
  • 缺点:对静态资源的访问压力也会落在Tomcat上

image.png

13.1.2前后端分离
  • 前后端分离:前端和后端分离开发和部署(前后端部署在不同的服务器)
  • 优点:1.将对静态资源的访问和对接口的访问进行分离,Tomcat服务器只负责数据服务的访问
    2.提高工作效率,分工加明确。前端只关注前端的事,后台只关心后台的活,两者开发可以同时进行
    3.彻底解放前后端。前端不再需要向后台提供模板,后台不需要在前端HTML中嵌入后台代码农

image.png

13.2登录认证存在的方式

13.2.1基于session实现单体项目用户认证

在单体项目中如何保证受限资源在用户未登录的情况下不允许访问?
image.png
在单体项目中,视图资源(页面)和接口(控制器)都在同一台服务器,用户的多次请求都是基于同一个会话(session),因此可以借助session来进行用户认证判断:
1.当用户登录成功之后,将用户信息存放到session
2.当用户再次访问受限资源时,验证session中是否存在用户信息,可以根据session有无用户信息来判断用户是否登录

13.2.2基于token实现前后端分离用户认证

由于在前后端分离项目开发中,前后端之间是通过异步交互完成数据访问的,因为前后端分离项目涉及跨域问题,因此不能基于session实现用户的认证。(即使可以,实现起来比较麻烦)
image.png
以下用两个案例来演示单体项目以及前后端分离项目的认证过程

13.3单体项目与前后端分离项目认证案例

13.3.1.单体项目认证过程

建议创建新项目完成
导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.baidu</groupId>
    <artifactId>springboot_project_02</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.21</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>

    </dependencies>

</project>

1.在resources文件夹下创建public文件夹,并编写index.html页面,login.html页面
index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
主页
</body>
<script>
    axios({
        method: 'get',
        url: '/user/1001',
    }).then(function (response) {
        console.log(response.data);
    });
</script>
</html>

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<form>
    用户名:<input type="text" id="username">
    密码:<input type="password" id="password">
    <input type="button" value="登录" onclick="doLogin();">
</form>
</body>

<script>
    function doLogin() {
        axios({
            method: 'post',
            url: '/user/login',
            data: {
                'username':  document.getElementById("username").value,
                'password': document.getElementById("password").value
            }
        }).then(function (response) {
            console.log(response);
            if (response.data.code == 200) {
                location.href = "index.html";
            } else {
                alert(response.data.message)
            }
        })
    }
</script>


</html>

编写实体类User

package com.baidu.domain;

import lombok.Data;

@Data
public class User {

    private Long id;
    private String username;
    private String password;

    public User() {
    }

    public User(Long id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }
}


编写用于登录的controller

package com.baidu.controller;

import com.baidu.domain.User;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController {
    /**
     * 模拟登录
     *
     * @param user
     * @param session
     * @return
     */
    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody User user, HttpSession session, ModelAndView modelAndView) {
        Map<String, Object> map = new HashMap<>();
        //模拟用户登录成功
        if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) {
            //保存用不登录信息
            User dbUser = new User(1001L, "admin", "123465");
            session.setAttribute("user", dbUser);
            map.put("code", 200);
            map.put("message", "登录成功");
        } else {
            //跳转到登录页面
            map.put("code", 500);
            map.put("message", "失败");
        }
        return map;
    }

    /**
     * 根据id查询用户信息
     * @param id
     * @param session
     * @return
     */
    @GetMapping("/{id}")
    public Map<String, Object> getById(@PathVariable("id") Long id, HttpSession session) {
        Map<String, Object> map = new HashMap<>();
        if (1001L == id) {
            User user = (User) session.getAttribute("user");
            System.out.println("user===>"+user);
            map.put("code", "200");
            map.put("message", "查询成功");
            map.put("data",user);
        } else {
            map.put("code", "500");
            map.put("data", null);
            map.put("message", "没有这个用户");
        }
        return map;
    }
}

编写拦截器

package com.baidu.interceptor;

import com.alibaba.fastjson.JSONObject;
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 LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("user");
        //如果用户没有登录
        if (null == user) {
            //跳转登录页面
            Map<String, Object> map = new HashMap<>();
            map.put("code", "500");
            map.put("message", "请先登录");
            response.setContentType("text/json;charset=UTF-8");
            response.getWriter().println(JSONObject.toJSONString(map));
            return false;
        }
        return true;
    }
}

配置拦截器

package com.baidu.config;

import com.baidu.interceptor.LoginInterceptor;
import com.baidu.interceptor.MyInterceptor;
import org.springframework.context.annotation.Bean;
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 MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns("/user/login", "/login.html")//放行
                .addPathPatterns("/**");//拦截
    }
}

测试:
在登录之前访问index.html页面,可以发现 会被拦截,进而被重定向到登录页面,
登录之后再次测试,可以发现无论打开多少个窗口访问主页,均不会被拦截

  • 第一访问服务器的时候,服务器会生成一个session用于记录会话
  • 服务器生成session时,会把id通过cookie的形式发送给浏览器
  • 浏览器在关闭之前(默认情况下)每次打开新的窗口,都会携带该cookie来访问服务器
  • 根据jsssioni的找到服务器生成的session
13.3.2前后端分离认证过程

1.把上一个案例的index.html与login.html代码拷贝到nginx的html目录下
image.png
2.修改index.html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
主页
</body>
<script>
    axios({
        method: 'get',
        url: 'http://localhost:81/user/1001',
    }).then(function (response) {
        console.log(response.data);
    });
</script>
</html>

3.修改login.html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<form>
    用户名:<input type="text" id="username">
    密码:<input type="password" id="password">
    <input type="button" value="登录" onclick="doLogin();">
</form>
</body>

<script>
    function doLogin() {
        axios({
            method: 'post',
            url: 'http://localhost:81/user/login',
            data: {
                'username':  document.getElementById("username").value,
                'password': document.getElementById("password").value
            }
        }).then(function (response) {
            console.log(response);
            if (response.data.code == 200) {
                location.href = "index.html";
            } else {
                alert(response.data.message)
            }
        })
    }
</script>


</html>

4.在该目录执行cmd命令
image.png
5.输入nginx然后回车
image.png

6.访问页面进行测试
访问地址:http://localhost/index.html
效果:
image.png

13.4SpringBoot解决跨域问题

在MvcConfig添加如下配置
image.png

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")//放行哪些域名
                .allowedMethods("*")//放行什么样的请求
                .maxAge(3600)   // 设置预检请求的有效时间, 根据需要自定义
                .allowCredentials(true); // 允许跨域发送cookies
    }

在index.html页面中添加

withCredentials: true,

image.png
在login.html页面中添加

withCredentials: true,

image.png

13.5同源策略与跨域相关概念

子资源
子资源是一个嵌入到HTML文档中的的HTML元素。在1993年,引入了第一个子资源。通过引入,网页变得更漂亮,而且更复杂。
当浏览器渲染一个带有的页面,它必须从一个域获取该子资源。当把引入到Web的世界,很快 web中便有了

域与跨域
域(origin)是由三部分组成的标识:协议、完整的主机名和端口。例如: http://example.comhttps://example.com是不同的源,一个使用http协议,另一个使用 https协议。并且http默认端口是80,而https 协议端口默认是443。因此在这个示例中,尽管主机地址是相同的,但是协议和端口不同。如果组成域的三部分有一个不同,则域不相同。下表给出了与URL的源进行对比的示例:具体可以参考浏览器的同源策略 - Web 安全 | MDN (mozilla.org)

URL结果原因
http://store.company.com/dir2/other.html同源只有路径不同
http://store.company.com/dir/inner/another.html同源只有路径不同
https://store.company.com/secure.html失败协议不同
http://store.company.com:81/dir/etc.html失败端口不同 ( http:// 默认端口是 80)
http://news.company.com/dir/other.html失败主机不同

跨域请求是指例如:我们访问https://example.com时,同时访问另一个域的子资源http://example.com/posts/animal.png
跨域的风险
现在假设有两个网站a.com和b.com,a.com是我们的网站,假设是一个电商类型或者公司的网站后台,需要登录之后才可以交易,并且凭证存储到Cookie 中。而在b.com这个网站嵌入了一些特殊的脚本,它尝试去读取a.com下的Cookie,如果当前浏览器并没有任何的跨域限制,那么 b.com读取到a.com在浏览器存储的Cookie信息后,就可以获取当前登录到a.com网站的用户信息、发送消息、购买商品、删除内容等等。
image.png
同源策略
同源策略通过阻止访问不同源的资源来防止跨域攻击。该策略仍然运行某些标签(比如)嵌入不同源的资源。即使没有同源策略明确的规范,现代浏览器都会以某种形式实现。互联网工程任务组(IETF)在RFC6454中制定了同源策略的相关规范。
不允许被跨域访问的资源:本地存储. IndexedDB.Cookie·AJAX(XMLHttpRequest)同源策略解决了很多问题,但限制性很强。在单页应用程序和大量媒体网站的时代,同源并没有放松和微调这些规则。
跨域解决方案
1.JSONP
2.反向代理
3.CORS
我们在上面使用的方式是CORS的解决方案

13.6CORS

CORS简介
CORS(跨域资源共享),是一种基于HTTP头的机制,该机制允许我们使用AJAX发送跨域请求,只要HTTP响应头中包含了相应的CORS响应头。CORS需要浏览器和服务器同时支持,CORS机制在老版本的浏览器中不支持,现代浏览器都支持CORS。在使用CORS发送AJAX请求时浏览器端代码和过去一致,服务器端需要配置CORS的响应头。官方文档: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
简单请求

不会发起CORS预检请求的请求,称为简单请求。满足下列要求的请求都是简单请求
1.使用下面的请求方式
	GET
	POST
	HEAD
2.请求头中只包含
     Accept
     Accept-Language
     Content-Language 
3.ContentType的取值只能包含下面三种之一
     text/plain
     multipart/form-data
     application/x-www-form-urlencoded

非简单请求
当发送的请求方式是PUT / DELETE或者请求头中的Content-Type值为application/json时,不再是简单请求,文档上称为:非简单请求
非简单请求在发送真正的请求之前首先会发送一个OPTIONS请求,这个请求我们叫做预检请求(preflight),浏览器会问下服务器是否允许当前源访问,然后再检查当前的请求方式以及请求头中的特殊字段是否被允许(token,application/json)。只有服务器配置了允许当前的请求方式以及特殊的请求头,浏览器才会发出所需的请求。

小结:

  • 1.前后端分离项目会导致平时我们正常的登录认证出现问题
  • 2.问题出现的原因是因为浏览器的同源策略导致cookie跨域问题(当然市面上有cookie跨域的解决方案,需要做一些配置,我们这里不做介绍)
  • 3.使用cors进行跨域,有简单请求与非简单的请求,如果一个请求是非简单请求,在发送真正的请求之前,会先发送一个预检请求。(重点!)

14.JWT

14.1 为什么使用JWT
  • 1.因为cookie存在跨域的问题,虽然市面上有解决的方案,更重要的是,在不支持cookie的场景中,我们还得自己实现cookie、session机制,这就比较麻烦。例如 微信小程序不支持cookie机制(可以自己实现),物联网设备、APP,这些不是基于浏览器的认证,本身就没有cookie。
  • 2.在分布式项目中,通常我们会做集群,此时用传统的cookie、session的登录方案,需要实现session共享,如果使用JWT则不存在这个问题。(不过需要考虑JWT失效的以及黑名单的情况)
14.2 什么是JWT

官网:https://jwt.io/
博客:https://www.cnblogs.com/fengzheng/p/13527425.html
jwt认证流程
image.png

14.3 JWT使用入门

1.导入依赖

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

编写Jwt工具类
密钥

private static final String SECRET = "secret@!123";

编写创建token的方法

public static String createToken() {
    try {
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.MINUTE,60*2);
        Algorithm algorithm = Algorithm.HMAC256(SECRET);
        String token = JWT.create()
            .withClaim("name", "zs")
            .withClaim("id", 1001L)
            .withExpiresAt(instance.getTime())
            .sign(algorithm);
        return token;
    } catch (JWTCreationException exception) {
        throw exception;
    }
}

编写校验token的方法

public static boolean verify(String token){
    try {
        Algorithm algorithm = Algorithm.HMAC256(SECRET);
        JWTVerifier verifier = JWT.require(algorithm)
            .build();
        DecodedJWT jwt = verifier.verify(token);
        String payload = jwt.getPayload();
        String name = jwt.getClaim("name").asString();
        long id = jwt.getClaim("id").asLong();
        System.out.println(id);
        System.out.println(name);
        return true;
    } catch (JWTVerificationException exception){
        return false;
    }
}
14.4 结合前端工程,使用jwt认证

1.修改token工具类,添加createToken的重载方法以及getPayload方法

 /**
     * 根据id与username生成token
     * @param id
     * @param username
     * @return
     */
    public static String createToken(long id, String username) {
        try {
            Calendar instance = Calendar.getInstance();
            instance.add(Calendar.MINUTE,60*2);
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            String token = JWT.create()
                    .withClaim("username", username)
                    .withClaim("id", id)
                    .withExpiresAt(instance.getTime())
                    .sign(algorithm);
            return token;
        } catch (JWTCreationException exception) {
            throw exception;
        }
    }

    /**
     * 获取token中payload
     * @param token
     * @return
     */
    public static DecodedJWT getPayload(String token) {
        return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    }

2.修改UserController的login方法,登录成功,生成token

    /**
     * 模拟登录
     *
     * @param user
     * @param session
     * @return
     */
    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody User user, HttpSession session) {
        Map<String, Object> map = new HashMap<>();
        //模拟用户登录成功
        if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) {
            //生成token 返回给客户端
            String token = JwtUtils.createToken(1001L,user.getUsername());
            map.put("data",token);
            map.put("code", 200);
            map.put("message", "登录成功");
        } else {
            //跳转到登录页面
            map.put("code", 500);
            map.put("message", "失败");
        }
        return map;
    }

3.修改UserController的getById方法

    /**
     * 根据id查询用户信息
     *
     * @param id
     * @param
     * @return
     */
    @GetMapping("/{id}")
    public Map<String, Object> getById(@PathVariable("id") Long id, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<>();
        //从作用域中获取id 与 username
        long tokenId = (long) request.getAttribute("id");
        String username = (String) request.getAttribute("username");
        //如果作用域中的id与用户传递过来的id相同,则从作用域中获取用户信息 返回(不需要在查询数据库)
        if (tokenId == id) {
            map.put("data", new User(id, username));
            map.put("code", "200");
            map.put("message", "查询用户成功");
        } else {
            map.put("code", "500");
            map.put("data", null);
            map.put("message", "没有这个用户");
        }
        return map;
    }

4.修改LoginInterceptor类,校验token是否合法


public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String, Object> map = new HashMap<>();
        if ("OPTIONS".equals(request.getMethod())) {
            System.out.println("跨域免检");
            return true;
        }
        String token = request.getHeader("token");
        //如果token合法
        if (token == null) {
            map.put("code", "500");
            map.put("message", "请先登录");
            response.setContentType("text/json;charset=UTF-8");
            response.getWriter().println(JSONObject.toJSONString(map));
            return false;
        }
        boolean flag = JwtUtils.verify(token);
        if (!flag) {
            map.put("code", "500");
            map.put("message", "无效的token");
            response.setContentType("text/json;charset=UTF-8");
            response.getWriter().println(JSONObject.toJSONString(map));
            return false;
        }
        Long id = JwtUtils.getPayload(token).getClaim("id").asLong();
        String username = JwtUtils.getPayload(token).getClaim("username").asString();
        //把token中存储的数据获取出来,保存到作用域中,以便后续方法方便 使用
        request.setAttribute("id", id);
        request.setAttribute("username", username);
        return true;
    }
}

5.修改前端工程,index.html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
主页
<script>
    axios.get('http://localhost/user/1001', {
        headers: {'token': localStorage.getItem("token")}
    })
    .then(response => {
        console.log(response.data)
    });
</script>
</body>
</html>

6.修改前端工程,login.html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<form>
    用户名:<input type="text" id="username">
    密码:<input type="password" id="password">
    <input type="button" value="登录" onclick="doLogin();">
</form>
</body>

<script>
    function doLogin() {
        axios({
            method: 'post',
            url: 'http://localhost/user/login',
            data: {
                'username': document.getElementById("username").value,
                'password': document.getElementById("password").value
            }
        }).then(function (response) {
            console.log(response.data);
            if (response.data.code == 200) {
                localStorage.setItem("token", response.data.data);
                location.href = "index.html";
            } else {
                alert(response.data.message);
            }
        }).catch(function (error) {
            console.error(error);
        });
    }
</script>


</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值