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>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
<version>3.5.10.1</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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:
|
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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user