Master Fine-Grained Permission Control in Spring Boot 3 with JWT and SpEL
This article demonstrates how to implement fine‑grained permission checks in Spring Boot 3 without using Spring Security, by creating custom HandlerInterceptors, JWT utilities, DAO and service layers, and integrating SpEL expressions for dynamic authorization, complete with code snippets and test results.
1. Introduction
We implement fine‑grained permission control in Spring Boot 3 without using other security frameworks (e.g., Spring Security). Permissions are checked via a custom HandlerInterceptor and logged, using JPA for user and permission data.
2. Practical Example
2.1 Dependency Management & Configuration
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
</dependency>
</code>Data source and JPA configuration:
<code>spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ddd
username: root
password: xxxooo
type: com.zaxxer.hikari.HikariDataSource
jpa:
generateDdl: false
hibernate:
ddlAuto: none
openInView: true
show-sql: true
</code>2.2 Entity Definitions
<code>@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "user", fetch = FetchType.EAGER)
private Set<Permission> permissions = new HashSet<>();
// getters, setters
}
</code> <code>@Entity
@Table(name = "t_permission")
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(cascade = CascadeType.REFRESH)
@JoinColumn(name = "uid")
private User user;
// getters, setters
}
</code>2.3 DAO & Service
<code>public interface UserRepository extends JpaRepository<User, Long> {
User findByUsernameAndPassword(String username, String password);
}
public interface PermissionRepository extends JpaRepository<Permission, Long> {
List<Permission> findByUser(User user);
}
</code> <code>@Service
public class UserService {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
public UserService(UserRepository userRepository, JwtUtil jwtUtil) {
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
}
public String login(String username, String password) {
User user = userRepository.findByUsernameAndPassword(username, password);
if (user == null) {
throw new RuntimeException("用户名或密码错误");
}
return jwtUtil.generateToken(user.getId());
}
public User queryUser(Long userId) {
return userRepository.findById(userId).orElse(null);
}
}
</code> <code>@Service
public class PermissionService {
private final PermissionRepository permissionRepository;
public PermissionService(PermissionRepository permissionRepository) {
this.permissionRepository = permissionRepository;
}
public List<Permission> findPermissions(Long userId) {
return permissionRepository.findByUser(new User(userId));
}
}
</code>2.4 JWT Utility
<code>@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(Long userId) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, String.valueOf(userId));
}
public Long getUserIdFromToken(String token) {
return Long.valueOf(getClaimFromToken(token, Claims::getSubject));
}
private <T> T getClaimFromToken(String token, Function<Claims, T> resolver) {
Claims claims = getAllClaimsFromToken(token);
return resolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parse(token)
.getPayload();
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.claims()
.add(claims)
.subject(subject)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.and()
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact();
}
}
</code>2.5 Interceptors
<code>@Component
public class TokenInterceptor implements HandlerInterceptor {
private static final String TOKEN_KEY = "X-TOKEN";
private final JwtUtil jwtUtil;
private final UserService userService;
public TokenInterceptor(JwtUtil jwtUtil, UserService userService) {
this.jwtUtil = jwtUtil;
this.userService = userService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader(TOKEN_KEY);
if (!StringUtils.hasLength(token)) {
token = request.getParameter("token");
}
if (!StringUtils.hasLength(token)) {
response.getWriter().println("Invalid token");
return false;
}
Long userId = jwtUtil.getUserIdFromToken(token);
SecurityContext.set(userService.queryUser(userId));
return true;
}
}
</code> <code>@Component
public class AuthInterceptor implements HandlerInterceptor {
private final PermissionService permissionService;
public AuthInterceptor(PermissionService permissionService) {
this.permissionService = permissionService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
PreAuthorize pre = hm.getMethodAnnotation(PreAuthorize.class);
if (pre != null) {
User user = SecurityContext.get();
if (user == null) {
response.getWriter().write("Goto login");
return false;
}
List<String> allowed = Arrays.asList(pre.value());
List<Permission> perms = permissionService.findPermissions(user.getId());
if (!hasAllowedPermission(allowed, perms)) {
response.getWriter().write("Access denied");
return false;
}
}
}
return true;
}
private boolean hasAllowedPermission(List<String> allowed, List<Permission> perms) {
List<String> names = perms.stream().map(Permission::getName).collect(Collectors.toList());
return allowed.stream().anyMatch(names::contains);
}
}
</code>2.6 Interceptor Registration
<code>@Component
public class InterceptorConfig implements WebMvcConfigurer {
private final TokenInterceptor tokenInterceptor;
private final AuthInterceptor authInterceptor;
public InterceptorConfig(TokenInterceptor tokenInterceptor, AuthInterceptor authInterceptor) {
this.tokenInterceptor = tokenInterceptor;
this.authInterceptor = authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).order(-2).addPathPatterns("/api/**");
registry.addInterceptor(authInterceptor).order(-1).addPathPatterns("/api/**");
}
}
</code>2.7 Custom Annotation
<code>@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
String value();
}
</code>2.8 Controllers
<code>@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/login")
public String login(String username, String password) {
return userService.login(username, password);
}
}
</code> <code>@RestController
@RequestMapping("/api")
public class ApiController {
@PreAuthorize("api.save")
@GetMapping("/save")
public Object save() {
return "save";
}
@PreAuthorize("api.update")
@GetMapping("/update")
public Object update() {
return "update";
}
}
</code>2.9 SpEL Integration
AuthInterceptor is enhanced to evaluate SpEL expressions. If the expression returns a string, it is treated as a list of required permissions; if it returns a boolean, the boolean result determines access.
<code>public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod hm) {
PreAuthorize pre = hm.getMethodAnnotation(PreAuthorize.class);
if (pre != null) {
User user = SecurityContext.get();
if (user == null) {
response.getWriter().write("Goto login");
return false;
}
String expr = pre.value();
MethodBasedEvaluationContext ctx = createContext(hm);
Object value = parser.parseExpression(expr).getValue(ctx);
if (value instanceof String) {
List<String> allowed = Arrays.asList(value.toString());
List<Permission> perms = permissionService.findPermissions(user.getId());
if (!hasAllowedPermission(allowed, perms)) {
response.getWriter().write("Access denied");
return false;
}
} else if (value instanceof Boolean ret) {
if (!ret) {
response.getWriter().write("Access denied");
return false;
}
return ret;
}
}
}
return true;
}
</code>Example endpoint using a SpEL expression:
<code>@PreAuthorize("username eq 'admin'")
@GetMapping("/delete")
public Object delete() {
return "delete";
}
</code>When the logged‑in user’s username is admin , the request is allowed; otherwise it is denied.
Test screenshots (omitted) show token acquisition, access without token (error), and successful calls after providing the JWT.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.