feat: 增加了 jwt 验证方式。

This commit is contained in:
c
2026-03-18 13:29:56 +08:00
parent 392e74134a
commit 8bb7dcca31
8 changed files with 197 additions and 53 deletions

View File

@@ -70,6 +70,10 @@ dependencies {
// Excel处理
implementation("org.apache.poi:poi:5.2.5")
implementation("org.apache.poi:poi-ooxml:5.2.5")
// Redis
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.apache.commons:commons-pool2")
}
tasks.withType<Test> {

View File

@@ -28,7 +28,9 @@ public class UrlPermissionAuthorizationManager implements AuthorizationManager<R
HttpServletRequest request = context.getRequest();
String requestURI = request.getRequestURI();
if (antPathMatcher.match("/auth/user/me", requestURI)) {
// 公开接口直接放行
if (antPathMatcher.match("/auth/user/me", requestURI)
|| antPathMatcher.match("/open/**", requestURI)) {
return new AuthorizationDecision(true);
}
Authentication auth = authentication.get();

View File

@@ -4,47 +4,66 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.niuan.erp.common.base.BaseResult;
import com.niuan.erp.common.base.LoginUser;
import com.niuan.erp.common.bean.UrlPermissionAuthorizationManager;
import com.niuan.erp.common.security.JwtAuthenticationFilter;
import com.niuan.erp.common.security.TokenService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
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.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
/**
* Security配置类 - 同时支持Session和JWT两种认证方式
* - Web端后台管理使用传统Session方式基于Cookie
* - 小程序端使用JWT方式基于Token
*/
@Configuration
@EnableWebSecurity // 启用 Web 安全
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UrlPermissionAuthorizationManager urlPermissionAuthorizationManager;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final TokenService tokenService;
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, SessionRegistry sessionRegistry) throws Exception {
http
// 👇 1. 启用 CSRF
// 禁用 CSRFJWT需要禁用Session方式通过其他方式防护
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 👇 2. 设置 Session
// 设置 Session支持Session方式同时允许JWT无状态请求
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry))
// 👇 3. 请求授权规则
// 请求授权规则
.authorizeHttpRequests(authz -> authz
.requestMatchers(
"/v3/api-docs/**", "/v3/api-docs",
@@ -52,24 +71,25 @@ public class SecurityConfig {
"/swagger-ui.html",
"/webjars/**",
"/doc.html",
"/favicon.ico").permitAll()
"/favicon.ico",
"/open/**",
"/api/auth/**").permitAll() // 小程序登录接口公开
.anyRequest().authenticated()
)
//
// 表单登录配置Web端使用
.formLogin(form -> form
.loginProcessingUrl("/auth/login") // 指定登录提交地址
.loginProcessingUrl("/auth/login")
.successHandler((request, response, authentication) -> {
// 登录成功:返回 JSON
// Web端登录成功:返回简单JSON
response.setContentType("application/json;charset=UTF-8");
LoginUser user = (LoginUser) authentication.getPrincipal();
response.getWriter().write(new ObjectMapper().writeValueAsString(BaseResult.success()));
})
.failureHandler((request, response, exception) -> {
// 登录失败:返回 JSON
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(BaseResult.error(3, exception.getMessage())));
response.getWriter().write(new ObjectMapper().writeValueAsString(
BaseResult.error(3, exception.getMessage())));
})
)
.logout(logout -> logout
@@ -78,47 +98,50 @@ public class SecurityConfig {
.deleteCookies("JSESSIONID")
.clearAuthentication(true)
.logoutSuccessHandler((request, response, authentication) -> {
// Web端登出
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(BaseResult.success()));
})
)
// 👇 6. 异常处理(认证失败 / 权限不足)
// 异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(401);
response.getWriter().write("Unauthorized: " + authException.getMessage());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(
BaseResult.error(HttpStatus.UNAUTHORIZED.value(), "未认证,请先登录")));
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(403);
response.getWriter().write("Forbidden: " + accessDeniedException.getMessage());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(
BaseResult.error(HttpStatus.FORBIDDEN.value(), "无权访问")));
})
);
// 添加JWT过滤器在UsernamePasswordAuthenticationFilter之前
// 这样JWT请求可以绕过Session认证直接通过Token认证
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public ConcurrentSessionFilter concurrentSessionFilter(SessionRegistry sessionRegistry) {
return new ConcurrentSessionFilter(sessionRegistry);
}
// 👇 提供 CORS 配置(生产环境请严格限制 origin
// 👇 提供 CORS 配置
@Bean
@Primary
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 允许的前端 origin(必须是具体地址,不能 *
// 允许的前端 origin
configuration.setAllowedOriginPatterns(List.of("http://localhost:*", "http://127.0.0.1:*"));
// ⚠️ 注意Spring Boot 2.4+ 用 allowedOriginPatterns 支持通配符
// 允许凭证Cookie
// 允许凭证Cookie
configuration.setAllowCredentials(true);
// 允许的方法和头
@@ -130,27 +153,4 @@ public class SecurityConfig {
source.registerCorsConfiguration("/**", configuration);
return source;
}
// 👇 密码编码器(必须)
// @Bean
// public PasswordEncoder passwordEncoder() {
// return md5PasswordEncoder;
// }
// 👇 AuthenticationProvider用于 DaoAuthenticationProvider
// @Bean
// public AuthenticationProvider authenticationProvider() {
// DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// authProvider.setUserDetailsService(userDetailsService);
// authProvider.setPasswordEncoder(passwordEncoder());
// return authProvider;
// }
// 👇 AuthenticationManager用于登录时 authenticate
// @Bean
// public AuthenticationManager authenticationManager(
// org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration config)
// throws Exception {
// return config.getAuthenticationManager();
// }
}

View File

@@ -3,7 +3,28 @@ package com.niuan.erp.module.sys.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.niuan.erp.module.sys.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
/**
* 根据角色ID查询用户ID列表
*/
@Select("SELECT DISTINCT ur.UserId FROM yy_usersrolemapping ur WHERE ur.RoleId = #{roleId}")
List<Long> selectUserIdsByRoleId(@Param("roleId") Long roleId);
/**
* 根据权限ID查询用户ID列表
*/
@Select("""
SELECT DISTINCT ur.UserId
FROM yy_usersrolemapping ur
INNER JOIN yy_rolepermission rp ON ur.RoleId = rp.RoleId
WHERE rp.PermissionId = #{permissionId}
""")
List<Long> selectUserIdsByPermissionId(@Param("permissionId") Long permissionId);
}

View File

@@ -3,6 +3,7 @@ package com.niuan.erp.module.sys.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.niuan.erp.common.base.BasePageReqParams;
import com.niuan.erp.common.base.LoginUser;
import com.niuan.erp.module.sys.controller.dto.SysUserDto;
import com.niuan.erp.module.sys.entity.SysUser;
@@ -21,4 +22,19 @@ public interface SysUserService {
void deleteBatch(List<Long> ids);
void setStatus(Long id, Integer status);
/**
* 根据用户ID加载用户信息
*/
LoginUser loadUserById(Long userId);
/**
* 根据角色ID查询用户ID列表
*/
List<Long> getUserIdsByRoleId(Long roleId);
/**
* 根据权限ID查询用户ID列表
*/
List<Long> getUserIdsByPermissionId(Long permissionId);
}

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.niuan.erp.common.base.BasePageReqParams;
import com.niuan.erp.common.security.TokenService;
import com.niuan.erp.common.utils.SecurityUtils;
import com.niuan.erp.module.sys.controller.dto.SysRoleDto;
import com.niuan.erp.module.sys.controller.dto.SysRoleSelectDto;
@@ -15,6 +16,7 @@ import com.niuan.erp.module.sys.mapper.RolePermissionMapper;
import com.niuan.erp.module.sys.mapper.SysPermissionMapper;
import com.niuan.erp.module.sys.mapper.SysRoleMapper;
import com.niuan.erp.module.sys.service.SysRoleService;
import com.niuan.erp.module.sys.service.SysUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -34,6 +36,10 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
private final RolePermissionMapper rolePermissionMapper;
private final TokenService tokenService;
private final SysUserService sysUserService;
@Override
public IPage<SysRoleDto> getSysRolePage(BasePageReqParams dto, LambdaQueryWrapper<SysRole> wrapper) {
IPage<SysRole> result = this.baseMapper.selectPage(new Page<>(dto.page(), dto.pageSize()), wrapper);
@@ -87,15 +93,34 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
// 使用循环单条插入,避免无主键实体使用批量 insert 方法的问题
newPermissions.forEach(rolePermissionMapper::insert);
}
// 角色修改后,踢掉所有拥有该角色的用户
List<Long> userIds = sysUserService.getUserIdsByRoleId(entity.getId());
if (userIds != null && !userIds.isEmpty()) {
userIds.forEach(tokenService::removeAllUserTokens);
}
}
@Override
public void deleteSysRole(Long id) {
// 角色删除前,踢掉所有拥有该角色的用户
List<Long> userIds = sysUserService.getUserIdsByRoleId(id);
if (userIds != null && !userIds.isEmpty()) {
userIds.forEach(tokenService::removeAllUserTokens);
}
this.baseMapper.deleteById(id);
}
@Override
public void deleteBatch(List<Long> ids) {
if (ids != null && !ids.isEmpty()) {
// 批量删除前,踢掉所有拥有这些角色的用户
for (Long roleId : ids) {
List<Long> userIds = sysUserService.getUserIdsByRoleId(roleId);
if (userIds != null && !userIds.isEmpty()) {
userIds.forEach(tokenService::removeAllUserTokens);
}
}
}
this.baseMapper.deleteByIds(ids);
}
@@ -104,6 +129,11 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
SysRole entity = this.baseMapper.selectById(id);
entity.setStatus(status);
this.baseMapper.updateById(entity);
// 角色状态修改后,踢掉所有拥有该角色的用户
List<Long> userIds = sysUserService.getUserIdsByRoleId(id);
if (userIds != null && !userIds.isEmpty()) {
userIds.forEach(tokenService::removeAllUserTokens);
}
}
@Override

View File

@@ -5,18 +5,24 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.niuan.erp.common.base.BasePageReqParams;
import com.niuan.erp.common.base.LoginUser;
import com.niuan.erp.common.exception.BusinessException;
import com.niuan.erp.common.security.TokenService;
import com.niuan.erp.common.utils.SecurityUtils;
import com.niuan.erp.module.sys.controller.dto.SysUserDto;
import com.niuan.erp.module.sys.converter.SysUserConverter;
import com.niuan.erp.module.sys.entity.SysPermission;
import com.niuan.erp.module.sys.entity.SysRole;
import com.niuan.erp.module.sys.entity.SysUser;
import com.niuan.erp.module.sys.entity.UserRole;
import com.niuan.erp.module.sys.mapper.SysPermissionMapper;
import com.niuan.erp.module.sys.mapper.SysRoleMapper;
import com.niuan.erp.module.sys.mapper.SysUserMapper;
import com.niuan.erp.module.sys.mapper.UserRoleMapper;
import com.niuan.erp.module.sys.service.SysUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -38,6 +44,10 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
private final SysRoleMapper sysRoleMapper;
private final SysPermissionMapper sysPermissionMapper;
private final TokenService tokenService;
private final PasswordEncoder passwordEncoder;
@Override
@@ -175,10 +185,16 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
// 更新用户角色关联
updateUserRoles(id, dto.roleIds());
// 用户修改后,踢掉该用户
tokenService.removeAllUserTokens(id);
}
@Override
public void deleteUser(Long id) {
// 用户删除前,踢掉该用户
tokenService.removeAllUserTokens(id);
// 删除用户角色关联
LambdaQueryWrapper<UserRole> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserRole::getUserId, id);
@@ -194,6 +210,9 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
return;
}
// 批量删除前,踢掉这些用户
ids.forEach(tokenService::removeAllUserTokens);
// 删除用户角色关联
LambdaQueryWrapper<UserRole> wrapper = new LambdaQueryWrapper<>();
wrapper.in(UserRole::getUserId, ids);
@@ -218,6 +237,9 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
updateUser.setUpdateDate(LocalDateTime.now());
this.baseMapper.updateById(updateUser);
// 用户状态修改后,踢掉该用户
tokenService.removeAllUserTokens(id);
}
private void checkLoginNameExists(String loginName, Long excludeId) {
@@ -258,4 +280,35 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
// 保存新的角色关联
saveUserRoles(userId, roleIds);
}
@Override
public LoginUser loadUserById(Long userId) {
SysUser user = this.baseMapper.selectById(userId);
if (user == null) {
return null;
}
List<SysRole> roleList = sysRoleMapper.selectByUserId(userId);
List<SysPermission> permissionList = sysPermissionMapper.selectByUserId(userId);
List<GrantedAuthority> authorities = permissionList.stream()
.filter(p -> StringUtils.hasText(p.getPermissionCode()))
.map(p -> new SimpleGrantedAuthority(p.getPermissionCode()))
.collect(Collectors.toUnmodifiableList());
return LoginUser.builder()
.user(user)
.roleList(roleList)
.authorities(authorities)
.build();
}
@Override
public List<Long> getUserIdsByRoleId(Long roleId) {
return this.baseMapper.selectUserIdsByRoleId(roleId);
}
@Override
public List<Long> getUserIdsByPermissionId(Long permissionId) {
return this.baseMapper.selectUserIdsByPermissionId(permissionId);
}
}

View File

@@ -10,6 +10,24 @@ spring:
url: jdbc:mysql://localhost:3306/ai
username: steven
password: 781203
data:
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 1000ms
# jwt配置
jwt:
secret: your-256-bit-secret-key-here-must-be-at-least-32-characters
expiration: 86400000
refresh-expiration: 604800000
# mybatis 设置
mybatis-plus: