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; }
}
}
核心流程说明:
- 从请求体中取出
username和password。 - 调用
userDetailsService.loadUserByUsername()查找用户,找不到则返回"用户不存在"。 - 构造
UsernamePasswordAuthenticationToken,调用authenticationManager.authenticate()进行密码校验。 - 校验通过后,将认证信息存入
SecurityContextHolder(本质是写入 Session),完成登录状态保持。 - 捕获
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入门与登录模块开发