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

- 新增登录与注册接口控制器 AuthController
- 创建登录表单数据类 LoginForm
- 定义用户实体类 User 并实现 UserDetails 接口
- 添加用户持久层接口 UserMapper- 实现认证服务 AuthServiceImpl 包含登录和注册逻辑
- 配置 Spring Security 安全策略并禁用 CSRF
- 引入 BCrypt 加密算法用于密码处理
- 添加全局异常处理器 ExceptionFilter 捕获业务异常
- 创建统一响应模型 R 封装返回结果
- 集成 MyBatis Plus依赖并配置数据源信息
This commit is contained in:
Grand-cocoa 2025-11-07 16:10:35 +08:00
parent 3a35848c4f
commit afa84020e5
14 changed files with 447 additions and 0 deletions

View File

@ -66,6 +66,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.10.1</version>
</dependency>
</dependencies>
<build>

View File

@ -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) {

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,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 <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
/**
* Returns the username used to authenticate the user. Cannot return
* <code>null</code>.
*
* @return the username (never <code>null</code>)
*/
@Override
public String getUsername() {
return user;
}
}

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,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<String> 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<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,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 <code>UserDetails</code>
* 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 <code>null</code>)
* @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;
}
}

View File

@ -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);
}
};
}
}

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,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<String> handleServiceException(ServiceException e) {
return R.error(e.getMessage());
}
}

View File

@ -0,0 +1,55 @@
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(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:
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