Implementing Fine-Grained Permission Control with Spring Security and OAuth2 (Part 4)

This article walks through building a Spring Security resource server with OAuth2, enabling method‑level, object‑level and URL‑level permission checks using annotations like @PreAuthorize, @PostAuthorize, @PostFilter, and demonstrates configuration, utility helpers, controller examples, testing steps, best practices, and common pitfalls.

Coder Trainee
Coder Trainee
Coder Trainee
Implementing Fine-Grained Permission Control with Spring Security and OAuth2 (Part 4)

Goal of This Part

After completing the OAuth2 authorization‑code flow and third‑party login in the previous episode, this fourth installment focuses on the authorization phase: how to restrict what an authenticated user can do by implementing fine‑grained permission control in a Spring Boot resource server.

Core Permission Annotations

Spring Security provides several SpEL‑based annotations for declarative access control: @PreAuthorize – checks before method execution (e.g., @PreAuthorize("hasRole('ADMIN')")). @PostAuthorize – checks after method execution, useful for validating the returned object. @PreFilter – filters method arguments before execution. @PostFilter – filters collection results after execution. @Secured – simple role‑based check.

Enabling Method Security

package com.teaching.resource.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ResourceServerConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                // public endpoints
                .requestMatchers("/api/public/**", "/actuator/health").permitAll()
                // all other endpoints require authentication; actual checks are done via annotations
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}

Utility Class for Current User Information

package com.teaching.resource.util;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;

@Component
public class SecurityUtils {
    /** Get current user ID from JWT claim */
    public static Long getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) return null;
        Object principal = authentication.getPrincipal();
        if (principal instanceof Jwt jwt) {
            return jwt.getClaim("user_id");
        }
        return null;
    }
    /** Get current username */
    public static String getCurrentUsername() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication == null ? null : authentication.getName();
    }
    /** Get current roles */
    @SuppressWarnings("unchecked")
    public static List<String> getCurrentRoles() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) return List.of();
        Object principal = authentication.getPrincipal();
        if (principal instanceof Jwt jwt) {
            return jwt.getClaim("roles");
        }
        return authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .toList();
    }
    /** Check if user has ADMIN role */
    public static boolean isAdmin() {
        Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        return authorities.stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("admin"));
    }
    /** Check if the current user matches the given ID */
    public static boolean isSelf(Long userId) {
        Long current = getCurrentUserId();
        return current != null && current.equals(userId);
    }
}

Article Controller – Permission Examples

package com.teaching.resource.controller;

import com.teaching.resource.util.SecurityUtils;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.*;

@RestController
@RequestMapping("/api/article")
public class ArticleController {
    // Role‑based access – only ADMIN can list all articles
    @GetMapping("/admin/list")
    @PreAuthorize("hasRole('ADMIN')")
    public List<Map<String, Object>> adminList() {
        return List.of(
                Map.of("id", 1, "title", "文章1"),
                Map.of("id", 2, "title", "文章2")
        );
    }
    // ADMIN or EDITOR can create
    @PostMapping("/create")
    @PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')")
    public Map<String, Object> createArticle(@RequestBody Map<String, Object> article) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "文章创建成功");
        result.put("articleId", System.currentTimeMillis());
        result.put("author", SecurityUtils.getCurrentUsername());
        return result;
    }
    // Delete requires write scope or ADMIN
    @DeleteMapping("/{id}")
    @PreAuthorize("hasAuthority('SCOPE_message.write') or hasRole('ADMIN')")
    public Map<String, Object> deleteArticle(@PathVariable Long id) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "文章删除成功");
        result.put("articleId", id);
        return result;
    }
    // Object‑level: users can only see their own articles
    @GetMapping("/my")
    @PreAuthorize("isAuthenticated()")
    public List<Map<String, Object>> getMyArticles() {
        Long userId = SecurityUtils.getCurrentUserId();
        return List.of(
                Map.of("id", 1, "title", "我的文章1", "authorId", userId),
                Map.of("id", 2, "title", "我的文章2", "authorId", userId)
        );
    }
    // Post‑authorize: ADMIN or owner can view details
    @GetMapping("/{id}")
    @PostAuthorize("hasRole('ADMIN') or returnObject.authorId == authentication.principal.getClaim('user_id')")
    public Map<String, Object> getArticle(@PathVariable Long id) {
        Map<String, Object> article = new HashMap<>();
        article.put("id", id);
        article.put("title", "文章标题 " + id);
        article.put("content", "文章内容...");
        article.put("authorId", 1001L); // simulated author
        return article;
    }
    // Method‑level filtering – only return own data
    @GetMapping("/list")
    @PreAuthorize("isAuthenticated()")
    @PostFilter("filterObject.authorId == authentication.principal.getClaim('user_id')")
    public List<Map<String, Object>> getAllArticles() {
        return List.of(
                Map.of("id", 1, "title", "文章1", "authorId", 1001L),
                Map.of("id", 2, "title", "文章2", "authorId", 1002L),
                Map.of("id", 3, "title", "文章3", "authorId", 1001L)
        );
    }
}

User Controller – Demonstrating Permission Checks

package com.teaching.resource.controller;

import com.teaching.resource.util.SecurityUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
import java.util.*;

@RestController
@RequestMapping("/api/user")
public class UserController {
    // Any authenticated user can query own info
    @GetMapping("/me")
    @PreAuthorize("isAuthenticated()")
    public Map<String, Object> getCurrentUser(Authentication authentication) {
        Jwt jwt = (Jwt) authentication.getPrincipal();
        Map<String, Object> result = new HashMap<>();
        result.put("username", jwt.getSubject());
        result.put("userId", jwt.getClaim("user_id"));
        result.put("roles", jwt.getClaim("roles"));
        result.put("department", jwt.getClaim("department"));
        result.put("loginTime", jwt.getClaim("login_time"));
        result.put("scopes", jwt.getClaim("scope"));
        return result;
    }
    // ADMIN or self can view a specific user
    @GetMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @securityUtils.isSelf(#id)")
    public Map<String, Object> getUser(@PathVariable Long id) {
        Map<String, Object> result = new HashMap<>();
        result.put("id", id);
        result.put("username", "user_" + id);
        result.put("email", "user" + id + "@example.com");
        return result;
    }
    // Only the owner can update
    @PutMapping("/{id}")
    @PreAuthorize("@securityUtils.isSelf(#id)")
    public Map<String, Object> updateUser(@PathVariable Long id, @RequestBody Map<String, Object> userInfo) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "用户信息更新成功");
        result.put("userId", id);
        return result;
    }
    // Admin‑only endpoint
    @GetMapping("/admin/users")
    @PreAuthorize("hasRole('ADMIN')")
    public Map<String, Object> listAllUsers() {
        Map<String, Object> result = new HashMap<>();
        result.put("users", new String[]{"user1", "user2", "admin"});
        result.put("total", 3);
        return result;
    }
    // VIP data – requires specific authority or ADMIN
    @GetMapping("/vip/data")
    @PreAuthorize("hasAuthority('SCOME_vip') or hasRole('ADMIN')")
    public Map<String, Object> getVipData() {
        Map<String, Object> result = new HashMap<>();
        result.put("data", "这是 VIP 专属数据");
        return result;
    }
}

Dynamic URL‑Level Permission Configuration

package com.teaching.resource.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import java.util.function.Supplier;

@Configuration
public class DynamicSecurityConfig {
    @Bean
    public AuthorizationManager<RequestAuthorizationContext> requestAuthorizationManager(HandlerMappingIntrospector introspector) {
        RequestMatcher adminMatcher = new MvcRequestMatcher(introspector, "/api/admin/**");
        RequestMatcher userMatcher = new MvcRequestMatcher(introspector, "/api/user/**");
        return new RequestMatcherDelegatingAuthorizationManager.Builder()
                .add(adminMatcher, (authentication, context) -> {
                    Supplier<Boolean> isAdmin = () -> authentication.get().getAuthorities().stream()
                            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
                    return new AuthorizationDecision(isAdmin.get());
                })
                .add(userMatcher, (authentication, context) -> new AuthorizationDecision(authentication.get().isAuthenticated()))
                .build();
    }
}

Testing the Permission Setup

Obtain tokens for a normal user and an admin using the password grant, then invoke the endpoints with Authorization: Bearer <token> headers. Expected results:

Normal user calling /api/article/admin/list → 403 Forbidden.

Normal user calling /api/article/my → 200 OK with only own articles.

Normal user trying to delete an article without message.write scope → 403.

Best Practices

Use role‑based checks ( @PreAuthorize("hasRole('ADMIN')")) for coarse‑grained control.

Combine authority checks and SpEL expressions for fine‑grained logic (e.g., owner‑only access).

Cache permission data with Spring Cache to avoid repeated DB hits.

Avoid heavy @PostFilter on large collections; prefer SQL‑level filtering.

Common Pitfalls and Solutions

@PreAuthorize not working: Ensure @EnableMethodSecurity(prePostEnabled = true) is present on a configuration class.

Difference between hasRole and hasAuthority: hasRole('ADMIN') checks for the ROLE_ prefix, while hasAuthority('admin') checks the exact authority string.

Performance of @PostFilter: It filters in memory; for large datasets move the filter into the database query.

Series Summary

This fourth article completes the OAuth2 authorization series, covering method‑level security, object‑level filtering, and dynamic URL permissions. The previous parts introduced the authorization server, deep‑dive into JWT, and third‑party login integrations.

Author "老 J" invites readers to suggest topics for the next series in the comments.

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.

JavaSpring BootjwtOAuth2permission-controlSpring Securitymethod-security
Coder Trainee
Written by

Coder Trainee

Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.

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.