java
47 lines · 7 steps
Method-level authorization with @PreAuthorize in Spring
A Spring REST controller guards each endpoint with a different SpEL security rule via @PreAuthorize.
Explained by
highlit
1@RestController
2@RequestMapping("/api/documents")
3@RequiredArgsConstructor
4public class DocumentController {
5
6 private final DocumentService documentService;
7
8 @GetMapping
9 @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
10 public List<DocumentDto> list(@AuthenticationPrincipal UserPrincipal principal) {
11 return documentService.findAccessibleFor(principal.getId());
12 }
13
14 @GetMapping("/{id}")
15 @PreAuthorize("@documentSecurity.canRead(#id, authentication)")
16 public DocumentDto get(@PathVariable Long id) {
17 return documentService.findById(id);
18 }
19
20 @PostMapping
21 @PreAuthorize("hasRole('EDITOR')")
22 public ResponseEntity<DocumentDto> create(@Valid @RequestBody CreateDocumentRequest request,
23 @AuthenticationPrincipal UserPrincipal principal) {
24 DocumentDto created = documentService.create(request, principal.getId());
25 return ResponseEntity.status(HttpStatus.CREATED).body(created);
26 }
27
28 @PutMapping("/{id}")
29 @PreAuthorize("hasRole('EDITOR') and @documentSecurity.isOwner(#id, authentication)")
30 public DocumentDto update(@PathVariable Long id,
31 @Valid @RequestBody UpdateDocumentRequest request) {
32 return documentService.update(id, request);
33 }
34
35 @DeleteMapping("/{id}")
36 @PreAuthorize("hasRole('ADMIN')")
37 @ResponseStatus(HttpStatus.NO_CONTENT)
38 public void delete(@PathVariable Long id) {
39 documentService.delete(id);
40 }
41
42 @PostMapping("/{id}/publish")
43 @PreAuthorize("hasAnyRole('EDITOR', 'ADMIN') and #request.reviewed")
44 public DocumentDto publish(@PathVariable Long id, @RequestBody PublishRequest request) {
45 return documentService.publish(id);
46 }
47}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1@PreAuthorize accepts SpEL, so each endpoint can express its own authorization rule inline.
- 2Beans like @documentSecurity let you move per-resource ownership checks out of the controller body.
- 3Combining role checks with method arguments (#id, #request) enables fine-grained, context-aware access control.
Related explainers
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
java
public class TimedSocketReader { private static final int READ_TIMEOUT_MS = 5_000; private static final int CONNECT_TIMEOUT_MS = 3_000;
Reading a socket with connect and read timeouts
sockets
timeouts
io
Intermediate
8 steps
go
package middleware import ( "net/http"
Per-plan export limits in Gin middleware
middleware
rate-limiting
authorization
Intermediate
7 steps
java
public final class EncodingDetector { public enum Encoding { UTF_8, UTF_16LE, UTF_16BE, UTF_32LE, UTF_32BE, ASCII, UNKNOWN
Detecting text encoding from raw bytes in Java
byte-manipulation
encoding-detection
bitwise-operations
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/method-level-authorization-with-preauthorize-in-spring-explained-java-b7c9/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.