From afa84020e5d01074771c31d5da5d78bf222da92b Mon Sep 17 00:00:00 2001 From: Grand-cocoa <1075576561@qq.com49111108+grand-cocoa@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:10:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=AE=9E=E7=8E=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=AE=A4=E8=AF=81=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增登录与注册接口控制器 AuthController - 创建登录表单数据类 LoginForm - 定义用户实体类 User 并实现 UserDetails 接口 - 添加用户持久层接口 UserMapper- 实现认证服务 AuthServiceImpl 包含登录和注册逻辑 - 配置 Spring Security 安全策略并禁用 CSRF - 引入 BCrypt 加密算法用于密码处理 - 添加全局异常处理器 ExceptionFilter 捕获业务异常 - 创建统一响应模型 R 封装返回结果 - 集成 MyBatis Plus依赖并配置数据源信息 --- pom.xml | 5 ++ .../java/com/kane/animo/AnimoApplication.java | 3 + .../animo/auth/controller/AuthController.java | 43 ++++++++++ .../java/com/kane/animo/auth/domain/User.java | 79 +++++++++++++++++++ .../animo/auth/domain/form/LoginForm.java | 14 ++++ .../kane/animo/auth/mapper/UserMapper.java | 14 ++++ .../kane/animo/auth/service/AuthService.java | 25 ++++++ .../auth/service/impl/AuthServiceImpl.java | 70 ++++++++++++++++ .../service/impl/UserDetailsServiceImpl.java | 45 +++++++++++ .../com/kane/animo/config/SecurityConfig.java | 54 +++++++++++++ .../animo/exception/ServiceException.java | 12 +++ .../kane/animo/filter/ExceptionFilter.java | 19 +++++ src/main/java/com/kane/animo/model/R.java | 55 +++++++++++++ src/main/resources/application.yml | 9 +++ 14 files changed, 447 insertions(+) create mode 100644 src/main/java/com/kane/animo/auth/controller/AuthController.java create mode 100644 src/main/java/com/kane/animo/auth/domain/User.java create mode 100644 src/main/java/com/kane/animo/auth/domain/form/LoginForm.java create mode 100644 src/main/java/com/kane/animo/auth/mapper/UserMapper.java create mode 100644 src/main/java/com/kane/animo/auth/service/AuthService.java create mode 100644 src/main/java/com/kane/animo/auth/service/impl/AuthServiceImpl.java create mode 100644 src/main/java/com/kane/animo/auth/service/impl/UserDetailsServiceImpl.java create mode 100644 src/main/java/com/kane/animo/config/SecurityConfig.java create mode 100644 src/main/java/com/kane/animo/exception/ServiceException.java create mode 100644 src/main/java/com/kane/animo/filter/ExceptionFilter.java create mode 100644 src/main/java/com/kane/animo/model/R.java 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 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