Building a Generic Web Authorization Annotation with JDK8: From Design to Compile-Time Validation

This article details the implementation of a generic @UserPermission annotation for horizontal authorization in Java web applications, covering annotation design, AOP-based interception, reflection-based parameter extraction, and compile-time validation using JDK8 annotation processors to prevent configuration errors.

Java Captain
Java Captain
Java Captain
Building a Generic Web Authorization Annotation with JDK8: From Design to Compile-Time Validation

Background

The author's company-internal Java application lacked horizontal authorization at the web layer, allowing users to access data from unauthorized companies. A penetration test exposed this risk, prompting a fix. With many web interfaces, the author sought a generic, easy-to-adopt solution using annotations, and used the opportunity to learn annotation development from scratch.

Authorization Scenario

A company can authorize one or multiple users; a user can be authorized by one or multiple companies. Users may only view data of authorized companies.

Architecture

The architecture diagram (referenced in source) shows the flow: Controller → AOP Aspect → UserPermissionManager → External Company-User Relationship Platform.

Implementation

Annotation Definition

The @UserPermission annotation targets TYPE and METHOD, retained at RUNTIME. Attributes: objectType (AuthObjectTypeEnum): COMPANY (single) or COMPANIES (multiple), default COMPANY valueType (AuthObjectValueEnum): RARE (raw parameter), OBJECT_FIELD, COLLECTION_FIELD, OBJECT_SUB_FIELD, OBJECT_COLLECTION_FIELD, default RARE index (int): parameter index (0-based), used when valueType != RARE, default 0 paramName (String): parameter or field name, dot-separated for nested fields, default "companyId" isIgnore (boolean): skip authorization when true, default false

Two enums define the allowed values with integer codes and Chinese descriptions.

Permission Manager

UserPermissionManager

wraps a Feign client to the company-user platform: checkCompany(userName, companyId) → single company check checkCompanies(userName, companyIds) → batch check with deduplication via stream().distinct(); if size==1 delegates to single check

Private checkResult validates ResultVO<Boolean> (non-null, success code, non-null data) and returns BooleanUtils.isTrue(data); throws BizException on failure

AOP Aspect

UserPermissionAspect

defines a pointcut on

execution(public * com.yourcompany.xxproject.web.controller..*.*(..))

and an @Around advice:

Resolves method/class annotations; method annotation overrides class annotation.

If no annotation or isIgnore(), proceeds directly.

Extracts UserInfoVO from request attribute "userVo"; throws if missing or username blank.

Admin users (via UserUtil.isAdminOrSystem) bypass authorization.

Calls extractAuthValue to obtain the authorization value(s) from method arguments based on annotation config.

Calls checkUserPermission which routes to manager's checkCompany or checkCompanies depending on objectType.

On denial, logs error and throws BizException with user-friendly message.

Parameter Extraction Logic

extractAuthValue

handles five valueType cases:

RARE : scans parameter names for paramName match, returns that argument.

OBJECT_FIELD : gets argument at index, uses reflection ( PropertyDescriptor) to invoke getter for paramName.

COLLECTION_FIELD : expects a Collection at index, extracts field from each element via getter, returns List<Long>.

OBJECT_SUB_FIELD : splits paramName by ".", first gets sub-object (e.g., companyInfo), then extracts nested field (e.g., companyId).

OBJECT_COLLECTION_FIELD : splits paramName, gets collection field from object, then extracts field from each collection element.

Helper methods: getLongValue handles Long, Integer, String → long; getLongListValue converts Collection to List<Long> via mapping. Reflection uses standard getters; no fallback to direct field access.

Design Rationale

Simplest case: parameter named companyId in signature → just @UserPermission.

Reduce repetition: place annotation on Controller class for common patterns.

Deduplicate company IDs before external call to reduce provider load.

Admin bypass handled in aspect (could also be in manager).

Prefer getter via PropertyDescriptor for POJO controller params; reflection only for getter invocation.

Usage Examples

A table maps nine scenarios to annotation attribute values:

First param, default name func(long companyId) → all defaults.

First param, ID list func(List<Long> companyIds) → objectType=COMPANIES, valueType=RARE, paramName=companyIds.

First param's field func(TaBO bo) with bo.companyId → valueType=OBJECT_FIELD.

First param's field list bo.companyIds → objectType=COMPANIES, valueType=OBJECT_FIELD, paramName=companyIds.

Second param's field func(TaBO, TbBO) with bbo.companyId → index=1.

First collection param's element field func(List<A> list) with abo.companyId → valueType=COLLECTION_FIELD.

Nested object field abo.companyInfo.companyId → valueType=OBJECT_SUB_FIELD, paramName=companyInfo.companyId.

Object's collection field elements abo.companyInfoList with companyInfo.companyId → valueType=OBJECT_COLLECTION_FIELD, paramName=companyInfoList.companyId.

Override class-level annotation to skip: isIgnore=true (other attributes arbitrary).

Edge Case Handling

Scenario: Task queryTask(Long taskId) where companyId is a property of Task (not in signature). Two options: extend annotation or manual code. Author chose manual: query Task, get companyId, call userPermissionManager directly. Reason: few such interfaces; extending annotation adds maintenance cost.

Compile-Time Validation

Constraints: valueType COLLECTION_FIELD or OBJECT_COLLECTION_FIELD requires objectType=COMPANIES; index >= 0. To catch misconfiguration at compile time, the author uses a JDK8 AbstractProcessor:

Move annotation code (except aspect) to a separate module; business module depends on it; ensure annotation module compiles first.

Implement UserPermissionProcessor with @SupportedAnnotationTypes and @SupportedSourceVersion(RELEASE_8). In process(), iterate annotated elements and validate: checkValueTypeObjectTypeConstraint: extracts enum values via processingEnv.getElementUtils().getElementValuesWithDefaults, parses enum strings (format "java: pkg.Enum.CONSTANT") using reflection to get actual enum instances, verifies the constraint. checkIndexConstraint: ensures index value ≥ 0.

Errors reported via

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, ...)

.

Register processor in META-INF/services/javax.annotation.processing.Processor with fully qualified class name.

Verification: IDE build shows error on violating annotation usage.

Future Iterations

Rename COMPANIES to MULTI_COMPANIES to avoid autocomplete confusion with COMPANY.

Combine horizontal (data) and vertical (role) authorization, possibly via another annotation.

Package as a second-party library; encapsulate remote-call details per microservice framework; optimize performance early as adoption grows.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavareflectionJDK8spring-aopWeb SecurityAnnotation ProcessingauthorizationCompile-time Validation
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.