feat:自定义接口权限

This commit is contained in:
xiang
2025-08-31 23:40:23 +08:00
parent 2296a997c9
commit bce483507c
14 changed files with 516 additions and 13 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> 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();
}

View File

@@ -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<ConfigAttribute> configAttributes) throws AccessDeniedException {
if (configAttributes == null || configAttributes.isEmpty()) {
return; // 没有限制,直接放行
}
Collection<? extends GrantedAuthority> 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;
}
}

View File

@@ -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<String, Collection<ConfigAttribute>> permissionMap = Maps.newHashMap();
@PostConstruct
public void loadPermissionMap() {
Map<String, Collection<ConfigAttribute>> map = loadPermission();
permissionMap.putAll(map);
}
@Override
public Collection<ConfigAttribute> 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<ConfigAttribute> 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<String, Collection<ConfigAttribute>>
*/
private Map<String, Collection<ConfigAttribute>> loadPermission() {
Map<String, Collection<ConfigAttribute>> map = new HashMap<>();
List<PermissionRoleDTO> 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;
}
}

View File

@@ -0,0 +1,6 @@
package com.xiang.xservice.auth.service.constants;
public class RedisConstant {
public static final String XS_PERMISSION_ROLE = "auth:permission:role";
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<XPermission> getPermissionList(XPermission permission);
List<XPermission> getPermissionByIds(@Param("id") List<Long> ids);
XPermission getPermissionById(@Param("id") Long id);
int insert(XPermission permission);
int update(XPermission permission);
int delBatch(@Param("list") List<Long> ids, @Param("operator") String operator);
List<PermissionRoleDTO> loadAllPermission();
}

View File

@@ -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<XRolePermission> getRolePermissionsByRoleId(@Param("roleId") Long roleId);
}

View File

@@ -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

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiang.xservice.auth.service.repository.mapper.XPermissionMapper">
<resultMap id="BaseResultMap" type="com.xiang.xservice.auth.service.entity.XPermission" >
<result column="id" property="id" />
<result column="name" property="name" />
<result column="code" property="code" />
<result column="type" property="type" />
<result column="parent_id" property="parentId" />
<result column="api_path" property="apiPath" />
<result column="method" property="method" />
<result column="created_time" property="createdTime" />
<result column="create_by" property="createBy" />
<result column="updated_time" property="updatedTime" />
<result column="update_by" property="updateBy" />
<result column="del_flag" property="delFlag" />
</resultMap>
<sql id="Base_Column_List">
id,
name,
code,
type,
parent_id,
api_path,
method,
created_time,
create_by,
updated_time,
update_by,
del_flag
</sql>
<insert id="insert" useGeneratedKeys="true" keyColumn="id" keyProperty="id" parameterType="com.xiang.xservice.auth.service.entity.XPermission">
INSERT INTO x_permission
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="null != name and '' != name">
name,
</if>
<if test="null != code and '' != code">
code,
</if>
<if test="null != type ">
type,
</if>
<if test="null != parentId ">
parent_id,
</if>
<if test="null != apiPath and '' != apiPath">
api_path,
</if>
<if test="null != method and '' != method">
method,
</if>
<if test="null != createdTime ">
created_time,
</if>
<if test="null != createBy and '' != createBy">
create_by,
</if>
<if test="null != updatedTime ">
updated_time,
</if>
<if test="null != updateBy and '' != updateBy">
update_by,
</if>
<if test="null != delFlag ">
del_flag
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="null != name and '' != name">
#{name},
</if>
<if test="null != code and '' != code">
#{code},
</if>
<if test="null != type ">
#{type},
</if>
<if test="null != parentId ">
#{parentId},
</if>
<if test="null != apiPath and '' != apiPath">
#{apiPath},
</if>
<if test="null != method and '' != method">
#{method},
</if>
<if test="null != createdTime ">
#{createdTime},
</if>
<if test="null != createBy and '' != createBy">
#{createBy},
</if>
<if test="null != updatedTime ">
#{updatedTime},
</if>
<if test="null != updateBy and '' != updateBy">
#{updateBy},
</if>
<if test="null != delFlag ">
#{delFlag}
</if>
</trim>
</insert>
<update id="delBatch" >
update x_permission set del_flag = 1, update_by = #{operator}, update_time = NOW()
where id in
<foreach collection="list" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</update>
<update id="update" parameterType="com.xiang.xservice.auth.service.entity.XPermission">
UPDATE x_permission
<set>
<if test="null != name and '' != name">name = #{name},</if>
<if test="null != code and '' != code">code = #{code},</if>
<if test="null != type ">type = #{type},</if>
<if test="null != parentId ">parent_id = #{parentId},</if>
<if test="null != apiPath and '' != apiPath">api_path = #{apiPath},</if>
<if test="null != method and '' != method">method = #{method},</if>
<if test="null != createdTime ">created_time = #{createdTime},</if>
<if test="null != createBy and '' != createBy">create_by = #{createBy},</if>
<if test="null != updatedTime ">updated_time = #{updatedTime},</if>
<if test="null != updateBy and '' != updateBy">update_by = #{updateBy},</if>
<if test="null != delFlag ">del_flag = #{delFlag}</if>
</set>
WHERE id = #{id}
</update>
<select id="getPermissionList" resultMap="BaseResultMap">
select <include refid="Base_Column_List"/>
from x_permission
<trim prefix="AND">
<where>
del_flag = 0
<if test="name != null and name != ''">name = #{name}</if>
<if test="code != null and code != ''">code = #{code}</if>
<if test="type != null">type = #{type}</if>
<if test="method != null and method != ''">method = #{method}</if>
<if test="apiPath != null and apiPath != ''">api_path = #{apiPath}</if>
</where>
</trim>
</select>
<select id="getPermissionByIds" resultMap="BaseResultMap">
select <include refid="Base_Column_List"/>
from x_permission
where id in
<foreach collection="list" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
<select id="getPermissionById" resultMap="BaseResultMap">
select <include refid="Base_Column_List"/>
from x_permission
where id = #{id}
</select>
<select id="loadAllPermission" resultType="com.xiang.xservice.auth.api.dto.resp.PermissionRoleDTO">
select p.api_path api_url, p.method, r.code as role_code
from x_permission p
join x_role_permission rp on p.id = rp.permission_id
join x_role r on rp.role_id = r.id
where p.del_flag = 0 and r.del_flag = 0 and r.status = 1
</select>
</mapper>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiang.xservice.auth.service.repository.mapper.XRolePermissionMapper">
<resultMap id="BaseResultMap" type="com.xiang.xservice.auth.service.entity.XRolePermission" >
<result property="roleId" column="role_id"/>
<result property="permissionId" column="permission_id"/>
</resultMap>
<sql id="Base_Column_List">
role_id,
permission_id
</sql>
<select id="getRolePermissionsByRoleId" resultMap="BaseResultMap">
select <include refid="Base_Column_List"/>
from x_role_permission
where role_id = #{roleId}
</select>
</mapper>