Spring-back-mini

后端小项目


吐槽 :
这个项目好难,还只是一个小的项目,很难想像大项目岂不是让我崩了,写了一堆不知道是啥的东西 ,本质就是一个登录的界面和注册,以及修改密码和管理的项目。
为啥感觉我这么简单的东西实现起来我真的想骂人啊!!!!真服了,现在找个项目练习都是怎么难的吗??我服了

spring 的理解:

  • spring 就是一个框架 -- 完整的工具箱子,里面有各种各样的工具
  • 管理对象的工具 -- Ioc容器
  • 管理事务的工具 -- Transaction
  • 管理数据库工具 -- JDBC
  • 管理安全的工具 -- Security
  • 托管Web工具 -- Spring MVC

SpringBoot 就是启动器 -- 给spring 这个工具箱添加的一个助手

  • springboot 就是自己内嵌了 Tomcat 不用自己装服务器
  • 自动配好了视图解析器,可以在application.yml中自己配置
  • springboot 中的pom.xml是用来存放框架的,需要什么框架自己手动添加
  • 启动springboot是在主函数中添加注解 @SpringBootApplication 的main 方法就可以直接运行了
  • spring-boot-starter-web这个依赖已经把spring mvc的所有jar包和默认配置全部打包好了

Redis 的配置

  • redis 在存数据的安按照配置文件的形式存储
// redis 的配置文件类
@Configuration
public class RedisConfig {

    @Bean // 将这个方法返回的对象 redisTemplate 交给 spring 容器管理,String 对应的就是key 类型, Object 对应的是 value型
    public RedisTemplate<String , Object> redisTemplate(

            // 这个是 spring boot 自动配置好的redis 连接工厂,包含了redis 需要连接的端口、密码等信息,将参数传递进去之后spring 自动会将connectionFactory 注入进去
            @Autowired RedisConnectionFactory connectionFactory
            ){

        // 这个是对 Key 转为普通的字符串,作用是在redis 看见的数据是明文的形式,而不是乱码的
        StringRedisSerializer keySerializer = new StringRedisSerializer();

        //这个是专门负责将Value 转换为Json格式的序列化器, Generic 是任意的意思,表示任意类型通用
        GenericJackson2JsonRedisSerializer valueSerializer =
                new GenericJackson2JsonRedisSerializer();

        // 创建一个空的 RedisTempalte 实例
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // 把连接工厂设置进去 ,相当于告诉 redisTemplate 去操作redis的时候,用这个 地址和密码连接,如果没有是连接不上redis 的
        redisTemplate.setConnectionFactory(connectionFactory);
        // 修改序列化方式 为 json 格式
        // Redis 有两种数据结构 : 普通键值对 String - String 类型 :Key 用 keySerializer , value 用的是 valueSerializer
        // Hash 结构 (类似java 的 hashMap): hash的字段名用 hashKeySerializer , 字段值用是: hashValueSerializer
       // 下面的语法也就是固定的
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setValueSerializer(valueSerializer);
        redisTemplate.setHashKeySerializer(keySerializer);
        redisTemplate.setHashValueSerializer(valueSerializer);

        return redisTemplate;
    }
}

WebConfig的配置

@Configuration   // 配置类在 spring 启动的时候会读取它的内容
@RequiredArgsConstructor // Lombok 提供的注解 ,自动生成构造器。为所有的 final 字段生成构造函数
public class WebConfig implements WebMvcConfigurer {

    // 定义的一个拦截器,用来校验用户是否登录、session是否有效
    private final SessionInterceptor sessionInterceptor;

    @Bean // 将返回的对象交给 spring 容器管理

    // FilterRegistrationBean 是 spring boot 提供的一个注册器, 手动注册  Filter 过滤器,可以控制它的执行顺序和拦截路径
    public FilterRegistrationBean<AuthSessionFilter> registrationBean(

            // 按照名称"authSessionFilter"注入。因为 spring 容器中肯能有多个 AuthSessionFilter 类型的 Bean ,这里明确指定
            @Qualifier("authSessionFilter") AuthSessionFilter authSessionFilter
            //  AuthSessionFilter 也是自定义过滤器用来处理 Session认证的
    ){

        // 创建一个注册器实例, 把 authSessionFilter 放进去
        FilterRegistrationBean<AuthSessionFilter> registrationBean
                = new FilterRegistrationBean<>(authSessionFilter);

        // 设置拦截路径:这个过滤器只对/user/* 路径下的请求生效也就是在/user/下的所有路径都生效
        registrationBean.addUrlPatterns("/user/*");

        // 将配置好的注册器返回给 Spring 容器 , spring boot 会自动读取它并且把 AuthSessionFilter 注册到Servlet 容器中
        return registrationBean;
    }


    // 配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(sessionInterceptor) /// 把 SessionInterceptor注册到拦截器中
                .addPathPatterns("/**")  // 拦截所有的路径
                .excludePathPatterns("/login" , "/register"); // 排除/login 和 /register 接口,也就是他们不需要经过Session校验,因为用户还没有登录,无法校验
    }
}

FilterInterceptor 的区别

对比维度 Filter(过滤器) Interceptor(拦截器)
属于谁 Servlet 容器(Tomcat、Jetty 等)
执行时机 请求进入 Servlet 之前,最外层 请求进入 Controller 之前,更靠近业务
拦截粒度 只能拦截 URL 路径(/user/*) 可以更精细(按路径、按请求方法)
能否获取 Spring Bean 能,但需要额外步骤(通过 WebApplicationContextUtils) 直接能,因为拦截器本身就是 Spring Bean,可以 @Autowired 其他 Bean
能否处理 ModelAndView 不能,只能在请求前后做处理 可以,可以在 postHandle 中修改返回的视图或数据
典型用途 字符编码、登录鉴权(JWT 校验)、日志记录、压缩响应 权限校验、Session 校验、性能监控、访问日志

统一的响应 遵守HTTP协议

// 后端统一处理接口
// 后端给前端返回数据是使用的统一格式,使得每次操作访问的时候后端都可以返回清晰的结构用户排查
@Setter // Lombok注解 自动生成setter 和 getter 方法
@Getter
public class R<T> {
    private int code; // 200 400 500
    private String message; // 消息 说明
    private boolean success; // 是否成功
    private T data; // 数据

    private static final Map<String, Object> PRESENT = Collections.EMPTY_MAP;

    public R(int code, String message, boolean success, T data) {
        this.code = code;
        this.message = message;
        this.success = success;
        this.data = data;
    }

    public R() {
    }

    public static <T> R<T> success(T data) {
        return new R<>(200, "success", true, data);
    }

    public static R<?> success() {
        return new R<>(200, "success", true, PRESENT);
    }

    public static R<?> fail(int code, String message) {
        return new R<>(code, message, false, PRESENT);
    }

    public static R<?> fail(String message) {
        return new R<>(500, message, false, PRESENT);
    }

    public static R<?> unauthoried(String message) {
        return new R<>(401 , message , false , PRESENT);
    }


}

统一异常处理


// 这个部分是在统一接口之后的最后一道防线,会拦截所有 Cotroller 层抛出的异常
@RestControllerAdvice // 给所有的Contrller层 装一个 监控,任何 controller层抛出的异常这个地方统一处理
public class GlobalExceptionHandler {

    @ExceptionHandler //异常处理器
    
    // 设置返回HTTP响应的状态码
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)  // 500
    // 这个确保返回的异常是符合HTTP 协议的,这样不会乱码
    public R<?> handlerException(Exception e){
        return R.fail(e.getMessage());
    }

}

第一个功能: 注册

  • 测试工具 Postman
  • 用户名需要唯一: redis 的快速失败机制 + 唯一约束
  • 密码加密 (bcrypt 加密)
  • 用户输入 JSON格式的 username 和password; 所以需要在 pojo 中增加一个 dto 层用户存放注册的数据
  • 保存到 tb_user 表格中
// 负责处理相关的 http 请求
@RestController   // 将所有的方法返回成 json的格式
@RequiredArgsConstructor  // 对所以被 final 修饰的对象自动创建构造函数
public class RegisterController {

    private final UserService userService;

    @PostMapping("/register")   // 指定 http 为 post 请求

    // @RequestBody 将前端提交的 json 自动转换成 UserCreateDto 对象
    public R<?> register(@RequestBody UserCreateDto dto) throws Exception{

        // 将 dto 属性复制到 User 实体,也就是将对象数据转为数据库的实体对象
        User user = BeanUtil.copyProperties(dto , User.class);
        
        // Bcrypt 加密密码 ,防止彩虹表的攻击
        user.setPassword(DigestUtil.bcrypt(dto.getPassword()));

        // 设置当前时间为创建的时间
        user.setCreateAt(LocalDateTime.now());

        // 调用服务层保存对象
        userService.saveUser(user);
        
        // 返回统一成功响应
        return R.success();
    }
}
服务层结构是业务逻辑实现
@Service // 被spring 管理
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    // 数据访问层 执行 sql 语言操作
    private final UserMapper userMapper;

    // redis 客户端 ,用于 分布锁
    private final RedisTemplate<String , Object> redisTemplate;

    // reids 锁的值
    private final static String PRESENT = "SAVE USER";

    
     // redis 防止并发重复注册 isUse 后面的就是redis 的分布锁 30 秒之内不可以重复注册。30 秒之后mysql中的唯一约束发力,
    @Override
    public void saveUser(User user) {

        // setIfAbsent = SETNX ;如果 key 值不存在就设置,存在就返回false, key 存储的是用户名

        boolean isUse = redisTemplate.opsForValue().
                setIfAbsent(user.getUsername() , PRESENT,30 , TimeUnit.SECONDS);

        // 用户存在就说名已经注册过了
        if (!isUse ){
            throw new RuntimeException("用户已经注册过了!!!!!!");
        }


        // 用户名字唯一 : 可以在数据的 username 列创建一个唯一约束
        // 插入数据库 , 对应的数据库 username 列需要建立 唯一索引作为保障
        int userId = userMapper.insert(user);

        System.out.println(userId); // 这个是用来调试输出的,在日志中比较重要
    }

    @Override
    // 根据用户名来查询用户 (用于登录验证)
    public User findUserByUsername(String username) throws Exception {
        return userMapper.selectByUsername(username);
    }


    // 分页功能对应的参数是(当前是第几页, 每页的条数 ,  查询的条件),这个查询的条件就是一个限制
    @Override
    public DataGrid<UserInfoVo> findList(int current, int count, UserQueryDto dto) throws Exception {

        // 封装分页结果对象
        DataGrid<UserInfoVo> dataGrid = new DataGrid<>();

        // PageHelper 启动分页: 当前页和每页的条数,会连接后续第一条 sql 自动拼接 LIMIT
        PageHelper.startPage(current , count);

        // 查询用户列表(条件查询, dto 中的非空字段会作为查询条件)
        List<UserInfoVo> users = userMapper.selectListSelective(dto);

        // PageInfo 封装分页信息 (总条数 , 总页数 , 每条数据都是users )
        PageInfo<UserInfoVo> info = new PageInfo<>(users);
        
        dataGrid.setTotal(info.getTotal());
        dataGrid.setPages(info.getPages());
        dataGrid.setRows(info.getList());
        return dataGrid;
    }
    
    
    // 根据主键更新用户信息
    @Override
    public void updateUser(User user) throws Exception {
        userMapper.updateById(user);
    }
    
    // 根据 用户的id 物理删除
    @Override
    public void remove(int userId) throws Exception {
        userMapper.deleteById(userId);
    }
}
mapper 层
// 本质上就是一个
@Mapper
public interface UserMapper {

    int insert (User user);

    User selectByUsername(String username);

    List<UserInfoVo> selectListSelective(UserQueryDto dto);

    void updateById(User user);

    void deleteById(int userId);
}

  • mapper配置可以直接调用接口
<?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.iweb.user.mapper.UserMapper">
    <resultMap id="BaseResultMap" type="com.iweb.user.domain.User">
        <id property="userId" column="user_id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="age" column="age"/>
        <result property="amount" column="amount"/>
        <result property="birth" column="birth"/>
        <result property="createAt" column="create_at"/>
    </resultMap>

    <insert id="insert" useGeneratedKeys="true" keyColumn="user_id">
        insert into tb_user (username, password, create_at)
        values (#{username}, #{password}, #{createAt})
    </insert>

    <select id="selectByUsername">
        select *
        from tb_user
        where username = #{username}
    </select>

    <select id="selectListSelective"
            parameterType="com.iweb.user.domain.dto.UserQueryDto"
            resultType="com.iweb.user.domain.vo.UserInfoVo">
        select u.user_id, u.username, u.age, u.birth, u.amount, u.birth, u.create_at
        from tb_user u
        <where>
            <trim suffixOverrides="and">
                <if test="age !=null">
                    u.age = #{age} and
                </if>
                <if test="amount != null">
                    u.amount = #{amount} and
                </if>
                <if test="birth != null">
                    u.birth = #{birth} and
                </if>
            </trim>
        </where>
    </select>

    <update id="updateById" parameterType="com.iweb.user.domain.User">
        update tb_user u
        <set>
            <if test="age !=null">
                u.age = #{age} ,
            </if>
            <if test="amount != null">
                u.amount = #{amount} ,
            </if>
            <if test="birth != null">
                u.birth = #{birth} ,
            </if>
        </set>
        where u.user_id = #{userId}
    </update>

    <delete id="deleteById" >
        delete
        from tb_user
        where user_id = #{userId}
    </delete>
</mapper>

Spring Security

  • 结构树
src
└── main
    ├── java
    │ └── com.iweb.user
    │   ├── commons
    │   │   └── Constant.java          # 常量类
    │   ├── config
    │   │   ├── AppConfig.java         # 项目自定义配置类
    │   │   ├── RedisConfig.java       # Redis配置
    │   │   └── WebConfig.java         # WebMvc配置
    │   ├── controller
    │   │   ├── LoginController.java   # 登录接口
    │   │   ├── RegisterController.java# 注册接口
    │   │   └── UserController.java    # 用户CRUD接口
    │   ├── domain
    │   │   ├── dto
    │   │   │   ├── UserCreateDto.java # 创建用户接收参数
    │   │   │   ├── UserEditDto.java   # 修改用户接收参数
    │   │   │   ├── UserLoginDto.java  # 登录接收参数
    │   │   │   └── UserQueryDto.java  # 用户查询分页条件
    │   │   └── vo
    │   │       ├── UserInfoVo.java    # 返回前端用户信息视图对象
    │   │       ├── DataGrid.java      # 分页返回封装对象
    │   │       └── User.java          # 用户数据库实体
    │   ├── exception
    │   │   └── UsernamePasswordException.java # 自定义账号密码错误异常
    │   ├── filter
    │   │   └── AuthSessionFilter.java  # Session认证过滤器
    │   ├── handler
    │   │   ├── AutoExceptionHandler.java
    │   │   └── GlobalExceptionHandler.java # 全局异常处理器
    │   ├── interceptor
    │   │   └── SessionInterceptor.java # Session登录拦截器
    │   ├── mapper
    │   │   └── UserMapper.java         # MyBatis Mapper接口
    │   ├── resp
    │   │   └── R.java                  # 统一返回结果封装
    │   ├── service
    │   │   ├── impl
    │   │   │   └── UserServiceImpl.java # Service业务实现类
    │   │   └── UserService.java        # Service业务接口
    │   ├── utils
    │   │   └── SessionUtil.java        # Session工具类
    │   └── UserApplication.java        # SpringBoot启动类
    └── resources
        ├── mapper
        │   └── UserMapper.xml          # MyBatis SQL映射文件
        └── application.yml             # 项目配置文件
├── test                                 # 单元测试目录
└── target                               # maven编译输出目录

在实验之前需要在MySQL中创建一个表格

CREATE TABLE tb_user (
  user_id INT PRIMARY KEY auto_increment,
  username VARCHAR(10),
  password VARCHAR(200),
  age INT ,
  amount DECIMAL ,
  birth DATE ,
  create_at TIMESTAMP
);

-- 登录界面中的用户为了保证是唯一的,需要用到唯一约束

ALTER TABLE tb_user ADD UNIQUE (username);

QQ_1785843710603

posted @ 2026-08-04 20:02  黑单  阅读(12)  评论(0)    收藏  举报