Treating security as a separate concern, and handling it in a layer above application logic is a good practice.
One project I was on was a great example. It involved a family of web services. The evolution of our authorization check went like this (it's a bit Java-centric, but hopefully the idea is clear):
- First as a shared jar bundled into all the services.
However, as we tweaked the security logic, it forced us to rebuild and redeploy everything. Yuck.
- Next a shared jar that implemented a JEE Filter, bundled with all the services.
This was better, but any tweak still required us to rebuild & redeploy everything.
- Next as a Tomcat filter that we deployed with Tomcat, outside the .war file
Better still. But our tweaks still required a deployment & reboot of the Tomcat servers.
- Finally as a reverse proxy that ran as its own service separate from the other services, which took all requests, and authorized and forwarded valid requests down to the right server, or else returned an "unauthorized" error.
And this worked like a charm, because changing the authorization concerns only required a rebuild and redeploy of one service -- the reverse proxy.
So, see, in that case we ended up abstracting security up and out of the service altogether.
I agree with your feeling that pushing security higher in the call stack as a gate makes the implementation logic much cleaner and easier to handle. If you combine application logic with security checks, there's the risk of doing extra work before finding out (whoops) they aren't allowed to do step 3. Or worse, it might be a rollback situation where the app actually has to un-do what it did in step 2.
It's valuable to treat your security as a separate layer -- whether that layer is in code, a servlet filter, or an upstream proxy. Even if the security is a shopping list of actions, determine what they want to do up front, and then validate they are authorized to do all that before any actual work begins. That provides a really important separation of concerns.
Another practice that I've found that greatly simplifies security concerns is tying security rules to "roles" or "scopes". You cold say, client X wants to do operations
X, Y and Z, and that requires scope
M and P. A quick lookup (possibly cached) answers quickly whether the operations they want to do are covered by the scopes they have.
How broad or granular the scopes is just a design question, but allows you to be broad or granular as needed.