SpringBoot 多人协作平台实战(6):SpringBoot Controller 入门与登录模块开发

SpringBoot 多人协作平台实战(6):SpringBoot Controller 入门与登录模块开发

本篇是 SpringBoot 多人协作博客平台系列实战的第六节,重点介绍后端 RESTful 接口规范、Spring Security 集成,以及 Auth 认证模块(注册、登录、状态查询、注销)的完整实现过程。


一、后端接口规范约定

在动手写代码之前,先和前端团队约定好接口规范,能大幅减少联调时的沟通成本。一个标准的接口文档通常包含以下要素:

要素 说明
接口路径 /auth/register
请求方式 GET / POST / PATCH / DELETE / PUT
参数格式 JSON (application/json) 或表单 (application/x-www-form-urlencoded)
参数字段及限制 字段名、类型、长度、是否必填
成功返回格式 {"status": "ok", ...}
失败返回格式 {"status": "fail", "msg": "错误原因"}

本项目后端线上根路径为://blog-server.hunger-valley.com


二、接口文档总览

认证模块(Auth)

POST /auth/register --- 用户注册

请求参数Content-Type: application/json):

字段 类型 限制
username String 长度 1~15,只能是字母、数字、下划线、中文
password String 长度 6~16,任意字符

返回示例(成功)

json 复制代码
{
  "status": "ok",
  "msg": "注册成功",
  "data": {
    "id": 1,
    "username": "hunger",
    "avatar": "http://avatar.com/1.png",
    "updatedAt": "2017-12-27T07:40:09.697Z",
    "createdAt": "2017-12-27T07:40:09.697Z"
  }
}

返回示例(失败)

json 复制代码
{"status": "fail", "msg": "错误原因"}

POST /auth/login --- 用户登录

请求参数与注册相同。

成功返回

json 复制代码
{
  "status": "ok",
  "msg": "登录成功",
  "data": {
    "id": 1,
    "username": "hunger",
    "avatar": "头像 url",
    "createdAt": "2017-12-27T07:40:09.697Z",
    "updatedAt": "2017-12-27T07:40:09.697Z"
  }
}

失败返回{"status": "fail", "msg": "用户不存在"}{"status": "fail", "msg": "密码不正确"}


GET /auth --- 判断是否已登录

无需提交参数。

json 复制代码
// 已登录
{ "status": "ok", "isLogin": true, "data": { ... } }

// 未登录
{ "status": "ok", "isLogin": false }

GET /auth/logout --- 注销登录
json 复制代码
// 成功
{"status": "ok", "msg": "注销成功"}

// 失败
{"status": "fail", "msg": "用户尚未登录"}

博客模块(Blog)

方法 路径 功能
GET /blog 获取博客列表(支持分页、按用户过滤、首页展示过滤)
GET /blog/:blogId 获取单篇博客详情
POST /blog 创建博客(需登录)
PATCH /blog/:blogId 修改博客(需登录,只能改自己的)
DELETE /blog/:blogId 删除博客(需登录,只能删自己的)

博客模块接口的具体参数与返回格式与认证模块规范一致,后续章节会逐一实现,本节重点聚焦 Auth 模块。


三、前端静态文件部署

将前端打包好的静态文件拷贝到 resources/static 目录下,SpringBoot 会自动将其作为静态资源对外提供。

css 复制代码
src/main/resources/static/
├── index.html
├── js/
└── css/

部署后访问:http://localhost:8080/index.html#/

注意:全局搜索前端代码,将原来指向线上地址的 API 请求根路径替换为本地地址(或留空走同域),否则前后端无法联通。


四、引入 Spring Security

pom.xml 中添加 Spring Security 依赖:

xml 复制代码
<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

五、配置 WebSecurityConfig

configuration 包下新建 WebSecurityConfig,继承 WebSecurityConfigurerAdapter

scala 复制代码
package hello.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            // /auth/** 下的接口不需要身份验证,直接放行
            .antMatchers("/", "/auth/**").permitAll();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        // 暂时使用内存用户,后续替换为数据库用户
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public AuthenticationManager customAuthenticationManager() throws Exception {
        return authenticationManager();
    }
}

关键点说明

  • .csrf().disable():关闭 CSRF 校验,因为我们的前端是独立的 SPA,使用 JSON 交互,不需要 CSRF token。
  • .antMatchers("/", "/auth/**").permitAll():认证相关接口对所有人放行,其余接口默认需要登录。
  • customAuthenticationManager() 暴露为 Bean,以便在 Controller 中注入使用。

六、User 实体类

typescript 复制代码
package hello.entity;

import java.time.Instant;

public class User {
    Integer id;
    String username;
    String avatar;
    Instant createdAt;
    Instant updatedAt;

    public User(Integer id, String username) {
        this.id = id;
        this.username = username;
        this.avatar = "";
        this.createdAt = Instant.now();
        this.updatedAt = Instant.now();
    }

    // Getter & Setter ...
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getAvatar() { return avatar; }
    public void setAvatar(String avatar) { this.avatar = avatar; }

    public Instant getCreatedAt() { return createdAt; }
    public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }

    public Instant getUpdatedAt() { return updatedAt; }
    public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}

七、JSON 序列化的重要规则

在写 Controller 之前,有两条关于 JSON 序列化的规则必须牢记:

规则一 :JSON 的属性名与类中的 getXxx() 方法名 直接相关,而与字段名无关。

例如 getUsername() → JSON 中出现 "username" 字段。

规则二 :布尔值的 JSON key 与 isXxx() 方法名 直接相关。

例如 isLogin() → JSON 中出现 "login" 字段(注意:Jackson 默认会去掉 is 前缀)。

这意味着,如果你在返回对象里加了一个 getXxx() 方法,JSON 里就会出现对应的字段,无论类里有没有声明对应的实例变量。


八、AuthController 实现

typescript 复制代码
package hello.controller;

import hello.entity.User;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
public class AuthController {

    private final UserDetailsService userDetailsService;
    private final AuthenticationManager authenticationManager;

    public AuthController(UserDetailsService userDetailsService,
                          AuthenticationManager authenticationManager) {
        this.userDetailsService = userDetailsService;
        this.authenticationManager = authenticationManager;
    }

    /** GET /auth ------ 判断用户是否已登录 */
    @GetMapping("/auth")
    public Object auth() {
        // 当前阶段固定返回未登录,后续接入 Session 后完善
        return new Result("ok", "用户没有登录", false);
    }

    /** POST /auth/login ------ 用户登录 */
    @PostMapping("/auth/login")
    public Result login(@RequestBody Map<String, String> usernameAndPasswordJson) {
        String username = usernameAndPasswordJson.get("username");
        String password = usernameAndPasswordJson.get("password");

        UserDetails userDetails;
        try {
            userDetails = userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException e) {
            return new Result("fail", "用户不存在", false);
        }

        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(
                        userDetails, password, userDetails.getAuthorities());

        try {
            authenticationManager.authenticate(token);
            // 将认证信息写入 SecurityContext(即 Session)
            SecurityContextHolder.getContext().setAuthentication(token);

            // 暂用硬编码用户,后续改为从数据库查询
            User loggedInUser = new User(1, "张三");
            return new Result("ok", "登录成功", true, loggedInUser);
        } catch (BadCredentialsException e) {
            return new Result("fail", "密码不正确", false);
        }
    }

    /** 统一返回结果封装 */
    public static class Result {
        String status;
        String msg;
        boolean isLogin;
        Object data;

        public Result(String status, String msg, boolean isLogin) {
            this(status, msg, isLogin, null);
        }

        public Result(String status, String msg, boolean isLogin, Object data) {
            this.status = status;
            this.msg = msg;
            this.isLogin = isLogin;
            this.data = data;
        }

        public String getStatus() { return status; }
        public String getMsg() { return msg; }
        public boolean isLogin() { return isLogin; }
        public Object getData() { return data; }
    }
}

核心流程说明

  1. 从请求体中取出 usernamepassword
  2. 调用 userDetailsService.loadUserByUsername() 查找用户,找不到则返回"用户不存在"。
  3. 构造 UsernamePasswordAuthenticationToken,调用 authenticationManager.authenticate() 进行密码校验。
  4. 校验通过后,将认证信息存入 SecurityContextHolder(本质是写入 Session),完成登录状态保持。
  5. 捕获 BadCredentialsException 处理密码错误的情况。

九、项目包结构总览

bash 复制代码
src/main/java/hello/
├── configuration/
│   └── WebSecurityConfig.java    # Security 配置
├── controller/
│   └── AuthController.java       # 认证接口
├── entity/
│   └── User.java                 # 用户实体
└── HelloApplication.java         # 启动类

十、用静态页面验证效果

完成上述代码后,启动项目,打开浏览器访问:

bash 复制代码
http://localhost:8080/index.html#/

使用配置好的内存用户(username: user / password: password)尝试登录,观察接口返回是否符合预期。


总结

本节完成了以下内容:

  • 梳理了项目的 RESTful 接口规范,明确了请求方式、参数格式和统一返回结构
  • 引入 Spring Security 并完成基础安全配置,放行 /auth/** 路径
  • 实现了 /auth/login/auth 接口的基础版本
  • 理解了 Spring Boot 中 JSON 序列化与 getter 方法的关系

目前登录模块只使用了内存中的单个用户,下一节将对接数据库,实现真正的多用户注册与登录功能。


系列课程:Java SpringBoot 多人协作平台实战 · 第六章 · SpringBoot Controller入门与登录模块开发

相关推荐
用户298698530141 小时前
用 Java 操作 Word 文档?试试添加内容控件
java·后端
带刺的坐椅1 小时前
Java AI 框架三国杀:Solon AI vs Spring AI vs LangChain4j 深度对比
java·ai·langchain4j·spring-ai·solon-ai
苍煜1 小时前
K8s 集群快速搭建(系列第八篇:单机/多节点集群实战)
java·容器·kubernetes
Chase_______1 小时前
Java 基础语言 ① —— Java 运行机制与开发环境:从 javac 到 JVM 全流程解析
java·jvm·python
北风toto1 小时前
在 Axios 中发送 POST 请求并携带参数通常有以下两种方式
java
cui_ruicheng1 小时前
Linux线程(二):pthread 线程库与线程控制
java·开发语言·jvm
山北雨夜漫步1 小时前
LangGraph
java·前端·算法
jakeswang2 小时前
【AI面经】大模型半夜发短信骂客户?Agent 工具调用失控,你如何设计防护机制?
java·后端
码上小翔哥2 小时前
Spring Boot Redis 缓存序列化踩坑记:GenericJackson2JsonRedisSerializer 的数组反序列化陷阱
java·redis