There's a moment in every backend project where "anyone can call any endpoint" stops being acceptable, and you have to actually decide who gets to see what. That moment hit me this week, and the answer was Spring Security with role-based access control (RBAC).
Here's what I built: a registration system where a user can sign up as either a regular USER or an ADMIN, and depending on which one they are, they see a completely different slice of the application. A regular user can only view their own basic info — username, email, that's it. An admin, on the other hand, can pull up the full list of every registered user in the system.
Simple to describe. Not quite as simple to get right the first time.
Letting Users Pick a Role at Registration
The registration endpoint takes a role field alongside the usual username/password/email — but I didn't want to just trust whatever the client sends blindly forever, since that's an easy way to let anyone register as an admin. For now, at this stage of the project, the role is accepted at signup and stored against the user, tied to Spring Security's GrantedAuthority model, so every user ends up carrying a ROLE_USER or ROLE_ADMIN authority from the moment their account exists.
That authority is what everything downstream checks against — not a name, not a flag scattered through the code, just one consistent thing Spring Security already knows how to reason about.
Locking Down Endpoints by Role
This is where Spring Security actually earns its keep. Instead of writing if (user.getRole().equals("ADMIN")) checks inside every controller method — which gets messy and easy to forget — the access rules live in the security configuration itself:
-
/api/users/me→ accessible to any authenticated user, returns only their own data -
/api/admin/users→ accessible only toROLE_ADMIN, returns the full user list
If a regular user tries hitting the admin endpoint directly, Spring Security shuts it down before the request even reaches the controller — a clean 403, no leaked data, no custom guard clause needed. That was honestly satisfying to see work correctly on the first real test.
Worth noting: the app is already configured stateless (no server-side sessions), but JWT isn't in the picture yet — right now, authentication happens via HTTP Basic on each request. It works, but it's clearly a placeholder until token-based auth is in place.
The Part That Actually Took Time
The RBAC rules themselves weren't the hard part — wiring up the UserDetailsService correctly so that authorities actually loaded and attached to the authenticated user on every request was where I spent most of my time. It's one of those things where the code looks fine, compiles fine, and then silently doesn't work because the authority string format doesn't match what the security config is checking for (ADMIN vs ROLE_ADMIN cost me a solid chunk of debugging time — Spring Security expects the ROLE_ prefix by convention, and it fails quietly rather than yelling at you about it).
Why This Matters Beyond the Exercise
This is the first time in this project that "the backend works" and "the backend is secure" stopped being the same statement. A working endpoint that returns data to anyone who asks isn't done — it's just functional. Adding the role layer is what actually makes it something you could put in front of real users without immediately regretting it.
Next up: extending this with proper password hashing policies and actually implementing JWT for authentication — the app is already configured stateless, so this is the natural next piece rather than a rework.
Top comments (0)