ADD: user login

This commit is contained in:
focus1024-wind
2025-12-23 11:52:24 +08:00
parent f779275018
commit 8d1cba57ca
18 changed files with 346 additions and 198 deletions

View File

@@ -1,8 +1,8 @@
-- 确保启用 uuid-ossp 扩展(用于生成 UUID -- 确保启用 uuid-ossp 扩展(用于生成 UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 创建 users -- 创建 account
CREATE TABLE users CREATE TABLE account
( (
id CHAR(36) PRIMARY KEY DEFAULT uuid_generate_v4()::varchar, id CHAR(36) PRIMARY KEY DEFAULT uuid_generate_v4()::varchar,
username VARCHAR(255) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE,
@@ -22,9 +22,9 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- 创建触发器:在 users 表 UPDATE 时自动调用 -- 创建触发器:在 account 表 UPDATE 时自动调用
CREATE TRIGGER trigger_update_users_updated_at CREATE TRIGGER trigger_update_account_updated_at
BEFORE UPDATE BEFORE UPDATE
ON users ON account
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column(); EXECUTE FUNCTION update_updated_at_column();

12
pom.xml
View File

@@ -34,6 +34,11 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.14</version>
</dependency>
<!-- Sa-Token 权限认证在线文档https://sa-token.cc --> <!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency> <dependency>
@@ -41,6 +46,13 @@
<artifactId>sa-token-spring-boot3-starter</artifactId> <artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version> <version>1.44.0</version>
</dependency> </dependency>
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.44.0</version>
</dependency>
<!-- 数据库 --> <!-- 数据库 -->
<dependency> <dependency>

View File

@@ -1,7 +1,5 @@
package com.xapg.energystoragesafety; package com.xapg.energystoragesafety;
import com.mybatisflex.core.keygen.KeyGeneratorFactory;
import com.xapg.energystoragesafety.config.UUIDKeyGenerator;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -9,11 +7,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
@MapperScan("com.xapg.energystoragesafety.mapper") @MapperScan("com.xapg.energystoragesafety.mapper")
public class EnergyStorageSafetyApplication { public class EnergyStorageSafetyApplication {
static { public static void main(String[] args) {
KeyGeneratorFactory.register("myUUID", new UUIDKeyGenerator());
}
public static void main(String[] args) {
SpringApplication.run(EnergyStorageSafetyApplication.class, args); SpringApplication.run(EnergyStorageSafetyApplication.class, args);
} }
} }

View File

@@ -0,0 +1,18 @@
package com.xapg.energystoragesafety.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 自定义业务异常
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {
private int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
}

View File

@@ -0,0 +1,40 @@
package com.xapg.energystoragesafety.config;
import cn.dev33.satoken.exception.NotLoginException;
import com.xapg.energystoragesafety.dto.CommonResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理类
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
// 捕获自定义业务异常
@ExceptionHandler(BusinessException.class)
public CommonResult<?> handleBusinessException(BusinessException e) {
return CommonResult.error(e.getCode(), e.getMessage());
}
// 捕获 SaCheckLogin 登录鉴权
@ExceptionHandler(NotLoginException.class)
public CommonResult<?> handleNotLoginException(NotLoginException e) {
return CommonResult.error(401, "用户未登录");
}
// 捕获空指针等运行时异常
@ExceptionHandler(NullPointerException.class)
public CommonResult<?> handleNullPointerException(NullPointerException e) {
// 实际项目中应记录日志
return CommonResult.error(500, "服务器内部错误NPE");
}
// 捕获所有未处理的异常(兜底)
@ExceptionHandler(Exception.class)
public CommonResult<?> handleGenericException(Exception e) {
// TODO: 记录日志(使用 Slf4j
e.printStackTrace(); // 仅示例,生产环境用 logger
return CommonResult.error(500, "服务器内部错误,请联系管理员");
}
}

View File

@@ -0,0 +1,25 @@
package com.xapg.energystoragesafety.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.jwt.StpLogicJwtForStateless;
import cn.dev33.satoken.stp.StpLogic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// Sa-Token 整合 jwt (Stateless 无状态模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForStateless();
}
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}

View File

@@ -1,14 +0,0 @@
package com.xapg.energystoragesafety.config;
import com.mybatisflex.core.keygen.IKeyGenerator;
import java.util.UUID;
public class UUIDKeyGenerator implements IKeyGenerator {
@Override
public Object generate(Object entity, String keyColumn) {
String id = UUID.randomUUID().toString();
System.out.println("asedasd: " + id);
return id;
}
}

View File

@@ -0,0 +1,50 @@
package com.xapg.energystoragesafety.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.util.SaResult;
import com.xapg.energystoragesafety.dto.CommonResult;
import com.xapg.energystoragesafety.dto.account.request.LoginRequest;
import com.xapg.energystoragesafety.entity.Account;
import com.xapg.energystoragesafety.services.AccountService;
import cn.dev33.satoken.stp.StpUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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;
@RestController
@RequestMapping("/account")
@Tag(name = "用户管理", description = "用户相关操作")
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@Operation(summary = "用户登录", description = "用户登录返回JWT")
@PostMapping("/login")
public CommonResult<String> login(@RequestBody LoginRequest loginRequest) {
// 登录失败,由拦截器拦截
accountService.login(loginRequest.getUsername(), loginRequest.getPassword());
return CommonResult.success(StpUtil.getTokenValue());
}
@PostMapping("/register")
public int register(@RequestBody Account account) {
return accountService.insertUser(account);
}
@PostMapping("/update")
public int update(@RequestBody Account account) {
return accountService.updateByUserName(account);
}
@SaCheckLogin
@PostMapping("/logout")
public void logout() {
StpUtil.logout();
}
}

View File

@@ -0,0 +1,30 @@
package com.xapg.energystoragesafety.dto;
import lombok.Data;
import java.util.Date;
@Data
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
private Long timestamp; // 单位:毫秒
public CommonResult(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis(); // 自动设置当前时间戳
}
// 成功响应
public static <T> CommonResult<T> success(T data) {
return new CommonResult<>(200, "success", data);
}
// 失败响应
public static <T> CommonResult<T> error(int code, String message) {
return new CommonResult<>(code, message, null);
}
}

View File

@@ -0,0 +1,9 @@
package com.xapg.energystoragesafety.dto.account.request;
import lombok.Data;
@Data
public class LoginRequest {
private String username;
private String password;
}

View File

@@ -1,18 +1,23 @@
package com.xapg.energystoragesafety.entity; package com.xapg.energystoragesafety.entity;
import com.mybatisflex.annotation.Id; import com.mybatisflex.annotation.*;
import com.mybatisflex.annotation.KeyType; import com.mybatisflex.core.keygen.KeyGenerators;
import com.mybatisflex.annotation.Table; import com.mybatisflex.core.mask.Masks;
import lombok.Data; import lombok.Data;
import java.sql.Date; import java.sql.Timestamp;
@Data @Data
@Table("account") @Table("account")
public class Account { public class Account {
@Id(keyType = KeyType.Auto) @Id(keyType = KeyType.Generator, value = KeyGenerators.uuid)
private Long id; private String id;
private String userName; private String username;
private Integer age; @ColumnMask(Masks.PASSWORD)
private Date birthday; private String password;
private String nickname;
@Column(onInsertValue = "CURRENT_TIMESTAMP")
private Timestamp createdAt;
@Column(onInsertValue = "CURRENT_TIMESTAMP", onUpdateValue = "CURRENT_TIMESTAMP")
private Timestamp updatedAt;
} }

View File

@@ -1,22 +0,0 @@
package com.xapg.energystoragesafety.entity;
import com.mybatisflex.annotation.*;
import com.mybatisflex.core.mask.Masks;
import lombok.Data;
import java.sql.Timestamp;
@Data
@Table("users")
public class Users {
@Id(keyType = KeyType.Generator, value = "myUUID")
private String id;
private String username;
@ColumnMask(Masks.PASSWORD)
private String password;
private String nickname;
@Column(onInsertValue = "CURRENT_TIMESTAMP")
private Timestamp createdAt;
@Column(onInsertValue = "CURRENT_TIMESTAMP", onUpdateValue = "CURRENT_TIMESTAMP")
private Timestamp updatedAt;
}

View File

@@ -1,7 +0,0 @@
package com.xapg.energystoragesafety.mapper;
import com.mybatisflex.core.BaseMapper;
import com.xapg.energystoragesafety.entity.Users;
public interface UsersMapper extends BaseMapper<Users> {
}

View File

@@ -0,0 +1,81 @@
package com.xapg.energystoragesafety.services;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import com.mybatisflex.core.mask.MaskManager;
import com.mybatisflex.core.query.QueryWrapper;
import com.xapg.energystoragesafety.config.BusinessException;
import com.xapg.energystoragesafety.entity.Account;
import com.xapg.energystoragesafety.mapper.AccountMapper;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicReference;
import static com.xapg.energystoragesafety.entity.table.AccountTableDef.ACCOUNT;
@Service
public class AccountService {
private final AccountMapper accountMapper;
public AccountService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
// 根据用户名查询用户
public Account selectByUsername(String username) {
if (username == null || username.trim().isEmpty()) {
return null;
}
QueryWrapper query = QueryWrapper.create()
.select()
.from(ACCOUNT)
.where(ACCOUNT.USERNAME.eq(username));
return accountMapper.selectOneByQuery(query);
}
// 新增用户
public int insertUser(Account account) {
if (account == null || account.getUsername() == null || account.getUsername().trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
Account exist = this.selectByUsername(account.getUsername());
if (exist != null) {
throw new RuntimeException("用户名已存在");
}
// 默认密码加密
account.setPassword(BCrypt.hashpw(account.getPassword(), BCrypt.gensalt()));
return accountMapper.insert(account);
}
// 更新用户信息(不允许修改用户名,密码)
public int updateByUserName(Account account) {
if (account == null || account.getUsername() == null || account.getUsername().trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
Account old = this.selectByUsername(account.getUsername());
if (old == null) {
throw new RuntimeException("用户不存在");
}
// 保持原用户名
account.setUsername(null);
account.setPassword(null);
return accountMapper.update(account);
}
// 用户登录校验
public void login(String username, String password) {
// 密码校验,取消脱敏处理
MaskManager.execWithoutMask(() -> {
Account account = this.selectByUsername(username);
// 找不到用户,用户名错误
if (account == null) {
throw new BusinessException(401, "用户名或密码错误");
}
// 密码错误
if (!BCrypt.checkpw(password, account.getPassword())) {
throw new BusinessException(401, "用户名或密码错误");
}
StpUtil.login(username);
});
}
}

View File

@@ -1,81 +0,0 @@
package com.xapg.energystoragesafety.services;
import cn.dev33.satoken.secure.BCrypt;
import com.mybatisflex.core.mask.MaskManager;
import com.mybatisflex.core.query.QueryWrapper;
import com.xapg.energystoragesafety.entity.Users;
import com.xapg.energystoragesafety.mapper.UsersMapper;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicReference;
import static com.xapg.energystoragesafety.entity.table.UsersTableDef.USERS;
@Service
public class UsersService {
private final UsersMapper usersMapper;
public UsersService(UsersMapper usersMapper) {
this.usersMapper = usersMapper;
}
// 根据用户名查询用户
public Users selectByUsername(String username) {
if (username == null || username.trim().isEmpty()) {
return null;
}
QueryWrapper query = QueryWrapper.create()
.select()
.from(USERS)
.where(USERS.USERNAME.eq(username));
return usersMapper.selectOneByQuery(query);
}
// 新增用户
public int insertUser(Users user) {
if (user == null || user.getUsername() == null || user.getUsername().trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
Users exist = this.selectByUsername(user.getUsername());
if (exist != null) {
throw new RuntimeException("用户名已存在");
}
// 默认密码加密
user.setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt()));
return usersMapper.insert(user);
}
// 更新用户信息(不允许修改用户名,密码)
public int updateByUserName(Users user) {
if (user == null || user.getUsername() == null || user.getUsername().trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
Users old = this.selectByUsername(user.getUsername());
if (old == null) {
throw new RuntimeException("用户不存在");
}
// 保持原用户名
user.setUsername(null);
user.setPassword(null);
return usersMapper.update(user);
}
// 用户登录校验
public Boolean login(String username, String password) {
AtomicReference<Boolean> isLogin = new AtomicReference<>(Boolean.FALSE);
// 密码校验,取消脱敏处理
MaskManager.execWithoutMask(() -> {
Users user = this.selectByUsername(username);
// 找不到用户,用户名错误
if (user == null) {
throw new RuntimeException("用户名或密码错误");
}
// 密码错误
if (!BCrypt.checkpw(password, user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
}
isLogin.set(Boolean.TRUE);
});
return isLogin.get();
}
}

View File

@@ -2,18 +2,26 @@ spring:
application: application:
name: energy-storage-safety name: energy-storage-safety
springdoc:
swagger-ui:
path: /docs
############## Sa-Token 配置 (文档: https://sa-token.cc) ############## ############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token: sa-token:
# jwt秘钥
jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
# 指定 token 提交时的前缀
token-prefix: Bearer
# token 名称(同时也是 cookie 名称) # token 名称(同时也是 cookie 名称)
token-name: energy-storage-safety token-name: Authorization
# 校验 Header 内容必须设置为true
is-read-header: true
# token 有效期(单位:秒) 默认30天-1 代表永久有效 # token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000 timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1 active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: false
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik # token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: uuid token-style: uuid
# 是否输出操作日志 # 是否输出操作日志

View File

@@ -1,50 +0,0 @@
package com.xapg.energystoragesafety;
import com.mybatisflex.core.query.QueryWrapper;
import com.xapg.energystoragesafety.entity.Users;
import com.xapg.energystoragesafety.mapper.UsersMapper;
import com.xapg.energystoragesafety.services.UsersService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static com.xapg.energystoragesafety.entity.table.UsersTableDef.USERS;
@SpringBootTest
public class UsersTest {
@Autowired
private UsersMapper usersMapper;
@Autowired
private UsersService usersService;
@Test
public void insertTest() {
QueryWrapper query = QueryWrapper.create().where("username = 'focus1'").from(USERS);
usersMapper.deleteByQuery(query);
Users users = new Users();
users.setUsername("focus1");
users.setPassword("plain_password");
users.setNickname("focus1");
usersService.insertUser(users);
}
@Test
public void selectTest() {
// 根据用户名搜索
System.out.println(usersService.selectByUsername("focus1"));
}
@Test
public void updateUser() {
Users users = usersService.selectByUsername("focus1");
users.setNickname("北溪入江流123");
usersService.updateByUserName(users);
}
@Test
public void loginTest() {
System.out.println(usersService.login("focus1", "plain_password"));
}
}

View File

@@ -0,0 +1,50 @@
package com.xapg.energystoragesafety.sql_test;
import com.mybatisflex.core.query.QueryWrapper;
import com.xapg.energystoragesafety.entity.Account;
import com.xapg.energystoragesafety.mapper.AccountMapper;
import com.xapg.energystoragesafety.services.AccountService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static com.xapg.energystoragesafety.entity.table.AccountTableDef.ACCOUNT;
@SpringBootTest
public class AccountTest {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountService accountService;
@Test
public void insertTest() {
QueryWrapper query = QueryWrapper.create().where("username = 'focus1'").from(ACCOUNT);
accountMapper.deleteByQuery(query);
Account account = new Account();
account.setUsername("focus1");
account.setPassword("plain_password");
account.setNickname("focus1");
accountService.insertUser(account);
}
@Test
public void selectTest() {
// 根据用户名搜索
System.out.println(accountService.selectByUsername("focus1"));
}
@Test
public void updateUser() {
Account account = accountService.selectByUsername("focus1");
account.setNickname("北溪入江流123");
accountService.updateByUserName(account);
}
@Test
public void loginTest() {
}
}