feat(auth): 实现用户认证模块
- 新增登录与注册接口控制器 AuthController - 创建登录表单数据类 LoginForm - 定义用户实体类 User 并实现 UserDetails 接口 - 添加用户持久层接口 UserMapper- 实现认证服务 AuthServiceImpl 包含登录和注册逻辑 - 配置 Spring Security 安全策略并禁用 CSRF - 引入 BCrypt 加密算法用于密码处理 - 添加全局异常处理器 ExceptionFilter 捕获业务异常 - 创建统一响应模型 R 封装返回结果 - 集成 MyBatis Plus依赖并配置数据源信息
This commit is contained in:
parent
3a35848c4f
commit
afa84020e5
5
pom.xml
5
pom.xml
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
79
src/main/java/com/kane/animo/auth/domain/User.java
Normal file
79
src/main/java/com/kane/animo/auth/domain/User.java
Normal 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;
|
||||
}
|
||||
}
|
||||
14
src/main/java/com/kane/animo/auth/domain/form/LoginForm.java
Normal file
14
src/main/java/com/kane/animo/auth/domain/form/LoginForm.java
Normal 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
|
||||
) {
|
||||
}
|
||||
14
src/main/java/com/kane/animo/auth/mapper/UserMapper.java
Normal file
14
src/main/java/com/kane/animo/auth/mapper/UserMapper.java
Normal 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> {
|
||||
}
|
||||
25
src/main/java/com/kane/animo/auth/service/AuthService.java
Normal file
25
src/main/java/com/kane/animo/auth/service/AuthService.java
Normal 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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
54
src/main/java/com/kane/animo/config/SecurityConfig.java
Normal file
54
src/main/java/com/kane/animo/config/SecurityConfig.java
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
12
src/main/java/com/kane/animo/exception/ServiceException.java
Normal file
12
src/main/java/com/kane/animo/exception/ServiceException.java
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/main/java/com/kane/animo/filter/ExceptionFilter.java
Normal file
19
src/main/java/com/kane/animo/filter/ExceptionFilter.java
Normal 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());
|
||||
}
|
||||
}
|
||||
55
src/main/java/com/kane/animo/model/R.java
Normal file
55
src/main/java/com/kane/animo/model/R.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user