How to Implement Custom Authentication Providers Using TokenDao and GatewayAuthorizationFilter in Apache Linkis
To implement custom authentication providers in Linkis, create a custom TokenDao implementation to fetch tokens from your data source, implement a TokenService (or extend CachedTokenService) to handle validation logic, and expose it as a Spring @Primary bean so GatewaySpringConfiguration injects it into the TokenAuthentication static entry point.
Apache Linkis provides a modular token-based authentication flow through its Spring Cloud Gateway. By leveraging the TokenDao interface and GatewayAuthorizationFilter, you can plug in custom authentication providers without modifying core gateway code. This guide walks through implementing custom TokenDao and TokenService implementations to integrate external token stores.
Understanding the Linkis Token Authentication Architecture
Linkis authenticates requests through a token-based flow wired into the Spring Cloud Gateway. The core components include:
How the Default Authentication Flow Works
- Incoming request reaches
GatewayAuthorizationFilter. - The filter asks
SecurityFilterwhether the request carries a token viaTokenAuthentication.isTokenRequest. - If a token is present,
TokenAuthentication.tokenAuthis invoked. TokenAuthenticationdelegates to the singletonTokenService(CachedTokenServiceby default) which:- Looks up the token record via
TokenDao. - Performs validation (expiry, allowed users, allowed hosts).
- Throws
TokenAuthExceptionif any check fails.
- Looks up the token record via
- On success, the token's owner is written into the
GatewayContextviaGatewaySSOUtils.setLoginUser.
Because the DAO and service are Spring beans, you can substitute them with your own implementation, creating a custom authentication provider without touching core gateway code.
Creating a Custom TokenDao Implementation
The TokenDao interface defines the contract for loading token data. To implement a custom provider, create a new DAO that reads tokens from your chosen source (e.g., external REST API, LDAP, or a different database).
// src/main/scala/com/yourcorp/linkis/auth/dao/CustomTokenDao.scala
package com.yourcorp.linkis.auth.dao
import org.apache.linkis.gateway.authentication.entity.TokenEntity
import org.apache.linkis.gateway.authentication.dao.TokenDao
import org.apache.ibatis.annotations.Param
import org.springframework.stereotype.Repository
import org.apache.http.impl.client.HttpClients
import org.apache.http.client.methods.HttpGet
@Repository
class CustomTokenDao extends TokenDao {
private val httpClient = HttpClients.createDefault()
private val baseUrl = "https://auth.mycorp.com/api/tokens"
override def selectTokenByName(@Param("tokenName") tokenName: String): TokenEntity = {
val request = new HttpGet(s"$baseUrl/name/$tokenName")
val response = httpClient.execute(request)
try {
val json = scala.io.Source.fromInputStream(response.getEntity.getContent).mkString
parseJsonToEntity(json)
} finally {
response.close()
}
}
override def selectTokenBySign(@Param("tokenSign") tokenSign: String): TokenEntity = {
// Implementation for sign-based lookup
val request = new HttpGet(s"$baseUrl/sign/$tokenSign")
// ... similar implementation
null
}
private def parseJsonToEntity(json: String): TokenEntity = {
// Parse JSON and map to TokenEntity fields
val entity = new TokenEntity()
// ... mapping logic
entity
}
}
Key implementation points:
- Annotate the class with
@Repositoryso Spring can inject it. - Implement all methods declared in
TokenDao(typicallyselectTokenByNameandselectTokenBySign). - Return
TokenEntityobjects populated with token metadata (name, signature, allowed users, allowed hosts, expiry).
Implementing a Custom TokenService
The TokenService interface defines high-level authentication operations. You can either implement the interface directly or extend CachedTokenService if you only need to modify specific behaviors (e.g., removing caching or adding custom validation rules).
Extending CachedTokenService for Custom Logic
If you want to bypass the Guava cache and always query your custom DAO, or add additional validation steps:
// src/main/scala/com/yourcorp/linkis/auth/service/CustomTokenService.scala
package com.yourcorp.linkis.auth.service
import org.apache.linkis.gateway.authentication.service.TokenService
import org.apache.linkis.gateway.authentication.bo.{Token, User}
import org.apache.linkis.gateway.authentication.entity.TokenEntity
import org.apache.linkis.gateway.authentication.exception.TokenAuthException
import org.apache.linkis.gateway.authentication.errorcode.LinkisGwAuthenticationErrorCodeSummary._
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import org.springframework.context.annotation.Primary
@Service
@Primary
class CustomTokenService extends TokenService {
@Autowired
private var tokenDao: com.yourcorp.linkis.auth.dao.CustomTokenDao = _
private def loadToken(tokenName: String): Token = {
val entity = tokenDao.selectTokenByName(tokenName)
if (entity == null) {
throw new TokenAuthException(INVALID_TOKEN.getErrorCode, INVALID_TOKEN.getErrorDesc)
}
convertToToken(entity)
}
private def convertToEntity(token: Token): TokenEntity = {
// Conversion logic from Token BO to TokenEntity
null
}
private def convertToToken(entity: TokenEntity): Token = {
// Conversion logic from TokenEntity to Token BO
new Token() {
override def getTokenName: String = entity.getTokenName
override def getBusinessOwner: String = entity.getBusinessOwner
override def getCreateTime: java.util.Date = entity.getCreateTime
override def getUpdateTime: java.util.Date = entity.getUpdateTime
override def getExpireTime: java.util.Date = entity.getExpireTime
override def getAllowedUsers: String = entity.getAllowedUsers
override def getAllowedHosts: String = entity.getAllowedHosts
override def isStale: Boolean = {
if (getExpireTime == null) false
else getExpireTime.before(new java.util.Date())
}
override def isUserLegal(user: String): Boolean = {
if (getAllowedUsers == null || getAllowedUsers.isEmpty) true
else getAllowedUsers.split(",").contains(user)
}
override def isHostLegal(host: String): Boolean = {
if (getAllowedHosts == null || getAllowedHosts.isEmpty) true
else getAllowedHosts.split(",").contains(host)
}
}
}
override def doAuth(tokenName: String, userName: String, host: String): Boolean = {
val token = loadToken(tokenName)
if (token.isStale) {
throw new TokenAuthException(TOKEN_IS_STALE.getErrorCode, TOKEN_IS_STALE.getErrorDesc)
}
if (!token.isUserLegal(userName)) {
throw new TokenAuthException(ILLEGAL_TOKENUSER.getErrorCode, ILLEGAL_TOKENUSER.getErrorDesc)
}
if (!token.isHostLegal(host)) {
throw new TokenAuthException(ILLEGAL_TOKENHOST.getErrorCode, ILLEGAL_TOKENHOST.getErrorDesc)
}
true
}
override def isTokenValid(tokenName: String): Boolean = {
try {
val token = loadToken(tokenName)
!token.isStale
} catch {
case _: Exception => false
}
}
override def isTokenAcceptableWithUser(tokenName: String, userName: String): Boolean = {
try {
val token = loadToken(tokenName)
!token.isStale && token.isUserLegal(userName)
} catch {
case _: Exception => false
}
}
override def getTokenByName(tokenName: String): Token = loadToken(tokenName)
override def getTokenByUserToken(tokenName: String, tokenSign: String): Token = {
// Implementation for sign-based lookup if needed
loadToken(tokenName)
}
}
Wiring the Custom Service with Spring
GatewaySpringConfiguration obtains the TokenService bean via field injection and registers it with the static TokenAuthentication object. To ensure your implementation is used, mark it with @Primary:
@Service
@Primary
class CustomTokenService extends TokenService {
// ... implementation
}
Alternatively, exclude the default CachedTokenService from component scanning or override it explicitly in a @Configuration class:
@Configuration
class AuthConfiguration {
@Bean
@Primary
def tokenService(customDao: CustomTokenDao): TokenService = {
new CustomTokenService(customDao)
}
}
The @PostConstruct method in GatewaySpringConfiguration automatically calls TokenAuthentication.setTokenService(tokenService), ensuring your custom logic handles all token authentication requests.
Extending Validation with LinkisPreFilter
For additional request-level validation that runs before token authentication—such as IP allowlisting or rate limiting—implement the LinkisPreFilter interface. GatewayAuthorizationFilter automatically iterates over all registered pre-filters (around line 159 in the source), aborting the request if any filter returns false.
// src/main/scala/com/yourcorp/linkis/filter/IpAllowListFilter.scala
package com.yourcorp.linkis.filter
import org.apache.linkis.gateway.http.GatewayContext
import org.apache.linkis.gateway.security.LinkisPreFilter
import org.springframework.stereotype.Component
@Component
class IpAllowListFilter extends LinkisPreFilter {
private val allowedIps = Set("10.0.0.1", "10.0.0.2", "192.168.1.100")
override def name: String = "IpAllowListFilter"
override def doFilter(gatewayContext: GatewayContext): Boolean = {
val clientIp = gatewayContext.getRequest.getRequestRealIpAddr()
if (allowedIps.contains(clientIp)) {
true
} else {
gatewayContext.getResponse.setStatusCode(403)
gatewayContext.getResponse.write("Access denied: IP not in allowlist")
false
}
}
}
Register the filter as a Spring @Component or manually add it via LinkisPreFilter.addFilter() in a configuration class. The filter executes before token validation, allowing you to reject requests based on custom business rules.
Testing Your Custom Authentication Provider
Once deployed, test the custom provider by sending requests with the Token-Key and Token-User headers:
curl -H "Token-Key: abcdef123456" \
-H "Token-User: alice" \
http://gateway.example.com/api/v1/engineconn/submit
The GatewayAuthorizationFilter extracts these headers and delegates to TokenAuthentication.tokenAuth, which invokes your CustomTokenService.doAuth method. If your DAO returns a valid TokenEntity for abcdef123456, and the token permits user alice from the requesting host, the request proceeds to the target service. Otherwise, the gateway returns a 401 or 403 error based on the specific validation failure.
Summary
- TokenDao provides the data access contract for token storage; implement this interface to read from external systems like REST APIs or LDAP.
- TokenService defines the authentication contract; extend
CachedTokenServiceor implement the interface directly to customize validation logic, caching behavior, or error handling. - Spring Integration requires marking your
TokenServicewith@Primary(or excluding the default bean) soGatewaySpringConfigurationinjects it into the staticTokenAuthenticationentry point. - LinkisPreFilter offers an extension point for pre-authentication checks such as IP allowlisting or rate limiting, executed by
GatewayAuthorizationFilterbefore token validation. - All customization happens without modifying
GatewayAuthorizationFilteror core gateway classes, preserving Linkis's modular architecture while supporting enterprise authentication requirements.
Frequently Asked Questions
How does GatewayAuthorizationFilter interact with custom TokenService implementations?
GatewayAuthorizationFilter acts as the global entry point for all gateway requests. When it detects a token-based request (via TokenAuthentication.isTokenRequest), it delegates to TokenAuthentication.tokenAuth, which internally calls the singleton TokenService bean. Because GatewaySpringConfiguration sets this bean via TokenAuthentication.setTokenService(tokenService) during application startup, any custom implementation you provide (marked with @Primary) automatically handles all token validation requests without requiring changes to the filter itself.
Can I use multiple authentication sources simultaneously with TokenDao?
The TokenDao interface is designed as a single contract per TokenService instance. However, you can implement a composite DAO that aggregates multiple sources internally. For example, your selectTokenByName method could first query a local cache, then fall back to a REST API, and finally check an LDAP directory. Since CachedTokenService (or your custom TokenService) calls the DAO methods, the service remains unaware of the underlying data federation, allowing you to support multiple authentication sources transparently.
What is the difference between implementing TokenService directly versus extending CachedTokenService?
Extending CachedTokenService is appropriate when you want to retain the default Guava-based caching behavior but modify specific aspects like validation rules or error handling. CachedTokenService already implements the full TokenService contract, including cache management and DAO delegation. Implementing TokenService directly gives you complete control over the authentication lifecycle—including whether to use caching at all, how to handle concurrent requests, and custom exception mapping. Use direct implementation when the default caching strategy conflicts with your requirements (such as needing real-time token revocation).
How do I troubleshoot when my custom authentication provider is not being invoked?
First, verify that your TokenService bean is annotated with @Primary to ensure it overrides the default CachedTokenService in the Spring context. Check the application logs during startup for the GatewaySpringConfiguration initialization; you should see the TokenAuthentication.setTokenService call completing without errors. If the gateway still uses default authentication, confirm that your custom DAO is correctly returning TokenEntity objects and not throwing exceptions that trigger fallback behavior. Finally, enable debug logging for org.apache.linkis.gateway to trace the request flow through GatewayAuthorizationFilter and verify that TokenAuthentication.isTokenRequest is detecting your token headers correctly.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →