feat(admin): Add admin authentication and user management

This commit is contained in:
2025-04-13 20:34:47 +08:00
parent 36380d50a7
commit 7520d56e0a
27 changed files with 922 additions and 28 deletions
@@ -0,0 +1,15 @@
package com.nuaa.video.admin.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@@ -0,0 +1,38 @@
package com.nuaa.video.admin.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 创建配置好的ObjectMapper
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// 使用Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
@@ -32,6 +32,12 @@ public class AdminUserDetails implements UserDetails {
@JsonIgnore
private String password;
/**
* 盐
*/
@JsonIgnore
private String salt;
/**
* 授权列表
*/
@@ -0,0 +1,57 @@
package com.nuaa.video.admin.security;
import com.nuaa.video.admin.entity.AdminUser;
import com.nuaa.video.admin.service.AdminUserService;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import java.util.ArrayList;
import java.util.List;
@Service
public class AdminUserDetailsServiceImpl implements UserDetailsService {
private final AdminUserService adminUserService;
// 添加@Lazy注解打破循环依赖
public AdminUserDetailsServiceImpl(@Lazy AdminUserService adminUserService) {
this.adminUserService = adminUserService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库加载用户
AdminUser adminUser = adminUserService.getByUsername(username);
if (adminUser == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 构建UserDetails对象
AdminUserDetails userDetails = new AdminUserDetails();
userDetails.setId(adminUser.getId());
userDetails.setUsername(adminUser.getUsername());
userDetails.setPassword(adminUser.getPassword());
userDetails.setSalt(adminUser.getSalt());
// 设置权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 从用户的角色中设置权限...
if (adminUser.getRoles() != null) {
adminUser.getRoles().forEach(role -> {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getCode()));
if (role.getPermissions() != null) {
role.getPermissions().forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission.getCode()))
);
}
});
}
userDetails.setAuthorities(authorities);
return userDetails;
}
}
@@ -0,0 +1,43 @@
package com.nuaa.video.admin.security;
import com.nuaa.video.admin.service.PasswordService;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {
private final PasswordService passwordService;
public CustomAuthenticationProvider(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder,
PasswordService passwordService) {
super();
setUserDetailsService(userDetailsService);
setPasswordEncoder(passwordEncoder);
this.passwordService = passwordService;
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
throw new BadCredentialsException("认证失败:没有提供密码");
}
String presentedPassword = authentication.getCredentials().toString();
// 使用密码服务验证密码,不再需要盐值
if (!passwordService.matches(presentedPassword, "", userDetails.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
}
}
@@ -1,14 +1,17 @@
package com.nuaa.video.admin.security;
import com.nuaa.video.admin.service.PasswordService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@@ -24,11 +27,15 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsService userDetailsService;
private final PasswordService passwordService;
private final PasswordEncoder passwordEncoder; // Inject instead of creating
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// Remove this method
// @Bean
// public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
// }
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
@@ -51,4 +58,13 @@ public class SecurityConfig {
return http.build();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
CustomAuthenticationProvider provider = new CustomAuthenticationProvider(
userDetailsService,
passwordEncoder, // Use injected passwordEncoder
passwordService);
return provider;
}
}
@@ -0,0 +1,39 @@
package com.nuaa.video.admin.service;
import com.nuaa.video.admin.entity.AdminUser;
/**
* 密码处理服务接口 - 使用标准BCrypt方式
*/
public interface PasswordService {
/**
* 生成新的盐值 (BCrypt已内置盐值,此方法保留但不再使用)
* @return 盐值字符串
*/
String generateSalt();
/**
* 加密密码
* @param rawPassword 原始密码
* @param salt 盐值 (BCrypt已内置盐值,此参数不再使用)
* @return 加密后的密码
*/
String encryptPassword(String rawPassword, String salt);
/**
* 验证密码
* @param rawPassword 原始密码
* @param salt 盐值 (BCrypt已内置盐值,此参数不再使用)
* @param encodedPassword 加密后的密码
* @return 是否匹配
*/
boolean matches(String rawPassword, String salt, String encodedPassword);
/**
* 为用户设置密码
* @param user 用户对象
* @param rawPassword 原始密码
*/
void setPassword(AdminUser user, String rawPassword);
}
@@ -0,0 +1,73 @@
package com.nuaa.video.admin.service.impl;
import com.nuaa.video.admin.entity.AdminUser;
import com.nuaa.video.admin.entity.AdminUserRole;
import com.nuaa.video.admin.mapper.AdminUserMapper;
import com.nuaa.video.admin.mapper.AdminUserRoleMapper;
import com.nuaa.video.admin.security.AdminUserDetails;
import com.nuaa.video.admin.service.PasswordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Slf4j
@Service
@RequiredArgsConstructor
public class AdminUserInitService implements ApplicationRunner {
private final AdminUserMapper adminUserMapper;
private final AdminUserRoleMapper adminUserRoleMapper;
private final PasswordService passwordService;
@Value("${app.init.admin-password:admin123}")
private String initAdminPassword;
@Override
public void run(ApplicationArguments args) throws Exception {
initAdminAccount();
}
// 移除 @Override 注解,因为这不是覆盖父类或接口的方法
public void initAdminAccount() {
// 检查是否已存在admin账号
AdminUser existingAdmin = adminUserMapper.selectByUsername("admin");
if (existingAdmin != null) {
log.info("管理员账号已存在,无需初始化");
return;
}
// 创建管理员账号
AdminUser adminUser = new AdminUser();
adminUser.setUsername("admin");
// 使用密码服务设置密码,不再需要手动设置盐值
passwordService.setPassword(adminUser, initAdminPassword);
adminUser.setRealName("系统管理员");
adminUser.setEmail("admin@example.com");
adminUser.setStatus(1); // 正常状态
adminUser.setSalt("aaaaaa"); // 盐值可以设置为默认值,该字段已抛弃
adminUser.setCreatedBy("system");
adminUser.setUpdatedBy("system");
adminUser.setCreateTime(LocalDateTime.now());
adminUser.setUpdateTime(LocalDateTime.now());
// 保存管理员
adminUserMapper.insert(adminUser);
// 创建管理员角色关联
AdminUserRole userRole = new AdminUserRole();
userRole.setAdminId(adminUser.getId());
userRole.setRoleId(1L); // 假设ID为1的角色是超级管理员
userRole.setCreatedBy("system");
userRole.setCreateTime(LocalDateTime.now());
adminUserRoleMapper.insert(userRole);
log.info("管理员账号初始化完成");
}
}
@@ -6,9 +6,12 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.video.admin.dto.AdminUserDTO;
import com.nuaa.video.admin.entity.AdminUser;
import com.nuaa.video.admin.entity.AdminUserRole;
import com.nuaa.video.admin.mapper.AdminPermissionMapper;
import com.nuaa.video.admin.mapper.AdminRoleMapper;
import com.nuaa.video.admin.mapper.AdminUserMapper;
import com.nuaa.video.admin.mapper.AdminUserRoleMapper;
import com.nuaa.video.admin.service.AdminUserService;
import com.nuaa.video.admin.service.PasswordService;
import com.nuaa.video.admin.vo.AdminRoleVO;
import com.nuaa.video.admin.vo.AdminUserVO;
import com.nuaa.video.common.model.vo.Result;
@@ -30,14 +33,19 @@ import java.util.stream.Collectors;
* 管理员服务实现类
*/
@Slf4j
// 在类定义中添加 PasswordService 依赖
@Service
@RequiredArgsConstructor
public class AdminUserServiceImpl extends ServiceImpl<AdminUserMapper, AdminUser> implements AdminUserService {
private final AdminUserMapper adminUserMapper;
private final AdminUserRoleMapper adminUserRoleMapper;
private final PasswordEncoder passwordEncoder;
private final AdminRoleMapper adminRoleMapper;
private final AdminPermissionMapper adminPermissionMapper;
private final PasswordService passwordService;
// 移除 PasswordEncoder 依赖
// private final PasswordEncoder passwordEncoder;
@Override
public AdminUser getByUsername(String username) {
return adminUserMapper.selectByUsername(username);
@@ -137,17 +145,10 @@ public class AdminUserServiceImpl extends ServiceImpl<AdminUserMapper, AdminUser
AdminUser adminUser = new AdminUser();
BeanUtils.copyProperties(adminUserDTO, adminUser);
// 生成盐值
String salt = UUID.randomUUID().toString().replace("-", "");
adminUser.setSalt(salt);
// 加密密码
if (StringUtils.hasText(adminUserDTO.getPassword())) {
adminUser.setPassword(passwordEncoder.encode(adminUserDTO.getPassword()));
} else {
// 默认密码为123456
adminUser.setPassword(passwordEncoder.encode("123456"));
}
// 使用密码服务设置密码 - 使用标准BCrypt方式
String rawPassword = StringUtils.hasText(adminUserDTO.getPassword()) ?
adminUserDTO.getPassword() : "123456";
passwordService.setPassword(adminUser, rawPassword);
// 设置默认状态
if (adminUser.getStatus() == null) {
@@ -214,10 +215,13 @@ public class AdminUserServiceImpl extends ServiceImpl<AdminUserMapper, AdminUser
// 如果密码不为空,则更新密码
if (StringUtils.hasText(adminUserDTO.getPassword())) {
adminUser.setPassword(passwordEncoder.encode(adminUserDTO.getPassword()));
// 使用密码服务设置新密码
adminUser.setSalt(existUser.getSalt()); // 保留原有盐值
passwordService.setPassword(adminUser, adminUserDTO.getPassword());
} else {
// 不更新密码
adminUser.setPassword(null);
adminUser.setSalt(null); // 同时不更新盐值
}
adminUser.setUpdatedBy(operator);
@@ -291,7 +295,11 @@ public class AdminUserServiceImpl extends ServiceImpl<AdminUserMapper, AdminUser
// 更新密码
AdminUser adminUser = new AdminUser();
adminUser.setId(id);
adminUser.setPassword(passwordEncoder.encode(newPassword));
adminUser.setSalt(existUser.getSalt()); // 保留原有盐值
// 使用密码服务设置新密码
passwordService.setPassword(adminUser, newPassword);
adminUser.setUpdatedBy(operator);
adminUser.setUpdateTime(LocalDateTime.now());
@@ -190,6 +190,7 @@ public class AuthServiceImpl implements AuthService {
userDetails.setId(adminUser.getId());
userDetails.setUsername(adminUser.getUsername());
userDetails.setPassword(adminUser.getPassword());
userDetails.setSalt(adminUser.getSalt());
// 设置权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
@@ -0,0 +1,50 @@
package com.nuaa.video.admin.service.impl;
import com.nuaa.video.admin.entity.AdminUser;
import com.nuaa.video.admin.service.PasswordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 密码处理服务实现 - 使用标准BCrypt方式
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PasswordServiceImpl implements PasswordService {
private final PasswordEncoder passwordEncoder;
@Override
public String generateSalt() {
// BCrypt已包含盐值,此方法不再需要
return "";
}
@Override
public String encryptPassword(String rawPassword, String salt) {
// 忽略salt参数,直接使用BCrypt加密
return passwordEncoder.encode(rawPassword);
}
@Override
public boolean matches(String rawPassword, String salt, String encodedPassword) {
// 添加日志
log.debug("密码验证 - 原始密码: [隐藏], 加密密码: {}", encodedPassword);
// 忽略salt参数,直接使用BCrypt验证
boolean result = passwordEncoder.matches(rawPassword, encodedPassword);
log.debug("密码验证结果: {}", result);
return result;
}
@Override
public void setPassword(AdminUser user, String rawPassword) {
// 直接使用BCrypt加密密码
user.setPassword(passwordEncoder.encode(rawPassword));
// 不再需要设置盐值
user.setSalt(null);
}
}
@@ -0,0 +1,53 @@
server:
port: 9004
spring:
application:
name: video-admin
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
namespace: public
group: DEFAULT_GROUP
sentinel:
transport:
dashboard: ${SENTINEL_HOST:sentinel}:${SENTINEL_PORT:8080}
port: 8724
eager: true
datasource:
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/video_admin?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: ${REDIS_HOST:redis}
port: ${REDIS_PORT:6379}
password: redis_EsJ8FX
database: 3
timeout: 10000
lettuce:
pool:
max-active: 200
max-wait: -1
max-idle: 10
min-idle: 0
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.nuaa.video.admin.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: is_deleted
logic-delete-value: 1
logic-not-delete-value: 0
app:
init:
admin-password: admin123