From bce483507cd920ca313b436cdead75bd82823f73 Mon Sep 17 00:00:00 2001 From: xiang Date: Sun, 31 Aug 2025 23:40:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E8=87=AA=E5=AE=9A=E4=B9=89=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/api/dto/resp/PermissionRoleDTO.java | 26 +++ .../main/java/com/xiang/AuthApplication.java | 2 + .../config/AuthorizationBeanConfig.java | 22 +++ .../config/AuthorizationServerConfig.java | 33 ++-- .../config/CustomAccessDecisionManager.java | 49 +++++ .../config/CustomSecurityMetadataSource.java | 72 ++++++++ .../auth/service/constants/RedisConstant.java | 6 + .../auth/service/entity/XPermission.java | 72 ++++++++ .../auth/service/entity/XRolePermission.java | 13 ++ .../repository/mapper/XPermissionMapper.java | 28 +++ .../mapper/XRolePermissionMapper.java | 11 ++ .../service/impl/XUserServiceImpl.java | 3 +- .../resources/mapper/user/XPermission.xml | 170 ++++++++++++++++++ .../mapper/user/XRolePermissionMapper.xml | 22 +++ 14 files changed, 516 insertions(+), 13 deletions(-) create mode 100644 xs-api/src/main/java/com/xiang/xservice/auth/api/dto/resp/PermissionRoleDTO.java create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationBeanConfig.java create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/config/CustomAccessDecisionManager.java create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/config/CustomSecurityMetadataSource.java create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/constants/RedisConstant.java create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XPermission.java create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XRolePermission.java create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/repository/mapper/XPermissionMapper.java create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/repository/mapper/XRolePermissionMapper.java create mode 100644 xs-service/src/main/resources/mapper/user/XPermission.xml create mode 100644 xs-service/src/main/resources/mapper/user/XRolePermissionMapper.xml diff --git a/xs-api/src/main/java/com/xiang/xservice/auth/api/dto/resp/PermissionRoleDTO.java b/xs-api/src/main/java/com/xiang/xservice/auth/api/dto/resp/PermissionRoleDTO.java new file mode 100644 index 0000000..593c29e --- /dev/null +++ b/xs-api/src/main/java/com/xiang/xservice/auth/api/dto/resp/PermissionRoleDTO.java @@ -0,0 +1,26 @@ +package com.xiang.xservice.auth.api.dto.resp; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PermissionRoleDTO { + + /** + * 接口路由地址 + */ + private String apiUrl; + + /** + * 请求方式 GET/POST/... + */ + private String method; + + /** + * 权限编码 + */ + private String roleCode; +} diff --git a/xs-server/src/main/java/com/xiang/AuthApplication.java b/xs-server/src/main/java/com/xiang/AuthApplication.java index b7a6143..5fb62af 100644 --- a/xs-server/src/main/java/com/xiang/AuthApplication.java +++ b/xs-server/src/main/java/com/xiang/AuthApplication.java @@ -2,8 +2,10 @@ package com.xiang; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @SpringBootApplication +@EnableMethodSecurity(prePostEnabled = true) public class AuthApplication { public static void main(String[] args) { SpringApplication.run(AuthApplication.class, args); diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationBeanConfig.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationBeanConfig.java new file mode 100644 index 0000000..1d8532b --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationBeanConfig.java @@ -0,0 +1,22 @@ +package com.xiang.xservice.auth.service.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.stereotype.Component; + +@Component +public class AuthorizationBeanConfig { + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); + // 默认从 "scope" 取,这里改成你自定义的 "authorities" + authoritiesConverter.setAuthoritiesClaimName("authorities"); + authoritiesConverter.setAuthorityPrefix(""); // 不要加 "SCOPE_" + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + return jwtAuthenticationConverter; + } +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationServerConfig.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationServerConfig.java index d471bf0..7f59356 100644 --- a/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationServerConfig.java +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationServerConfig.java @@ -16,6 +16,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; @@ -33,7 +34,9 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import java.time.Duration; import java.util.Objects; @@ -50,6 +53,7 @@ public class AuthorizationServerConfig { private final JdbcTemplate jdbcTemplate; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final JwtAuthenticationConverter jwtAuthenticationConverter; @Bean @Order(1) @@ -61,7 +65,9 @@ public class AuthorizationServerConfig { @Bean @Order(2) - public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, + CustomSecurityMetadataSource metadataSource, + CustomAccessDecisionManager decisionManager) throws Exception { http .csrf().disable() // 禁用 CSRF .authorizeRequests(authorizeRequests -> authorizeRequests @@ -69,18 +75,23 @@ public class AuthorizationServerConfig { .antMatchers("/open/**").permitAll() .antMatchers("/private/**").authenticated() .anyRequest().authenticated() + .withObjectPostProcessor(new ObjectPostProcessor() { + @Override + public O postProcess(O object) { + object.setSecurityMetadataSource(metadataSource); + object.setAccessDecisionManager(decisionManager); + return object; + } + }) ) - .exceptionHandling(exception -> - exception - .authenticationEntryPoint(customAuthenticationEntryPoint) - .accessDeniedHandler(customAccessDeniedHandler)) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .oauth2ResourceServer(oauth -> - oauth - .jwt() - .and() - .authenticationEntryPoint(customAuthenticationEntryPoint) - .accessDeniedHandler(customAccessDeniedHandler)); + .oauth2ResourceServer(oauth2 -> + oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter)) + ); return http.build(); } diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/config/CustomAccessDecisionManager.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/CustomAccessDecisionManager.java new file mode 100644 index 0000000..bf755d2 --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/CustomAccessDecisionManager.java @@ -0,0 +1,49 @@ +package com.xiang.xservice.auth.service.config; + + +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +/** + * 访问决策器 + * 决定用户是否有权访问 + */ +@Component +public class CustomAccessDecisionManager implements AccessDecisionManager { + @Override + public void decide(Authentication authentication, Object object, + Collection configAttributes) throws AccessDeniedException { + + if (configAttributes == null || configAttributes.isEmpty()) { + return; // 没有限制,直接放行 + } + + Collection userAuthorities = authentication.getAuthorities(); + + for (ConfigAttribute attribute : configAttributes) { + String requiredRole = attribute.getAttribute(); + for (GrantedAuthority authority : userAuthorities) { + if (requiredRole.trim().equals(authority.getAuthority())) { + return; // 匹配成功 + } + } + } + throw new AccessDeniedException("用户没有权限!"); + } + + @Override + public boolean supports(ConfigAttribute attribute) { + return true; + } + + @Override + public boolean supports(Class clazz) { + return true; + } +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/config/CustomSecurityMetadataSource.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/CustomSecurityMetadataSource.java new file mode 100644 index 0000000..4b1e3a4 --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/CustomSecurityMetadataSource.java @@ -0,0 +1,72 @@ +package com.xiang.xservice.auth.service.config; + +import com.google.common.collect.Maps; +import com.xiang.xservice.auth.api.dto.resp.PermissionRoleDTO; +import com.xiang.xservice.auth.service.repository.mapper.XPermissionMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 动态权限数据源 (核心) + * 当前请求需要哪些角色。 + */ +@Component +@RequiredArgsConstructor +public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { + private final XPermissionMapper permissionMapper; + private final Map> permissionMap = Maps.newHashMap(); + + + @PostConstruct + public void loadPermissionMap() { + Map> map = loadPermission(); + permissionMap.putAll(map); + } + + @Override + public Collection getAttributes(Object object) throws IllegalArgumentException { + HttpServletRequest request = ((FilterInvocation) object).getRequest(); + String requestUrl = request.getRequestURI(); + String method = request.getMethod(); + String key = requestUrl + ":" + method; + return permissionMap.get(key); + } + + @Override + public Collection getAllConfigAttributes() { + return permissionMap.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + } + + @Override + public boolean supports(Class clazz) { + return FilterInvocation.class.isAssignableFrom(clazz); + } + + /** + * 从数据库加载所有 URL → 对应角色 的映射 + * @return Map> + */ + private Map> loadPermission() { + Map> map = new HashMap<>(); + List list = permissionMapper.loadAllPermission(); + for (PermissionRoleDTO dto : list) { + String key = dto.getApiUrl() + ":" + dto.getMethod(); + map.computeIfAbsent(key, k -> new ArrayList<>()) + .add(new SecurityConfig(dto.getRoleCode())); + } + return map; + } +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/constants/RedisConstant.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/constants/RedisConstant.java new file mode 100644 index 0000000..9fea9f1 --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/constants/RedisConstant.java @@ -0,0 +1,6 @@ +package com.xiang.xservice.auth.service.constants; + +public class RedisConstant { + + public static final String XS_PERMISSION_ROLE = "auth:permission:role"; +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XPermission.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XPermission.java new file mode 100644 index 0000000..e378167 --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XPermission.java @@ -0,0 +1,72 @@ +package com.xiang.xservice.auth.service.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class XPermission { + /** + * id + */ + private Long id; + + /** + * 名称 + */ + private String name; + + /** + * 编码 + */ + private String code; + + /** + * 类型 1=菜单 2=按钮 3=接口 + */ + private Integer type; + + /** + * 父类id + */ + private Long parentId; + + /** + * 路径接口 + */ + private String apiPath; + + /** + * 请求方法 + */ + private String method; + + /** + * 创建时间 + */ + private LocalDateTime createdTime; + + /** + * 创建人 + */ + private String createBy; + + /** + * 修改时间 + */ + private LocalDateTime updatedTime; + + /** + * 修改人 + */ + private String updateBy; + + /** + * 删除标识(0:未删除 1:已删除) + */ + private Integer delFlag; +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XRolePermission.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XRolePermission.java new file mode 100644 index 0000000..68e2b03 --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XRolePermission.java @@ -0,0 +1,13 @@ +package com.xiang.xservice.auth.service.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class XRolePermission { + private Long roleId; + private Long permissionId; +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/repository/mapper/XPermissionMapper.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/repository/mapper/XPermissionMapper.java new file mode 100644 index 0000000..2c1d1b0 --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/repository/mapper/XPermissionMapper.java @@ -0,0 +1,28 @@ +package com.xiang.xservice.auth.service.repository.mapper; + +import com.xiang.xservice.auth.api.dto.resp.PermissionRoleDTO; +import com.xiang.xservice.auth.service.entity.XPermission; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@Mapper +public interface XPermissionMapper { + + List getPermissionList(XPermission permission); + + List getPermissionByIds(@Param("id") List ids); + + XPermission getPermissionById(@Param("id") Long id); + + int insert(XPermission permission); + + int update(XPermission permission); + + int delBatch(@Param("list") List ids, @Param("operator") String operator); + + List loadAllPermission(); +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/repository/mapper/XRolePermissionMapper.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/repository/mapper/XRolePermissionMapper.java new file mode 100644 index 0000000..ac0ad4c --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/repository/mapper/XRolePermissionMapper.java @@ -0,0 +1,11 @@ +package com.xiang.xservice.auth.service.repository.mapper; + +import com.xiang.xservice.auth.service.entity.XRolePermission; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface XRolePermissionMapper { + + List getRolePermissionsByRoleId(@Param("roleId") Long roleId); +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/service/impl/XUserServiceImpl.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/service/impl/XUserServiceImpl.java index cdb12f0..66cdb64 100644 --- a/xs-service/src/main/java/com/xiang/xservice/auth/service/service/impl/XUserServiceImpl.java +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/service/impl/XUserServiceImpl.java @@ -24,7 +24,6 @@ import com.xiang.xservice.auth.service.repository.mapper.XUserRoleMapper; import com.xiang.xservice.auth.service.service.XUserService; import com.xiang.xservice.basic.enums.DelStatusEnum; import com.xiang.xservice.basic.exception.BusinessException; -import com.xiang.xservice.basic.utils.JsonUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -94,7 +93,7 @@ public class XUserServiceImpl implements XUserService { // 自定义 scope .claim("timestamp", System.currentTimeMillis()) .claim("username", request.getUsername()) - .claim("roles", JsonUtils.toJsonString(roleCodes)) + .claim("authorities", roleCodes) .build(); // 2. 编码生成 token diff --git a/xs-service/src/main/resources/mapper/user/XPermission.xml b/xs-service/src/main/resources/mapper/user/XPermission.xml new file mode 100644 index 0000000..97dbd2e --- /dev/null +++ b/xs-service/src/main/resources/mapper/user/XPermission.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + id, + name, + code, + type, + parent_id, + api_path, + method, + created_time, + create_by, + updated_time, + update_by, + del_flag + + + + INSERT INTO x_permission + + + name, + + + code, + + + type, + + + parent_id, + + + api_path, + + + method, + + + created_time, + + + create_by, + + + updated_time, + + + update_by, + + + del_flag + + + + + #{name}, + + + #{code}, + + + #{type}, + + + #{parentId}, + + + #{apiPath}, + + + #{method}, + + + #{createdTime}, + + + #{createBy}, + + + #{updatedTime}, + + + #{updateBy}, + + + #{delFlag} + + + + + + update x_permission set del_flag = 1, update_by = #{operator}, update_time = NOW() + where id in + + #{id} + + + + + UPDATE x_permission + + name = #{name}, + code = #{code}, + type = #{type}, + parent_id = #{parentId}, + api_path = #{apiPath}, + method = #{method}, + created_time = #{createdTime}, + create_by = #{createBy}, + updated_time = #{updatedTime}, + update_by = #{updateBy}, + del_flag = #{delFlag} + + WHERE id = #{id} + + + + + + + \ No newline at end of file diff --git a/xs-service/src/main/resources/mapper/user/XRolePermissionMapper.xml b/xs-service/src/main/resources/mapper/user/XRolePermissionMapper.xml new file mode 100644 index 0000000..f8d8fb6 --- /dev/null +++ b/xs-service/src/main/resources/mapper/user/XRolePermissionMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + role_id, + permission_id + + + + + \ No newline at end of file