feat(auth): 实现用户认证模块

- 新增认证控制器 AuthController,提供登录和注册接口
- 创建 LoginForm 记录类用于接收登录/注册参数
- 定义 User 实体类映射数据库用户表结构
- 添加 UserMapper 接口继承 MyBatis-Plus 的 BaseMapper
- 实现 AuthService 接口,完成登录与注册业务逻辑
- 集成 Sa-Token 框架进行权限认证管理
- 配置拦截器排除 /auth/login 路径的认证检查
- 引入 ServiceException 自定义服务异常类
- 增加全局异常处理器 ExceptionFilter 处理认证相关异常
- 创建统一响应模型 R 封装接口返回数据格式
- 在主应用类上添加 Mapper 扫描注解支持 MyBatis 映射
- 更新 application.yml 配置文件,加入数据源及 MyBatis-Plus 设置
- 修改 pom.xml 添加 MyBatis-Plus 和 Sa-Token 相关依赖项
This commit is contained in:
Grand-cocoa 2025-11-07 16:24:22 +08:00
parent 3a35848c4f
commit 6ceab3cf86
13 changed files with 361 additions and 2 deletions

10
pom.xml
View File

@ -63,8 +63,14 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>com.baomidou</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.10.1</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.39.0</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -1,5 +1,7 @@
package com.kane.animo; package com.kane.animo;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.annotation.MapperScans;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -8,6 +10,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @author Spring Boot Framework * @author Spring Boot Framework
*/ */
@SpringBootApplication @SpringBootApplication
@MapperScan("com.kane.animo.*.mapper")
public class AnimoApplication { public class AnimoApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -0,0 +1,43 @@
package com.kane.animo.auth.controller;
import com.kane.animo.auth.domain.form.LoginForm;
import com.kane.animo.auth.service.AuthService;
import com.kane.animo.model.R;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 认证
* @author Kane
* @since 2025/11/7 14:25
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
@Resource
private AuthService service;
/**
* 登录
* @param from 登录参数
* @return 登录结果
*/
@PostMapping("/login")
public R<String> login(@RequestBody LoginForm from) {
return service.login(from);
}
/**
* 注册
* @param from 注册参数
* @return 注册结果
*/
@PostMapping("/register")
public R<String> register(@RequestBody LoginForm from) {
return service.register(from);
}
}

View File

@ -0,0 +1,55 @@
package com.kane.animo.auth.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.InputStream;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户
* @author Kane
* @since 2025/11/07 03:10
*/
@Data
@TableName("user")
public class User implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 用户名
*/
private String user;
/**
* 密码
*/
private String password;
/**
* 头像
*/
private InputStream avatar;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,14 @@
package com.kane.animo.auth.domain.form;
/**
* 登录参数
* @author Kane
* @since 2025/11/7 14:28
* @param user 用户名
* @param password 密码
*/
public record LoginForm(
String user,
String password
) {
}

View File

@ -0,0 +1,14 @@
package com.kane.animo.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kane.animo.auth.domain.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户持久层
* @author Kane
* @since 2025/11/7 15:11
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@ -0,0 +1,25 @@
package com.kane.animo.auth.service;
import com.kane.animo.auth.domain.form.LoginForm;
import com.kane.animo.model.R;
/**
* 认证服务
* @author Kane
* @since 2025/11/7 14:49
*/
public interface AuthService {
/**
* 登录
* @param from 登录参数
* @return 登录结果
*/
R<String> login(LoginForm from);
/**
* 注册
* @param from 注册参数
* @return 注册结果
*/
R<String> register(LoginForm from);
}

View File

@ -0,0 +1,67 @@
package com.kane.animo.auth.service.impl;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.kane.animo.auth.domain.User;
import com.kane.animo.auth.domain.form.LoginForm;
import com.kane.animo.auth.mapper.UserMapper;
import com.kane.animo.auth.service.AuthService;
import com.kane.animo.exception.ServiceException;
import com.kane.animo.model.R;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* 认证服务
* @author Kane
* @since 2025/11/7 14:49
*/
@Service
public class AuthServiceImpl implements AuthService {
@Resource
private UserMapper userMapper;
/**
* 登录
*
* @param from 登录参数
* @return 登录结果
*/
@Override
public R<String> login(LoginForm from) {
User one = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getUser, from.user())
.one();
if (one == null){
throw new ServiceException("用户不存在");
}
if (!BCrypt.checkpw(from.password(), one.getPassword())){
throw new ServiceException("密码错误");
}
StpUtil.login(one.getId());
return R.success();
}
/**
* 注册
*
* @param from 注册参数
* @return 注册结果
*/
@Override
public R<String> register(LoginForm from) {
User one = new LambdaQueryChainWrapper<>(userMapper)
.eq(User::getUser, from.user())
.one();
if (one != null){
throw new ServiceException("用户已存在");
}
User user = new User();
user.setUser(from.user());
user.setPassword(BCrypt.hashpw(from.password(), BCrypt.gensalt()));
userMapper.insert(user);
return login(from);
}
}

View File

@ -0,0 +1,23 @@
package com.kane.animo.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器配置
* @author Kane
* @since 2025/11/7 16:13
*/
@Configuration
public class SaTokenConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/auth/login");
// .excludePathPatterns("/auth/register");
}
}

View File

@ -0,0 +1,12 @@
package com.kane.animo.exception;
/**
* 服务异常
* @author Kane
* @since 2025/11/7 15:16
*/
public class ServiceException extends RuntimeException {
public ServiceException(String message) {
super(message);
}
}

View File

@ -0,0 +1,25 @@
package com.kane.animo.filter;
import cn.dev33.satoken.exception.NotLoginException;
import com.kane.animo.exception.ServiceException;
import com.kane.animo.model.R;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 异常过滤器
* @author Kane
* @since 2025/11/7 15:16
*/
@RestControllerAdvice
public class ExceptionFilter {
@ExceptionHandler(ServiceException.class)
public R<String> handleServiceException(ServiceException e) {
return R.error(e.getMessage());
}
@ExceptionHandler(NotLoginException.class)
public R<String> handleException(NotLoginException e) {
return R.error("认证失败 - 未登录");
}
}

View File

@ -0,0 +1,63 @@
package com.kane.animo.model;
import lombok.Data;
/**
* 统一返回结果
* @author Kane
* @since 2025/11/7 14:50
*/
@Data
public class R<T> {
private int code;
private String message;
private T data;
private long timestamp;
public static <T> R<T> success() {
R<T> r = new R<>();
r.setCode(200);
r.setMessage("success");
r.setData(null);
r.setTimestamp(System.currentTimeMillis());
return r;
}
public static <T> R<T> success(T data) {
R<T> r = new R<>();
r.setCode(200);
r.setMessage("success");
r.setData(data);
r.setTimestamp(System.currentTimeMillis());
return r;
}
public static <T> R<T> success(String message, T data) {
R<T> r = new R<>();
r.setCode(200);
r.setMessage(message);
r.setData(data);
r.setTimestamp(System.currentTimeMillis());
return r;
}
public static <T> R<T> error(String message) {
R<T> r = new R<>();
r.setCode(500);
r.setMessage(message);
r.setTimestamp(System.currentTimeMillis());
return r;
}
public static <T> R<T> build(int code, String message) {
R<T> r = new R<>();
r.setCode(code);
r.setMessage(message);
r.setTimestamp(System.currentTimeMillis());
return r;
}
public static <T> R<T> build(int code, String message, T data) {
R<T> r = new R<>();
r.setCode(code);
r.setMessage(message);
r.setData(data);
r.setTimestamp(System.currentTimeMillis());
return r;
}
}

View File

@ -4,3 +4,12 @@ server:
spring: spring:
application: application:
name: Animo name: Animo
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/animo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&useSSL=false
username: animo
password: WS6PCwksRpEYNpNt
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml