7 Practical Ways to Secure Spring Boot APIs with Permission Control

This article walks through seven concrete implementations for controlling access to Spring Boot 3.5.0 controller endpoints—including Spring Security, Sa‑Token, custom annotation with AOP, interceptor, URL‑based filter, custom HandlerMapping, and a Spring Cloud Gateway filter—providing code samples, configuration details, and test results to help developers choose the right approach for API security.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
7 Practical Ways to Secure Spring Boot APIs with Permission Control

1. Introduction

In Spring Boot development, controlling API permissions is a core security measure. Complex role hierarchies (admin, user, guest) require fine‑grained restrictions to prevent unauthorized access and data leaks. This article presents seven mainstream ways to protect controller endpoints, balancing security and development efficiency.

2. Implementation Cases

2.1 Based on Spring Security

Dependency

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Static user configuration (application.yml)

spring:
  security:
    user:
      name: pack
      password: 123123
      roles:
        - USERS
        - MGR

Security configuration

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain packSecurityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable());
        http.authorizeHttpRequests(registry -> {
            registry.requestMatchers("/resources/**").permitAll();
            registry.requestMatchers("/api/**").authenticated();
        });
        http.formLogin(withDefaults());
        http.exceptionHandling(ehc -> {
            ehc.accessDeniedPage("/resources/noauthority.html");
        });
        return http.build();
    }
}

Controller example

@GetMapping("/query")
@PreAuthorize("hasRole('USERS')")
public ResponseEntity<String> query() {
    return ResponseEntity.ok("query");
}

2.2 Based on Sa‑Token

Sa‑Token is a lightweight Java permission framework that supports Spring Boot 2/3.

Dependency

<dependency>
  <groupId>cn.dev33</groupId>
  <artifactId>sa-token-spring-boot3-starter</artifactId>
  <version>1.45.0</version>
</dependency>

Simulated login endpoint

@GetMapping("/login")
public ResponseEntity<String> login() {
    if (!StpUtil.isLogin()) {
        StpUtil.login("xxxooo", 15000);
    }
    return ResponseEntity.ok(StpUtil.getTokenValueByLoginId("xxxooo"));
}

Custom StpInterface to provide roles

@Component
public class SaPermissionStpInterface implements StpInterface {
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        return List.of();
    }
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return List.of("ADMIN", "MGR");
    }
}

Interceptor to enforce permission

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }
}

Controller example

@GetMapping("/query")
@SaCheckRole(value = {"USERS"})
public ResponseEntity<String> query() {
    return ResponseEntity.ok("query");
}

2.3 Custom Annotation + AOP

When a heavyweight security framework is not desired, a custom annotation combined with AOP can enforce permissions.

Annotation definition

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
    String[] value();
}

AOP aspect

@Aspect
@Component
public class PermissionAspect {
    @Before("@annotation(permission)")
    public void checkPermission(JoinPoint joinPoint, Permission permission) {
        String[] neededPerm = permission.value();
        Set<String> permissions = UserContext.getPermissions();
        if (Collections.disjoint(permissions, Set.copyOf(Arrays.asList(neededPerm)))) {
            throw new AccessDeniedException("权限不足");
        }
    }
}

// Simulated user context
public class UserContext {
    public static Set<String> getPermissions() {
        return Set.of("user.create", "user.update", "user.query");
    }
}

Controller example

@GetMapping("/query")
@Permission({"api.query"})
public ResponseEntity<String> query() {
    return ResponseEntity.ok("query");
}

2.4 Custom Annotation + Interceptor

This approach moves the check to the web layer using HandlerInterceptor.

Interceptor definition

@Component
public class PermissionInterceptor implements HandlerInterceptor {
    private final ObjectMapper objectMapper;
    public PermissionInterceptor(ObjectMapper objectMapper) { this.objectMapper = objectMapper; }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) return true;
        HandlerMethod hm = (HandlerMethod) handler;
        Permission permission = hm.getMethodAnnotation(Permission.class);
        if (permission != null) {
            String[] neededPerm = permission.value();
            Set<String> permissions = UserContext.getPermissions();
            if (Collections.disjoint(permissions, Set.copyOf(Arrays.asList(neededPerm)))) {
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().println(objectMapper.writeValueAsString(Map.of("code", -1, "msg", "无权限")));
                return false;
            }
        }
        return true;
    }
}

MvcConfigurer to register the interceptor

@Configuration
public class PermissionMvcConfigurer implements WebMvcConfigurer {
    private final PermissionInterceptor permissionInterceptor;
    public PermissionMvcConfigurer(PermissionInterceptor permissionInterceptor) {
        this.permissionInterceptor = permissionInterceptor;
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(permissionInterceptor).addPathPatterns("/api/**");
    }
}

Controller example (same as AOP case)

@GetMapping("/query")
@Permission({"api.query"})
public ResponseEntity<String> query() {
    return ResponseEntity.ok("query");
}

2.5 URL‑Path Matching Filter

Extending OncePerRequestFilter allows permission checks based on the request URI.

Filter implementation

@Component
public class PermissionFilter extends OncePerRequestFilter {
    private static final Map<String, Set<String>> permissions = new HashMap<>();
    static {
        permissions.put("/api/query", Set.of("user.query"));
        permissions.put("/api/create", Set.of("user.create"));
    }
    private final ObjectMapper objectMapper;
    public PermissionFilter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestUri = request.getRequestURI();
        Set<String> userPerms = UserContext.getPermissions();
        if (!Collections.disjoint(userPerms, permissions.get(requestUri))) {
            filterChain.doFilter(request, response);
            return;
        }
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().println(objectMapper.writeValueAsString(Map.of("code", -1, "msg", "无权限")));
    }
}

Controller endpoints

@GetMapping("/query")
public ResponseEntity<String> query() { return ResponseEntity.ok("query"); }

@GetMapping("/create")
public ResponseEntity<String> create() { return ResponseEntity.ok("create"); }

Simulated user permissions

public class UserContext {
    public static Set<String> getPermissions() {
        return Set.of("user.query"); // adjust per test case
    }
}

2.6 Custom HandlerMapping (high‑performance option)

The article references a separate write‑up that replaces the core MVC mapping component for maximum performance. The detailed code is omitted here but can be found in the linked article.

2.7 Spring Cloud Gateway Permission Validation

A dedicated gateway service can perform token parsing and permission checks before routing to downstream services.

Dependency

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>

Gateway route configuration (application.yml)

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: path_route
              uri: http://localhost:8080
              predicates:
                - Path=/api-1/**
              filters:
                - StripPrefix=1

GlobalFilter implementation

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class PermissionGlobalFilter implements GlobalFilter {
    private static final Map<String, Set<String>> permissions = new HashMap<>();
    private static final Map<String, Set<String>> path_permissions = new HashMap<>();
    static {
        permissions.put("xxxooo", Set.of("user.query"));
        permissions.put("aaabbb", Set.of("user.create"));
        path_permissions.put("/api-1/api/query", Set.of("user.query"));
        path_permissions.put("/api-1/api/create", Set.of("user.create"));
    }
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getRawPath();
        String token = request.getQueryParams().getFirst("token");
        if (!StringUtils.hasLength(token)) {
            return writeErrorResponse(exchange, "缺少Token,请先登录");
        }
        Set<String> requiredPermission = path_permissions.get(path);
        if (requiredPermission == null || requiredPermission.isEmpty()) {
            return chain.filter(exchange);
        }
        Set<String> userPermissions = permissions.get(token);
        if (Collections.disjoint(requiredPermission, userPermissions)) {
            return writeErrorResponse(exchange, "无权限");
        }
        return chain.filter(exchange);
    }
    private Mono<Void> writeErrorResponse(ServerWebExchange exchange, String message) {
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        String result = "{\"code\": -1, \"msg\": \"" + message + "\"}";
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
        return exchange.getResponse().writeWith(Mono.just(buffer));
    }
}

Test screenshots (omitted here) demonstrate successful token‑based access and proper error responses when permissions are insufficient.

Conclusion

The seven approaches range from using the built‑in Spring Security framework to lightweight custom solutions and a dedicated gateway filter. Developers can select a method based on project size, performance requirements, and architectural preferences.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AOPSpring BootgatewayAPI Securitypermission-controlSpring SecuritySa-Token
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.