多用户博客系统搭建(1):表设计+登录注册接口

目录

🔅提前看:

  1. 本文基于前几章的学习基础,开始正式写一个"多用户博客系统"项目。
  2. 用到bcrypt密码加密、JWT鉴权、apifox测试。
  3. 最后附面试可能询问的问题。适合初学者快速上手。

🏗️一、表设计

1、User、Article、Comment表结构


2、SQL 语句

sql 复制代码
use blog_system_database;

CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    role VARCHAR(20) DEFAULT 'user',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);


-- 文章表
CREATE TABLE post (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    title VARCHAR(200) NOT NULL,
    content TEXT NOT NULL,
    status VARCHAR(20) DEFAULT 'published', -- draft/published
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES user(id)
);

-- 评论表
CREATE TABLE comment (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    post_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    parent_id BIGINT DEFAULT NULL, -- 回复谁的评论
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (post_id) REFERENCES post(id),
    FOREIGN KEY (user_id) REFERENCES user(id)
);

🏗️二、新建项目,实现注册+登录接口

这是整个项目的骨架,后面所有模块都照这个逻辑来写:

密码加密使用bcrypt,登陆时使用JWT鉴权。

1. 编写User实体类 User

java 复制代码
package com.example.blog.entity;

import lombok.Data;
import java.time.LocalDateTime;

@Data // 💡 Lombok 自动生成 getter/setter/toString
public class User {
    private Long id;               // 主键
    private String username;       // 用户名
    private String password;       // 密码(存加密后的)
    private String email;          // 邮箱
    private String role;           // 角色(user / admin)
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

💡 核心点:

密码不存明文,注册时我们会加密。

createdAt / updatedAt 不用手动填,MySQL 自动生成。

2. 编写Mapper接口 UserMapper

java 复制代码
package com.example.blog.mapper;

import com.example.blog.entity.User;
import org.apache.ibatis.annotations.*;

@Mapper
public interface UserMapper {
    @Insert("INSERT INTO user(username, password, email, role) VALUES(#{username}, #{password}, #{email}, #{role})")
    void insert(User user);

    @Select("SELECT * FROM user WHERE username = #{username}")
    User findByUsername(String username);
}

💡 核心点:

这里用 MyBatis 注解版,简单直接;

注册用户插入数据;

登录时根据用户名查找。

3. 业务逻辑层 UserService

java 复制代码
package com.example.blog.service;

import com.example.blog.entity.User;
import com.example.blog.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

    // 💡 注册:加密密码再存数据库
    public void register(User user) {
        user.setPassword(encoder.encode(user.getPassword()));
        user.setRole("user");
        userMapper.insert(user);
    }

    // 💡 登录:验证用户名存在 + 密码匹配
    public boolean login(String username, String rawPassword) {
        User dbUser = userMapper.findByUsername(username);
        if (dbUser == null) return false;
        return encoder.matches(rawPassword, dbUser.getPassword());
    }
}

💡 核心点:

BCryptPasswordEncoder 是 Spring 自带加密器;

matches 方法会自动加盐校验;

永远不要自己用 MD5 或手写加密算法。

4. 控制器层 UserController

java 复制代码
package com.example.blog.controller;

import com.example.blog.entity.User;
import com.example.blog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    // 💡 注册接口
    @PostMapping("/register")
    public String register(@RequestBody User user) {
        userService.register(user);
        return "注册成功";
    }

    // 💡 登录接口
    @PostMapping("/login")
    public String login(@RequestBody User user) {
        boolean success = userService.login(user.getUsername(), user.getPassword());
        return success ? "登录成功" : "用户名或密码错误";
    }
}

💡 核心点:

用 @RestController 直接返回字符串;

前端(或 Apifox)请求时用 POST + JSON;

示例 JSON:

复制代码
{
  "username": "Alice",
  "password": "123456",
  "email": "test@example.com"
}

5. 测试(Apifox)

1.在config软件包下新建SecurityConfig.java文件:

java 复制代码
package com.example.blog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 新写法:禁用 CSRF
                .csrf(csrf -> csrf.disable())
                // 放行注册和登录接口
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/user/register", "/user/login").permitAll()
                        .anyRequest().authenticated()
                )
                // 不使用默认登录页
                .formLogin(form -> form.disable())
                // 不使用 HTTP Basic 认证
                .httpBasic(basic -> basic.disable());

        return http.build();
    }
}

2.在apifox上新建注册和登录接口进行测试。

🏗️三、JWT登录鉴权模块

1、JWT 模块核心目标

JWT(JSON Web Token)是用来做 登录状态保持 的。

思路就是:

用户登录成功 → 服务端签发一个 token → 前端保存 →

后续请求时带上 token → 后端校验 → 放行请求。


2、模块结构设计

我们会增加以下几个部分(共四步)👇:

模块 作用
JwtUtil 负责创建 & 解析 token
JwtFilter 拦截请求、校验 token 是否有效
LoginController 登录成功后签发 token
SecurityConfig 注册过滤器,让接口受保护

3、引入依赖(pom.xml)

先确认你加上 JWT 依赖(Spring Boot 没自带):

xml 复制代码
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- 用于JSON序列化 -->
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

4、核心逻辑代码框架

  • JwtUtil ------ 工具类
    生成 & 解析 JWT 的地方。
java 复制代码
package com.example.demo.utils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtUtil {

    private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1小时

    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(key)
                .compact();
    }

    public static String parseToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
        } catch (JwtException e) {
            return null; // token 无效或过期
        }
    }
}

  • 登录接口中签发 Token
    修改你的 UserController 登录部分:
java 复制代码
@PostMapping("/login")
public Result<String> login(@RequestBody User user) {
    boolean success = userService.login(user.getUsername(), user.getPassword());
    if (success) {
        String token = JwtUtil.generateToken(user.getUsername());
        return Result.success(token); // 登录成功返回 token
    } else {
        return Result.error("用户名或密码错误");
    }
}

补充一个通用结果返回类Result,在UserController中导入

java 复制代码
//com.example.blog.entity

package com.example.blog.entity;

/**
 * 通用返回结果类,用于封装接口响应。
 * @param <T> 数据类型
 */
public class Result<T> {

    private Integer code;  // 状态码:0成功,1失败
    private String msg;    // 提示信息
    private T data;        // 返回数据

    // 构造函数
    public Result(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    // 静态方法:快速返回成功
    public static <T> Result<T> success(T data) {
        return new Result<>(0, "success", data);
    }

    // 静态方法:快速返回错误
    public static <T> Result<T> error(String msg) {
        return new Result<>(1, msg, null);
    }

    // Getter / Setter
    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

  • 过滤器 JwtFilter
    拦截请求,验证 header 中的 token:
java 复制代码
package com.example.demo.filter;

import com.example.demo.utils.JwtUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        String path = req.getRequestURI();
        // 注册、登录接口不拦截
        if (path.contains("/register") || path.contains("/login")) {
            chain.doFilter(request, response);
            return;
        }

        String token = req.getHeader("Authorization");
        if (token == null || JwtUtil.parseToken(token) == null) {
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            res.getWriter().write("Unauthorized");
            return;
        }

        chain.doFilter(request, response);
    }
}

  • 注册过滤器(配置类)
java 复制代码
package com.example.demo.config;

import com.example.demo.filter.JwtFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        FilterRegistrationBean<JwtFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new JwtFilter());
        bean.addUrlPatterns("/*"); // 拦截所有请求
        bean.setOrder(1);
        return bean;
    }
}

5、测试步骤(用 Apifox)

1️⃣ 调用 /user/login → 得到一个 token(字符串)

→ 如果 token 正确,就能访问;否则返回 401。

  • 目前项目结构:

💾四、 学习重点(面试能讲的点)

1、项目基础类问题:

💬 Q1:你这个项目的整体架构是怎样的?

复制代码
A:
我这个项目是基于 Spring Boot 的三层架构,分为 Controller、Service、Mapper 三层:

Controller:负责接收前端请求、返回响应;
Service:封装业务逻辑;
Mapper(或DAO):通过 MyBatis 操作数据库;
另外我还设计了统一响应类 Result,保证接口返回格式一致,并用 JWT 实现用户登录状态管理。

💬 Q2:注册、登录接口的实现流程是怎样的?

复制代码
A:
注册:
前端传入用户名、密码、邮箱 → 后端先检查用户名是否重复 → 用 BCryptPasswordEncoder 加密密码 → 存入数据库。

登录:
用户输入账号密码 → 从数据库查出用户 → 用 BCrypt 校验密码 → 如果正确,用 JwtUtil 生成 token 返回前端。

💬 Q3:你为什么选择 JWT 做登录认证?

复制代码
A:
因为 JWT(JSON Web Token)是一种无状态认证方式,不需要服务器存储 session,比较适合前后端分离项目。
每次请求只要在 Header 里带上 token,后端就能校验身份,提高扩展性和性能。

2、安全与加密相关

💬 Q4:为什么要用 BCrypt,而不是 MD5?

复制代码
A:
MD5 速度快但不安全,容易被暴力破解。
BCrypt 自带随机盐值和多次加密机制,即使密码相同也会得到不同密文,安全性更高,是 Spring Security 推荐的方案。

💬 Q5:JWT 的结构是什么样的?

复制代码
A:
JWT 由三部分组成,用点 . 分隔:
Header.Payload.Signature

例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VybmFtZSI6IkFsaWNlIn0.
y6jQLFXo7T3lKkODv-h8Z...

Header(头部):说明算法(如 HS256)
Payload(载荷):存放用户信息
Signature(签名):用密钥签名保证数据没被篡改

💬 Q6:token 原理是什么?为什么不用 Session?

复制代码
A:
Token 是一种无状态认证机制。
当用户登录成功后,后端生成一个 token(通常是 JWT),里面包含了用户信息(如用户名、过期时间),并用密钥签名。
前端拿到 token 后会保存在 localStorage 或 cookie 里,每次请求都在 Header 里携带这个 token。
后端通过验证签名即可确认身份,而不需要保存 session。

3、Spring 相关问题

💬 Q7:Spring Boot 是怎么启动项目的?

复制代码
A:
项目入口是 @SpringBootApplication 注解标注的主类(比如 BlogApplication)。
Spring Boot 在启动时会自动扫描同包下的所有组件(Controller、Service、Mapper 等),完成依赖注入。

💬 Q8:@Autowired 和 @Resource 有什么区别?

复制代码
A:
@Autowired 是 Spring 的注解,按类型注入;

@Resource 是 JSR 标准注解,默认按名称注入;
在单实例 Bean 下效果类似,但推荐用 @Autowired 搭配 @Qualifier 更灵活。

💬 Q9:过滤器和拦截器的区别

复制代码
A:
"Filter 属于 Servlet 层,适合做登录鉴权这种全局校验;
Interceptor 属于 Spring 层,更适合做权限或日志处理。"

4、数据库设计类问题

💬 Q10:你在设计用户表时考虑了哪些字段?

复制代码
A:
id(主键)
username(唯一索引)
password(BCrypt 加密后)
email
created_at、updated_at(记录时间)
这样既能满足基本功能,也方便后续扩展,比如角色、状态、头像字段等。
相关推荐
Chrikk4 分钟前
现代化 C++ 工程构建:CMake 与包管理器的依赖治理
开发语言·c++
❀͜͡傀儡师24 分钟前
SpringBoot 扫码登录全流程:UUID 生成、状态轮询、授权回调详解
java·spring boot·后端
a努力。38 分钟前
国家电网Java面试被问:Spring Boot Starter 制作原理
java·spring boot·面试
一 乐39 分钟前
酒店预约|基于springboot + vue酒店预约系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
世转神风-1 小时前
qt-kits-警告:No C++ compiler,无法正常解析工程项目.pro文件
开发语言·c++
翔云 OCR API1 小时前
承兑汇票识别接口技术解析与应用实践
开发语言·人工智能·python·计算机视觉·ocr
元周民1 小时前
matlab求两个具有共根的多项式的所有共根(未详细验证)
开发语言·matlab
guslegend1 小时前
Tomact高级使用及原理剖析
java
Code blocks1 小时前
SpringBoot从0-1集成Minio对象存储
java·spring boot·后端