diff --git a/build.gradle.kts b/build.gradle.kts index 3b07427..02e0869 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/src/main/java/com/niuan/erp/common/bean/UrlPermissionAuthorizationManager.java b/src/main/java/com/niuan/erp/common/bean/UrlPermissionAuthorizationManager.java index 2683039..e788f2d 100644 --- a/src/main/java/com/niuan/erp/common/bean/UrlPermissionAuthorizationManager.java +++ b/src/main/java/com/niuan/erp/common/bean/UrlPermissionAuthorizationManager.java @@ -28,7 +28,9 @@ public class UrlPermissionAuthorizationManager implements AuthorizationManager 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(); -// } } diff --git a/src/main/java/com/niuan/erp/module/sys/mapper/SysUserMapper.java b/src/main/java/com/niuan/erp/module/sys/mapper/SysUserMapper.java index a78793d..07e61dc 100644 --- a/src/main/java/com/niuan/erp/module/sys/mapper/SysUserMapper.java +++ b/src/main/java/com/niuan/erp/module/sys/mapper/SysUserMapper.java @@ -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 { + + /** + * 根据角色ID查询用户ID列表 + */ + @Select("SELECT DISTINCT ur.UserId FROM yy_usersrolemapping ur WHERE ur.RoleId = #{roleId}") + List 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 selectUserIdsByPermissionId(@Param("permissionId") Long permissionId); } \ No newline at end of file diff --git a/src/main/java/com/niuan/erp/module/sys/service/SysUserService.java b/src/main/java/com/niuan/erp/module/sys/service/SysUserService.java index c848831..f1d0315 100644 --- a/src/main/java/com/niuan/erp/module/sys/service/SysUserService.java +++ b/src/main/java/com/niuan/erp/module/sys/service/SysUserService.java @@ -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 ids); void setStatus(Long id, Integer status); + + /** + * 根据用户ID加载用户信息 + */ + LoginUser loadUserById(Long userId); + + /** + * 根据角色ID查询用户ID列表 + */ + List getUserIdsByRoleId(Long roleId); + + /** + * 根据权限ID查询用户ID列表 + */ + List getUserIdsByPermissionId(Long permissionId); } diff --git a/src/main/java/com/niuan/erp/module/sys/service/impl/SysRoleServiceImpl.java b/src/main/java/com/niuan/erp/module/sys/service/impl/SysRoleServiceImpl.java index e396aac..9360481 100644 --- a/src/main/java/com/niuan/erp/module/sys/service/impl/SysRoleServiceImpl.java +++ b/src/main/java/com/niuan/erp/module/sys/service/impl/SysRoleServiceImpl.java @@ -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 impl private final RolePermissionMapper rolePermissionMapper; + private final TokenService tokenService; + + private final SysUserService sysUserService; + @Override public IPage getSysRolePage(BasePageReqParams dto, LambdaQueryWrapper wrapper) { IPage result = this.baseMapper.selectPage(new Page<>(dto.page(), dto.pageSize()), wrapper); @@ -87,15 +93,34 @@ public class SysRoleServiceImpl extends ServiceImpl impl // 使用循环单条插入,避免无主键实体使用批量 insert 方法的问题 newPermissions.forEach(rolePermissionMapper::insert); } + // 角色修改后,踢掉所有拥有该角色的用户 + List userIds = sysUserService.getUserIdsByRoleId(entity.getId()); + if (userIds != null && !userIds.isEmpty()) { + userIds.forEach(tokenService::removeAllUserTokens); + } } @Override public void deleteSysRole(Long id) { + // 角色删除前,踢掉所有拥有该角色的用户 + List userIds = sysUserService.getUserIdsByRoleId(id); + if (userIds != null && !userIds.isEmpty()) { + userIds.forEach(tokenService::removeAllUserTokens); + } this.baseMapper.deleteById(id); } @Override public void deleteBatch(List ids) { + if (ids != null && !ids.isEmpty()) { + // 批量删除前,踢掉所有拥有这些角色的用户 + for (Long roleId : ids) { + List 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 impl SysRole entity = this.baseMapper.selectById(id); entity.setStatus(status); this.baseMapper.updateById(entity); + // 角色状态修改后,踢掉所有拥有该角色的用户 + List userIds = sysUserService.getUserIdsByRoleId(id); + if (userIds != null && !userIds.isEmpty()) { + userIds.forEach(tokenService::removeAllUserTokens); + } } @Override diff --git a/src/main/java/com/niuan/erp/module/sys/service/impl/SysUserServiceImpl.java b/src/main/java/com/niuan/erp/module/sys/service/impl/SysUserServiceImpl.java index 35764ab..a7e98eb 100644 --- a/src/main/java/com/niuan/erp/module/sys/service/impl/SysUserServiceImpl.java +++ b/src/main/java/com/niuan/erp/module/sys/service/impl/SysUserServiceImpl.java @@ -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 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 impl // 更新用户角色关联 updateUserRoles(id, dto.roleIds()); + + // 用户修改后,踢掉该用户 + tokenService.removeAllUserTokens(id); } @Override public void deleteUser(Long id) { + // 用户删除前,踢掉该用户 + tokenService.removeAllUserTokens(id); + // 删除用户角色关联 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(UserRole::getUserId, id); @@ -194,6 +210,9 @@ public class SysUserServiceImpl extends ServiceImpl impl return; } + // 批量删除前,踢掉这些用户 + ids.forEach(tokenService::removeAllUserTokens); + // 删除用户角色关联 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(UserRole::getUserId, ids); @@ -218,6 +237,9 @@ public class SysUserServiceImpl extends ServiceImpl 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 impl // 保存新的角色关联 saveUserRoles(userId, roleIds); } + + @Override + public LoginUser loadUserById(Long userId) { + SysUser user = this.baseMapper.selectById(userId); + if (user == null) { + return null; + } + + List roleList = sysRoleMapper.selectByUserId(userId); + List permissionList = sysPermissionMapper.selectByUserId(userId); + List 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 getUserIdsByRoleId(Long roleId) { + return this.baseMapper.selectUserIdsByRoleId(roleId); + } + + @Override + public List getUserIdsByPermissionId(Long permissionId) { + return this.baseMapper.selectUserIdsByPermissionId(permissionId); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index af1bc01..f191eeb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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: