diff --git a/pom.xml b/pom.xml
index 83906db..d5162a2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,6 +66,11 @@
org.springframework.boot
spring-boot-starter-security
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ 3.5.10.1
+
diff --git a/src/main/java/com/kane/animo/AnimoApplication.java b/src/main/java/com/kane/animo/AnimoApplication.java
index 3b8b9da..65261be 100644
--- a/src/main/java/com/kane/animo/AnimoApplication.java
+++ b/src/main/java/com/kane/animo/AnimoApplication.java
@@ -1,5 +1,7 @@
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.autoconfigure.SpringBootApplication;
@@ -8,6 +10,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @author Spring Boot Framework
*/
@SpringBootApplication
+@MapperScan("com.kane.animo.*.mapper")
public class AnimoApplication {
public static void main(String[] args) {
diff --git a/src/main/java/com/kane/animo/auth/controller/AuthController.java b/src/main/java/com/kane/animo/auth/controller/AuthController.java
new file mode 100644
index 0000000..0d6d2f8
--- /dev/null
+++ b/src/main/java/com/kane/animo/auth/controller/AuthController.java
@@ -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 login(@RequestBody LoginForm from) {
+ return service.login(from);
+ }
+
+ /**
+ * 注册
+ * @param from 注册参数
+ * @return 注册结果
+ */
+ @PostMapping("/register")
+ public R register(@RequestBody LoginForm from) {
+ return service.register(from);
+ }
+}
diff --git a/src/main/java/com/kane/animo/auth/domain/User.java b/src/main/java/com/kane/animo/auth/domain/User.java
new file mode 100644
index 0000000..3701f39
--- /dev/null
+++ b/src/main/java/com/kane/animo/auth/domain/User.java
@@ -0,0 +1,79 @@
+package com.kane.animo.auth.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 用户
+ * @author Kane
+ * @since 2025/11/07 03:10
+ */
+@Data
+@TableName("user")
+public class User implements Serializable, UserDetails {
+
+ @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;
+
+ /**
+ * Returns the authorities granted to the user. Cannot return null.
+ *
+ * @return the authorities, sorted by natural key (never null)
+ */
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of();
+ }
+
+ /**
+ * Returns the username used to authenticate the user. Cannot return
+ * null.
+ *
+ * @return the username (never null)
+ */
+ @Override
+ public String getUsername() {
+ return user;
+ }
+}
diff --git a/src/main/java/com/kane/animo/auth/domain/form/LoginForm.java b/src/main/java/com/kane/animo/auth/domain/form/LoginForm.java
new file mode 100644
index 0000000..7d27ed4
--- /dev/null
+++ b/src/main/java/com/kane/animo/auth/domain/form/LoginForm.java
@@ -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
+) {
+}
diff --git a/src/main/java/com/kane/animo/auth/mapper/UserMapper.java b/src/main/java/com/kane/animo/auth/mapper/UserMapper.java
new file mode 100644
index 0000000..865c26b
--- /dev/null
+++ b/src/main/java/com/kane/animo/auth/mapper/UserMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/kane/animo/auth/service/AuthService.java b/src/main/java/com/kane/animo/auth/service/AuthService.java
new file mode 100644
index 0000000..1391bc6
--- /dev/null
+++ b/src/main/java/com/kane/animo/auth/service/AuthService.java
@@ -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 login(LoginForm from);
+
+ /**
+ * 注册
+ * @param from 注册参数
+ * @return 注册结果
+ */
+ R register(LoginForm from);
+}
diff --git a/src/main/java/com/kane/animo/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/kane/animo/auth/service/impl/AuthServiceImpl.java
new file mode 100644
index 0000000..e5328a1
--- /dev/null
+++ b/src/main/java/com/kane/animo/auth/service/impl/AuthServiceImpl.java
@@ -0,0 +1,70 @@
+package com.kane.animo.auth.service.impl;
+
+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.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.crypto.bcrypt.BCrypt;
+import org.springframework.stereotype.Service;
+
+/**
+ * 认证服务
+ * @author Kane
+ * @since 2025/11/7 14:49
+ */
+@Service
+public class AuthServiceImpl implements AuthService {
+
+ @Resource
+ private AuthenticationManager authenticationManager;
+
+ @Resource
+ private UserMapper userMapper;
+
+ /**
+ * 登录
+ *
+ * @param from 登录参数
+ * @return 登录结果
+ */
+ @Override
+ public R login(LoginForm from) {
+ Authentication authenticationToken = new UsernamePasswordAuthenticationToken(from.user(), from.password());
+ Authentication authenticate = authenticationManager.authenticate(authenticationToken);
+ if (authenticate == null){
+ throw new ServiceException("用户名或密码错误");
+ }
+ User user = (User) authenticate.getPrincipal();
+
+ return R.success("OK");
+ }
+
+ /**
+ * 注册
+ *
+ * @param from 注册参数
+ * @return 注册结果
+ */
+ @Override
+ public R 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);
+ }
+}
diff --git a/src/main/java/com/kane/animo/auth/service/impl/UserDetailsServiceImpl.java b/src/main/java/com/kane/animo/auth/service/impl/UserDetailsServiceImpl.java
new file mode 100644
index 0000000..585c8fb
--- /dev/null
+++ b/src/main/java/com/kane/animo/auth/service/impl/UserDetailsServiceImpl.java
@@ -0,0 +1,45 @@
+package com.kane.animo.auth.service.impl;
+
+import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
+import com.kane.animo.auth.domain.User;
+import com.kane.animo.auth.mapper.UserMapper;
+import com.kane.animo.exception.ServiceException;
+import jakarta.annotation.Resource;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author Kane
+ * @since 2025/11/7 15:24
+ */
+@Service
+public class UserDetailsServiceImpl implements UserDetailsService {
+
+ @Resource
+ private UserMapper userMapper;
+
+ /**
+ * Locates the user based on the username. In the actual implementation, the search
+ * may possibly be case sensitive, or case insensitive depending on how the
+ * implementation instance is configured. In this case, the UserDetails
+ * object that comes back may have a username that is of a different case than what
+ * was actually requested..
+ *
+ * @param username the username identifying the user whose data is required.
+ * @return a fully populated user record (never null)
+ * @throws UsernameNotFoundException if the user could not be found or the user has no
+ * GrantedAuthority
+ */
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ User one = new LambdaQueryChainWrapper<>(userMapper)
+ .eq(User::getUser, username)
+ .one();
+ if (one == null){
+ throw new ServiceException("用户不存在");
+ }
+ return one;
+ }
+}
diff --git a/src/main/java/com/kane/animo/config/SecurityConfig.java b/src/main/java/com/kane/animo/config/SecurityConfig.java
new file mode 100644
index 0000000..fb9114a
--- /dev/null
+++ b/src/main/java/com/kane/animo/config/SecurityConfig.java
@@ -0,0 +1,54 @@
+package com.kane.animo.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+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.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.bcrypt.BCrypt;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+/**
+ * @author Kane
+ * @since 2025/11/7 14:03
+ */
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ return http.csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(request ->
+ request.requestMatchers("/auth/login", "/auth/register").permitAll()
+ .anyRequest().authenticated()
+ )
+ .sessionManagement(session ->
+ session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ ).build();
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
+ return authenticationConfiguration.getAuthenticationManager();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new PasswordEncoder() {
+ @Override
+ public String encode(CharSequence charSequence) {
+ return BCrypt.hashpw(charSequence.toString(), BCrypt.gensalt());
+ }
+
+ @Override
+ public boolean matches(CharSequence charSequence, String s) {
+ return BCrypt.checkpw(charSequence.toString(), s);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/kane/animo/exception/ServiceException.java b/src/main/java/com/kane/animo/exception/ServiceException.java
new file mode 100644
index 0000000..d5373dd
--- /dev/null
+++ b/src/main/java/com/kane/animo/exception/ServiceException.java
@@ -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);
+ }
+}
diff --git a/src/main/java/com/kane/animo/filter/ExceptionFilter.java b/src/main/java/com/kane/animo/filter/ExceptionFilter.java
new file mode 100644
index 0000000..b31705d
--- /dev/null
+++ b/src/main/java/com/kane/animo/filter/ExceptionFilter.java
@@ -0,0 +1,19 @@
+package com.kane.animo.filter;
+
+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 handleServiceException(ServiceException e) {
+ return R.error(e.getMessage());
+ }
+}
diff --git a/src/main/java/com/kane/animo/model/R.java b/src/main/java/com/kane/animo/model/R.java
new file mode 100644
index 0000000..1c11a1a
--- /dev/null
+++ b/src/main/java/com/kane/animo/model/R.java
@@ -0,0 +1,55 @@
+package com.kane.animo.model;
+
+import lombok.Data;
+
+/**
+ * 统一返回结果
+ * @author Kane
+ * @since 2025/11/7 14:50
+ */
+@Data
+public class R {
+ private int code;
+ private String message;
+ private T data;
+ private long timestamp;
+ public static R success(T data) {
+ R r = new R<>();
+ r.setCode(200);
+ r.setMessage("success");
+ r.setData(data);
+ r.setTimestamp(System.currentTimeMillis());
+ return r;
+ }
+ public static R success(String message, T data) {
+ R r = new R<>();
+ r.setCode(200);
+ r.setMessage(message);
+ r.setData(data);
+ r.setTimestamp(System.currentTimeMillis());
+ return r;
+ }
+ public static R error(String message) {
+ R r = new R<>();
+ r.setCode(500);
+ r.setMessage(message);
+ r.setTimestamp(System.currentTimeMillis());
+ return r;
+ }
+ public static R build(int code, String message) {
+ R r = new R<>();
+ r.setCode(code);
+ r.setMessage(message);
+ r.setTimestamp(System.currentTimeMillis());
+ return r;
+ }
+ public static R build(int code, String message, T data) {
+ R r = new R<>();
+ r.setCode(code);
+ r.setMessage(message);
+ r.setData(data);
+ r.setTimestamp(System.currentTimeMillis());
+ return r;
+ }
+
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index b4d1286..9eadcb5 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -4,3 +4,12 @@ server:
spring:
application:
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