This is what I came up with on my own:
1) Access Control via Lightning component
<aura:component controller="AccessCtrl">
<c:checkAccess conditionA="{!c.foo}" .../>
...
</aura:component>
<c:checkAccess />
would check the conditions in Apex on component init() and throw an AuraHandledException if the user has no access.
I really liked that solution as it clearly separates access control from the components that use it. No other Apex or Lightning controller is sprinkled with access logic.
But it's fake security because its client side. And client side code can always be hacked. Just open the browser debugger and route the javascript to ignore the response or not even do the callback.
So what next?
2) Check in all @AuraEnabled methods
public class MySecureComponentCtrl {
@AuraEnabled
public static void businessLogic(Id accountId, List<Contact> contactsToDelete) {
verifyUserHasAccess(accountId);
delete contactsToDelete;
}
...
private static void verifyUserHasAccess(Id recordId) {
if(...) {
throw new AuraHandleException('no access');
}
}
}
But even that is not sufficient as I might just pass in a different account id which I have access to and delete totally unrelated contacts.

3) Check not before but while you are doing relevant stuff
public class MySecureComponentCtrl {
@AuraEnabled
public static void businessLogic(Id accountId, List<Contact> contactsToDelete) {
verifyIsAllowedToDelete(accountId, contactsToDelete);
delete contactsToDelete;
}
...
private static void verifyIsAllowedToDelete(Id accountId, List<Contact> contactsToDelete) {
if(...) {
throw new AuraHandleException('no access');
}
}
}