Week 3 | Lesson 12

Security & Authentication

Authentication, Authorization, JWT, Spring Security Configuration & Implementation



© 2026 by Monika Protivová

Authentication

Authentication

Authentication is the process of verifying the identity of a user, device, or system.
It answers the question: "Who are you?"

Authentication is a fundamental security concept that ensures only legitimate users can access your system.

Key aspects of authentication:

  • Identity Verification - Confirming that users are who they claim to be
  • Credential Validation - Checking provided credentials against stored data
  • Session Management - Maintaining authenticated state across requests
  • Security Measures - Protecting against common attack vectors

Authentication is different from Authorization - Authentication verifies identity, while Authorization determines what an authenticated user can do.

Authentication Methods

There are several common methods for implementing authentication, each with its own benefits and use cases.

Common authentication methods include:

  • Username/Password - Traditional credential-based authentication
    • Simple to implement and understand
    • Requires secure password storage (hashing)
    • Vulnerable to password-based attacks
  • Token-based Authentication - Using tokens to represent authenticated sessions
    • More secure than session-based auth
    • Stateless and scalable
    • Common implementations: JWT, API keys
  • OAuth/OAuth2 - Delegated authorization framework
    • Allows third-party authentication
    • Used by social login providers
    • Complex but powerful

Username/Password Authentication

The most common form of authentication where users provide a username and password combination.

Implementation considerations:

  • Password Hashing - Never store passwords in plain text
    • Use strong hashing algorithms (bcrypt, Argon2)
    • Include salt to prevent rainbow table attacks
    • Use appropriate work factors for computational cost

  • Password Policies - Enforce strong password requirements
    • Minimum length and complexity
    • Regular password rotation
    • Prevent password reuse

  • Security Measures
    • Rate limiting to prevent brute force attacks
    • Account lockout after failed attempts
    • Secure transmission (HTTPS)
    • Multi-factor authentication (MFA)

Token-based Authentication

Token-based authentication uses tokens to represent authenticated sessions, providing a stateless and scalable approach.

How token-based authentication works

  1. User provides credentials (username/password)
  2. Server validates credentials
  3. Server generates a token and returns it to the client
  4. Client includes the token in subsequent requests
  5. Server validates the token for each request

Common token formats

  • JWT (JSON Web Tokens)
  • Opaque tokens with server-side lookup
  • API keys for service-to-service communication

Advantages

  • Stateless - Server doesn't need to store session information
  • Scalable - Easy to distribute across multiple servers
  • Cross-domain - Works across different domains and services
  • Mobile-friendly - Ideal for mobile applications and APIs

OAuth 2.0

OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on third-party services.

OAuth 2.0 roles

  • Resource Owner - The user who authorizes access to their data
  • Client - The application requesting access to user data
  • Authorization Server - Server that authenticates the user and issues access tokens
  • Resource Server - Server hosting the protected user data

Common OAuth 2.0 grant types

  • Authorization Code - Most secure, suitable for web applications
  • Implicit - For client-side applications (less secure)
  • Client Credentials - For service-to-service communication
  • Resource Owner Password Credentials - Direct username/password (discouraged)

Benefits

Single sign-on (SSO), reduced password fatigue, leverages established identity providers.

Authorization

Authorization

Authorization is the process of determining what actions an authenticated user is allowed to perform. It answers the question: "What can you do?"

Authorization occurs after authentication and determines the permissions and access rights of an authenticated user.

Key concepts in authorization:

  • Permissions - Specific actions a user can perform (read, write, delete)
  • Roles - Groups of permissions assigned to users (admin, user, guest)
  • Resources - Objects or services being protected (files, APIs, data)
  • Policies - Rules that determine access based on various conditions

Authorization ensures that users can only access resources and perform actions that they are explicitly allowed to.

Authorization vs Authentication

Understanding the difference between authentication and authorization is crucial for implementing proper security.

Authentication

  • "Who are you?"
  • Verifies user identity
  • Validates credentials
  • Happens first
  • Examples:
    • Username/password
    • Biometric verification
    • Two-factor authentication

Authorization

  • "What can you do?"
  • Determines user permissions
  • Controls access to resources
  • Happens after authentication
  • Examples:
    • Role-based access control
    • Permission checking
    • Resource-level security

Analogy

Think of a hotel - authentication is showing your ID at check-in (proving who you are), while authorization is your room key working only for your specific room (what you can access).

Authorization Mechanisms

Different approaches to implementing authorization, each suitable for different scenarios and requirements.

Common authorization mechanisms

  • Role-Based Access Control (RBAC)
    • Users are assigned roles
    • Roles have specific permissions
    • Simple and widely used
    • Example: admin, editor, viewer
  • Attribute-Based Access Control (ABAC)
    • Permissions based on attributes
    • More flexible and granular
    • Considers user, resource, and environment attributes
    • Example: time of day, location, department
  • Access Control Lists (ACL)
    • Permissions attached to specific resources
    • Fine-grained control
    • Can become complex to manage
    • Example: file system permissions

Implementation

Authorization logic is typically implemented in the service layer. When using JWT for authentication, authorization can be done by checking claims in the token. See the JWT topic for more details on JWT-based authorization.

Basic Authentication

Basic Authentication

Basic Authentication is a simple authentication scheme built into the HTTP protocol. It uses base64-encoded username and password credentials.

Basic Authentication is defined in RFC 7617 and is one of the simplest authentication mechanisms available.

Key characteristics

  • Built into HTTP protocol
  • Simple to implement and understand
  • Widely supported by browsers and HTTP clients
  • Credentials sent with every request
  • No additional handshake or setup required

The credentials are encoded using Base64, which provides encoding but NOT encryption - the credentials can be easily decoded.

How Basic Auth Works

Basic Authentication follows a simple challenge-response mechanism between client and server.

Authentication flow

  1. Client makes a request to a protected resource
  2. Server responds with 401 Unauthorized and includes WWW-Authenticate: Basic realm="Protected Area" header
  3. Client prompts user for credentials (or uses stored ones)
  4. Client sends request with Authorization: Basic <encoded-credentials> header
  5. Server decodes and validates credentials
  6. Server responds with requested resource or 403 Forbidden

The encoded credentials format is: base64(username:password)

Basic Auth Implementation

Creating the Authorization header

  1. Combine username and password with a colon:
    val credentials = "$username:$password"
  2. Encode the combined string using Base64:
    val encodedCredentials = Base64.getEncoder().encodeToString(credentials.toByteArray())
  3. Create the Authorization header value:
    val authHeader = "Basic $encodedCredentials"
  4. The resulting Authorization header will look like this:
    Basic am9obi5kb2U6c2VjcmV0cGFzc3dvcmQ=

Processing the Authorization header

fun extractCredentials(authHeader: String): Pair<String, String>? { if (!authHeader.startsWith("Basic ")) return null val encoded = authHeader.removePrefix("Basic ") val decoded = String(Base64.getDecoder().decode(encoded)) val parts = decoded.split(":", limit = 2) return if (parts.size == 2) { Pair(parts[0], parts[1]) } else null }

Basic Auth Security Considerations

While simple to implement, Basic Authentication has several security limitations that must be addressed.

Security vulnerabilities

  • No Encryption - Base64 encoding is easily reversible
  • Credentials in Every Request - Increases exposure window
  • No Token Expiration - Credentials remain valid until changed
  • Browser Storage - Browsers may cache credentials
  • Logout Limitations - Difficult to implement proper logout

Security best practices

  • HTTPS Only - Always use HTTPS to encrypt transmission
  • Strong Passwords - Enforce strong password policies
  • Rate Limiting - Implement request rate limiting
  • Account Lockout - Lock accounts after failed attempts
  • Monitor and Log - Track authentication attempts

Basic Auth is suitable for simple use cases but consider more secure alternatives for production applications.

JWT (JSON Web Tokens)

JWT (JSON Web Tokens)

JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims to be transferred between two parties.

JWT is defined in RFC 7519 and is widely used for authentication and information exchange in modern web applications.

Key characteristics of JWT

  • Self-contained - Contains all necessary information within the token
  • Stateless - Server doesn't need to store session information
  • Compact - Small size suitable for HTTP headers and URLs
  • Secure - Can be signed and optionally encrypted
  • Cross-platform - Language and platform independent

Common use cases

  • Authentication and authorization
  • Information exchange between services
  • Single Sign-On (SSO) implementations
  • API access control

JWT Structure

A JWT consists of three parts separated by dots: Header.Payload.Signature

JWT structure: xxxxx.yyyyy.zzzzz

Header

  • Contains metadata about the token
  • Specifies the signing algorithm
  • Encoded in Base64URL
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

  • Contains the claims (user data)
  • Standard and custom claims
  • Encoded in Base64URL
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Signature

  • Verifies the token hasn't been tampered with
  • Created using header + payload + secret
  • Algorithm specified in header
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

JWT Claims

Claims are statements about an entity (typically the user) and additional data. There are three types of claims: registered, public, and private.

Registered Claims - Standard claims with predefined meanings:

  • iss (Issuer) - Who issued the token
  • sub (Subject) - Who the token is about
  • aud (Audience) - Who the token is intended for
  • exp (Expiration Time) - When the token expires
  • nbf (Not Before) - When the token becomes valid
  • iat (Issued At) - When the token was issued
  • jti (JWT ID) - Unique identifier for the token

Private Claims - Custom claims for sharing information between parties, including authorization data:

{
  "iss": "https://auth.example.com",
  "sub": "user123",
  "aud": "https://api.example.com",
  "exp": 1516243022,
  "iat": 1516239422,
  "email": "user@example.com",
  "roles": ["admin", "user"],
  "permissions": ["read:users", "write:posts"],
  "department": "engineering",
  "organization": "acme-corp"
}

Common authorization claims: roles, permissions, department, organization, level

JWT Playground - Try It Yourself!

Use jwt.io to decode, verify, and generate JWTs interactively.

What is JWT.io?

An online tool to decode and verify JSON Web Tokens.

What you can do:

  • Decode existing JWTs to see header and payload
  • Verify JWT signatures with your secret
  • Generate new JWTs with custom claims
  • Test different signing algorithms
  • Learn JWT structure interactively

Try it now:

Visit https://jwt.io/ and paste the example JWT from the right to see it decoded!

Example JWT Token:

Copy this token and paste it into jwt.io to see the decoded header and payload:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsInJvbGVzIjpbInVzZXIiLCJhZG1pbiJdLCJpYXQiOjE3MzY1NTk2MDAsImV4cCI6MTczNjU2MzIwMH0.Kl_VdXsqGqBT8JIqvvKj5R8XFqPqHZqvBqzWx9YqNhQ

Decodes to:

// Header
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload
{
  "sub": "user123",
  "name": "John Doe",
  "email": "john.doe@example.com",
  "roles": ["user", "admin"],
  "iat": 1736559600,
  "exp": 1736563200
}

JWT Authentication Flow

Typical flow of JWT-based authentication in a web application.

JWT authentication process

  1. User Login - User submits credentials to authentication endpoint
  2. Credential Validation - Server validates username and password
  3. Token Generation - Server creates JWT with user information and claims
  4. Token Response - Server returns JWT to client (usually in response body)
  5. Token Storage - Client stores JWT (localStorage, sessionStorage, or httpOnly cookie)
  6. Authenticated Requests - Client includes JWT in Authorization header for subsequent requests
  7. Token Validation - Server validates JWT signature and claims for each request
  8. Token Expiration - Token expires and user needs to re-authenticate or refresh token

Authorization with JWT

JWT tokens can carry authorization information, making them ideal for stateless authorization decisions.

JWT-based authorization process

  1. User authenticates and receives JWT with authorization claims (roles, permissions)
  2. For each request, server extracts and validates JWT
  3. Server checks claims against required permissions for the requested action
  4. Access granted or denied based on authorization rules
  5. Authorization logic typically implemented in the service layer

Benefits of JWT authorization:

  • Stateless - No server-side session storage needed for permissions
  • Self-contained - All authorization information embedded in token
  • Scalable - Works across distributed systems without shared state
  • Fast - No database lookups needed for basic authorization checks

For highly dynamic permissions, consider hybrid approaches where JWT contains basic roles while detailed permissions are fetched as needed.

JWT Security Considerations

While JWTs provide many benefits, there are important security considerations to keep in mind.

Security Best Practices

  • Use Strong Secrets - Use cryptographically strong signing keys
  • Short Expiration Times - Keep token lifetimes short (15-30 minutes)
  • HTTPS Only - Always transmit tokens over secure connections
  • Validate All Claims - Always validate issuer, audience, and expiration
  • Implement Token Refresh - Use refresh tokens for longer sessions
  • Secure Storage - Store tokens securely on the client side

Common Vulnerabilities:

  • None Algorithm Attack - Always verify the algorithm
  • Weak Signing Keys - Use sufficiently long random keys
  • Token Leakage - Protect against XSS and injection attacks
  • Replay Attacks - Implement proper expiration and nonce handling

Remeber!

JWTs are signed, not encrypted by default. Sensitive data should be encrypted separately.

Spring Security Introduction

Spring Security

Spring Security is a powerful and highly customizable authentication and access control framework for Spring applications

Spring Security provides comprehensive security services for Java EE-based enterprise software applications. It is the de facto standard for securing Spring-based applications.

Spring Security Provides

  • Authentication - Verifying the identity of users
  • Authorization - Controlling access to resources
  • Protection Against Exploits - CSRF, session fixation, clickjacking, etc.
  • Integration - LDAP, OAuth 2.0, SAML, JWT, and more

Spring Security integrates seamlessly with Spring Boot, providing auto-configuration and sensible defaults that can be customized as needed.

Spring Security Architecture

Understanding the core components of Spring Security's authentication and authorization architecture

Key Components

  • SecurityContext - Holds security information about the current thread
  • Authentication - Represents the token for an authentication request or authenticated principal
  • AuthenticationManager - Processes authentication requests
  • UserDetailsService - Core interface for loading user-specific data
  • SecurityFilterChain - Defines security filter configurations for different URL patterns

How It Works

  1. Request enters the application
  2. Security filters intercept the request
  3. Authentication filters extract credentials
  4. AuthenticationManager validates credentials
  5. SecurityContext is populated with Authentication
  6. Authorization checks are performed
  7. Request proceeds to controller or is rejected

Spring Security Features

Authentication Methods

  • Form-based login
  • HTTP Basic authentication
  • OAuth 2.0 / OpenID Connect
  • LDAP authentication
  • JWT (JSON Web Tokens)
  • Remember-me authentication

Authorization Strategies

  • URL-based security (requestMatchers)
  • Method-level security (@PreAuthorize, @Secured)
  • Role-based access control (RBAC)
  • Expression-based access control

Protection Features

  • CSRF (Cross-Site Request Forgery) protection
  • Session management
  • Password encoding
  • Security headers (X-Frame-Options, CSP, etc.)
  • CORS configuration

Spring Security Configuration

Configuring Spring Security

Spring Security configuration defines how your application handles authentication and authorization

In Spring Boot 3.x, security configuration is done through @Configuration classes that define SecurityFilterChain beans.

@Configuration @EnableWebSecurity class SecurityConfiguration { @Bean fun securityFilterChain( http: HttpSecurity ): SecurityFilterChain { http { authorizeHttpRequests { authorize("/public/**", permitAll) authorize("/admin/**", hasRole("ADMIN")) authorize(anyRequest, authenticated) } formLogin { } httpBasic { } csrf { disable() } } return http.build() } }

  • @EnableWebSecurity - Enables Spring Security's web security support
  • @Configuration - Indicates this class provides Spring beans

SecurityFilterChain Configuration

@Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http { // URL-based authorization authorizeHttpRequests { authorize("/api/public/**", permitAll) authorize("/api/user/**", hasRole("USER")) authorize("/api/admin/**", hasRole("ADMIN")) authorize(anyRequest, authenticated) } // Authentication methods httpBasic { } // Enable HTTP Basic formLogin { // Enable form-based login loginPage = "/login" defaultSuccessUrl = "/dashboard" } // Logout configuration logout { logoutUrl = "/logout" logoutSuccessUrl = "/login" } // CSRF protection (disable for stateless APIs) csrf { disable() } // Session management sessionManagement { sessionCreationPolicy = SessionCreationPolicy.STATELESS } } return http.build() }

Password Encoding

Never store passwords in plain text - always use a secure password encoder

Spring Security provides several password encoders. BCryptPasswordEncoder is the recommended choice for most applications.

@Configuration class SecurityConfiguration { @Bean fun passwordEncoder(): PasswordEncoder { return BCryptPasswordEncoder() } @Bean fun userDetailsService(passwordEncoder: PasswordEncoder): UserDetailsService { val user = User.builder() .username("user") .password(passwordEncoder.encode("password")) .roles("USER") .build() val admin = User.builder() .username("admin") .password(passwordEncoder.encode("admin123")) .roles("USER", "ADMIN") .build() return InMemoryUserDetailsManager(user, admin) } }

Available Password Encoders:

  • BCryptPasswordEncoder - Recommended (uses bcrypt hashing)
  • Pbkdf2PasswordEncoder - Uses PBKDF2
  • SCryptPasswordEncoder - Uses scrypt
  • Argon2PasswordEncoder - Uses Argon2
⚠️ Never use NoOpPasswordEncoder in production!

Spring Security Authentication

UserDetailsService

UserDetailsService is the core interface for loading user-specific data during authentication

The UserDetailsService interface has a single method that loads a user by username. Spring Security uses this to retrieve user information during authentication.

@Service class CustomUserDetailsService( private val userRepository: UserRepository ) : UserDetailsService { override fun loadUserByUsername(username: String): UserDetails { val user = userRepository.findByUsername(username) ?: throw UsernameNotFoundException("User not found: $username") return User.builder() .username(user.username) .password(user.password) .roles(*user.roles.toTypedArray()) .accountExpired(!user.isActive) .accountLocked(user.isLocked) .credentialsExpired(user.isPasswordExpired) .disabled(!user.isEnabled) .build() } }

Key Points:

  • Load user from your database or data source
  • Return a UserDetails object
  • Throw UsernameNotFoundException if user not found
  • Include roles and account status information

Custom Authentication Provider

For complex authentication logic, implement a custom AuthenticationProvider

While UserDetailsService is sufficient for most cases, you can implement AuthenticationProvider for complete control over the authentication process.

@Component class CustomAuthenticationProvider( private val userDetailsService: UserDetailsService, private val passwordEncoder: PasswordEncoder ) : AuthenticationProvider { override fun authenticate(authentication: Authentication): Authentication { val password = authentication.credentials.toString() val userDetails = userDetailsService.loadUserByUsername(authentication.name) // Verify password if (!passwordEncoder.matches(password, userDetails.password)) { throw BadCredentialsException("Invalid credentials") } // Additional custom checks if (!isAllowedToLogin(userDetails)) { throw LockedException("User account is locked") } return UsernamePasswordAuthenticationToken(userDetails, password, userDetails.authorities) } override fun supports(authentication: Class<*>): Boolean { return authentication == UsernamePasswordAuthenticationToken::class.java } private fun isAllowedToLogin(userDetails: UserDetails): Boolean = true }

Accessing Authentication Information

Retrieve the currently authenticated user in your controllers and services
@RestController @RequestMapping("/api") class UserController { // 1. Using SecurityContextHolder (works anywhere) @GetMapping("/me/v1") fun getCurrentUserV1(): String { val authentication = SecurityContextHolder.getContext().authentication return authentication.name } // 2. Using @AuthenticationPrincipal annotation (recommended) @GetMapping("/me/v2") fun getCurrentUserV2( @AuthenticationPrincipal userDetails: UserDetails ): String { return userDetails.username } // 3. Using Authentication parameter @GetMapping("/me/v3") fun getCurrentUserV3(authentication: Authentication): String { return authentication.name } // 4. Using Principal parameter @GetMapping("/me/v4") fun getCurrentUserV4(principal: Principal): String { return principal.name } }
💡 Best Practice:
Use @AuthenticationPrincipal in controllers for clean, type-safe access to user details.

Spring Security Authorization

Authorization in Spring Security

Spring Security provides multiple ways to configure authorization rules, from URL-based patterns to fine-grained method-level security.

URL-Based Authorization (in SecurityFilterChain):

@Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeHttpRequests { // Public endpoints authorize("/api/public/**", permitAll) authorize("/api/health", permitAll) // Role-based authorization authorize("/api/user/**", hasRole("USER")) authorize("/api/admin/**", hasRole("ADMIN")) authorize("/api/moderator/**", hasAnyRole("ADMIN", "MODERATOR")) // Authority-based authorization authorize("/api/reports/**", hasAuthority("REPORT_READ")) // All other requests require authentication authorize(anyRequest, authenticated) } } return http.build() }

Authorization Matchers:

  • permitAll - Allow everyone (no authentication needed)
  • authenticated - Require any authentication
  • hasRole("ROLE") - Require specific role
  • hasAnyRole("ROLE1", "ROLE2") - Require any of the roles
  • hasAuthority("AUTH") - Require specific authority

Controller-Level Authorization

Apply authorization rules at the controller class level using @PreAuthorize to secure all endpoints in that controller.

Place @PreAuthorize on the controller class itself to apply the same authorization rule to all methods. This is useful when all endpoints in a controller require the same level of access.

Example 1: Admin-Only Controller

// All methods require ADMIN role @RestController @RequestMapping("/api/admin/users") @PreAuthorize("hasRole('ADMIN')") class AdminUserController( private val userService: UserService ) { @GetMapping fun getAllUsers(): List<User> = userService.getAll() @DeleteMapping("/{id}") fun deleteUser(@PathVariable id: Long) = userService.delete(id) @PutMapping("/{id}/role") fun updateUserRole(@PathVariable id: Long, @RequestBody role: String) { userService.updateRole(id, role) } }

Example 2: Authenticated Users Only

// All methods require authentication @RestController @RequestMapping("/api/profile") @PreAuthorize("isAuthenticated()") class ProfileController( private val profileService: ProfileService ) { @GetMapping fun getProfile(): Profile = profileService.getCurrentUserProfile() @PutMapping fun updateProfile(@RequestBody profile: ProfileRequest) { profileService.update(profile) } }
Use class-level authorization when: All endpoints in a controller require the same level of access. This keeps your code DRY and makes security requirements explicit.

Method-Level Security

Enable method-level security with @EnableMethodSecurity and use annotations to protect specific methods.
@Configuration @EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) class SecurityConfiguration @Service class UserService { @PreAuthorize("hasRole('USER')") fun getUserProfile(userId: Long): UserProfile { // ... } @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')") fun updateUser(userId: Long, data: UserUpdateRequest) { // ... } // Multiple conditions @PreAuthorize("hasRole('ADMIN') and hasAuthority('DELETE_USER')") fun deleteUser(userId: Long) { // ... } // Simple role check @Secured("ROLE_ADMIN") fun adminOnlyOperation() { // ... } // Check return value @PostAuthorize("returnObject.ownerId == authentication.principal.id") fun getDocument(docId: Long): Document { // ... } }

Security Annotations:

  • @PreAuthorize - Check before method execution (most common)
  • @PostAuthorize - Check after method execution (can access return value)
  • @Secured - Simpler role-based checks
  • @RolesAllowed - JSR-250 standard annotation

Combining Class and Method Level Authorization

Use class-level @PreAuthorize as a default, then override specific methods with different authorization rules when needed.

When most endpoints in a controller need the same authorization but a few require different access levels, you can combine class-level and method-level annotations. Method-level annotations override class-level ones.

@RestController @RequestMapping("/api/reports") @PreAuthorize("hasRole('MANAGER')") // Default for all methods class ReportController( private val reportService: ReportService ) { // Inherits MANAGER requirement from class @GetMapping fun getReports(): List<Report> { return reportService.getAll() } // Inherits MANAGER requirement from class @GetMapping("/{id}") fun getReport(@PathVariable id: Long): Report { return reportService.getById(id) } // Override: Only ADMIN can delete reports @DeleteMapping("/{id}") @PreAuthorize("hasRole('ADMIN')") fun deleteReport(@PathVariable id: Long) { reportService.delete(id) } // Override: Public endpoint, no authentication required @GetMapping("/summary") @PreAuthorize("permitAll()") fun getPublicSummary(): ReportSummary { return reportService.getSummary() } // Override: Multiple roles allowed @PostMapping @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER', 'SUPERVISOR')") fun createReport(@RequestBody request: ReportRequest): Report { return reportService.create(request) } }

Authorization Hierarchy

  1. Method-level annotations take precedence (highest priority)
  2. Class-level annotations apply to methods without their own annotations
  3. SecurityFilterChain URL patterns apply as a baseline (lowest priority)

Custom Authorization Logic

Create custom permission evaluators for complex authorization scenarios.
@Component("customSecurity") class CustomSecurityService { fun canAccessResource( authentication: Authentication, resourceId: Long ): Boolean { val userId = (authentication.principal as UserDetails).username // Custom business logic return checkOwnership(userId, resourceId) || hasAdminRole(authentication) } fun hasPermission( authentication: Authentication, permission: String ): Boolean { // Check permissions from database or external service return true } private fun checkOwnership( userId: String, resourceId: Long ): Boolean { // Check if user owns the resource return true } private fun hasAdminRole(authentication: Authentication): Boolean { return authentication.authorities.any { it.authority == "ROLE_ADMIN" } } }
@Service class DocumentService { @PreAuthorize("@customSecurity.canAccessResource(authentication, #documentId)") fun getDocument(documentId: Long): Document { // ... } @PreAuthorize("@customSecurity.hasPermission(authentication, 'DOCUMENT_DELETE')") fun deleteDocument(documentId: Long) { // ... } }
💡 Tip: Use custom security beans for complex authorization logic that involves database queries or external service calls.

Practice