Sa-Token's Same-Token Locks Down Internal Services from Direct Calls
1. Requirement Scenario
In microservice projects, our sub-services generally cannot be directly accessed from the external network; requests must be forwarded through the gateway to be considered legitimate. This isolation between sub-services and the external network is generally divided into two types:
- Physical isolation: Sub-services are deployed in a designated internal network environment, and only the gateway is open to the external network.
- Logical isolation: Sub-services and the gateway are both exposed to the external network, but the sub-services have a permission interception layer to ensure they only accept requests sent from the gateway. Direct access to sub-services bypassing the gateway will prompt: Invalid request.
This authentication requirement involves two links: Gateway Forwarding Authentication and Internal Service Call Authentication
Sa-Token provides two solutions:
- Use the OAuth2.0 client credentials mode, using the Client-Token as the identity credential for each service for permission verification.
- Use the identity verification capability provided by the Same-Token module to complete permission authentication between services.
This article mainly explains the integration steps for Solution 2, the Same-Token module. Its authentication process is similar to OAuth2.0, but the usage is more concise. The code in this article is based on Sa-Token v1.46.0.
Sa-Token is an open-source, free, one-stop Java permission authentication framework, mainly solving a series of permission-related issues such as login authentication, permission authentication, single sign-on, OAuth2, and microservice gateway authentication. Open Source Address: https://gitee.com/dromara/sa-token Online Documentation: https://sa-token.com
2. Gateway Forwarding Authentication
1. Introduce Dependencies
The dependencies introduced at the gateway are (using SpringCloud Gateway as an example):
<!-- Sa-Token Permission Authentication (Reactor Reactive Integration), Online Docs: https://sa-token.com -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.46.0</version>
</dependency>
<!-- Sa-Token integrates RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>1.46.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
Gradle method:
// Sa-Token Permission Authentication (Reactor Reactive Integration), Online Docs: https://sa-token.com
implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:1.46.0'
// Sa-Token integrates RedisTemplate
implementation 'cn.dev33:sa-token-redis-template:1.46.0'
implementation 'org.apache.commons:commons-pool2'
Note: For SpringBoot 3.x, please use sa-token-reactor-spring-boot3-starter instead; for SpringBoot 4.x, please use sa-token-reactor-spring-boot4-starter.
The dependencies introduced in the sub-service are:
<!-- Sa-Token Permission Authentication, Online Docs: https://sa-token.com -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.46.0</version>
</dependency>
<!-- Sa-Token integrates RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>1.46.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
Gradle method:
// Sa-Token Permission Authentication, Online Docs: https://sa-token.com
implementation 'cn.dev33:sa-token-spring-boot-starter:1.46.0'
// Sa-Token integrates RedisTemplate
implementation 'cn.dev33:sa-token-redis-template:1.46.0'
implementation 'org.apache.commons:commons-pool2'
Note: For SpringBoot 3.x, please use sa-token-spring-boot3-starter instead; for SpringBoot 4.x, please use sa-token-spring-boot4-starter.
The sa-token-dao-redis-jackson from older articles has been renamed to sa-token-redis-jackson. The current documentation recommends switching to sa-token-redis-template. Since v1.46.0, this plugin uses Redis 6.0+'s SET KEEPTTL. Redis versions below 6.0 will report an ERR syntax error. See Common Questions for handling.
2. Add Same-Token at the Gateway
Add a global filter for the gateway:
/**
* Global filter, adds Same-Token to requests
*/
@Component
public class ForwardAuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest newRequest = exchange
.getRequest()
.mutate()
// Append Same-Token parameter to the request
.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
.build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
}
}
This filter appends the Same-Token parameter to the Request header (the header name is SA-SAME-TOKEN), and this parameter will be forwarded to the sub-service.
3. Verify the Parameter in the Sub-Service
Add a filter in the sub-service to verify the parameter:
/**
* Sa-Token Permission Authentication Configuration Class
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// Register Sa-Token Global Filter
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/favicon.ico")
.setAuth(obj -> {
// Verify Same-Token identity credential — The following two lines can be simplified to: SaSameUtil.checkCurrentRequestToken();
String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN);
SaSameUtil.checkToken(token);
})
.setError(e -> {
return SaResult.error(e.getMessage());
})
;
}
}
Start the gateway and sub-service, and test the access:
If forwarded through the gateway, access is normal. Direct access to the sub-service will prompt:
Invalid Same-Token: xxx
3. RPC Call Authentication
Sometimes we need to call an interface of another service from within a service, which also requires adding Same-Token as an identity credential.
Adding Same-Token in a service is similar to the gateway process. Let's take the RPC framework Feign as an example:
1. First, add a FeignInterceptor on the caller side
/**
* Feign interceptor, performs some operations before the Feign request is sent
*/
@Component
public class FeignInterceptor implements RequestInterceptor {
// Add the Same-Token request header for Feign RPC calls
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
// If you want the called party to have session state, you also need to add satoken to the request header here
// requestTemplate.header(StpUtil.getTokenName(), StpUtil.getTokenValue());
}
}
2. Use this Interceptor in the calling interface
/**
* Service Call
*/
@FeignClient(
name = "sp-home", // Service name
configuration = FeignInterceptor.class, // Request interceptor (key code)
fallbackFactory = SpCfgInterfaceFallback.class // Service degradation handling
)
public interface SpCfgInterface {
// Get specific configuration info from the server side
@RequestMapping("/SpConfig/getConfig")
public String getConfig(@RequestParam("key")String key);
}
The code on the called side does not need to be changed (register the global filter according to the code in the gateway forwarding authentication section), just keep it started and test.
4. Same-Token Module Details
Same-Token — specifically solves identity authentication verification during mutual calls between homologous systems. Its role is not limited to microservice call scenarios.
The basic usage flow is: The service caller obtains a Token and submits it in the request. The called party retrieves the Token for verification: if the Token matches, verification passes; otherwise, the service is denied.
First, let's preview the relevant APIs of this module:
// Get the current Same-Token
SaSameUtil.getToken();
// Determine if a Same-Token is valid
SaSameUtil.isValid(token);
// Verify if a Same-Token is valid (throws an exception if invalid)
SaSameUtil.checkToken(token);
// Verify if the Same-Token provided by the current Request is valid (throws an exception if invalid)
SaSameUtil.checkCurrentRequestToken();
// Refresh the Same-Token once (Note: Do not call this repeatedly across multiple services in a cluster environment)
SaSameUtil.refreshToken();
// The recommended key to use when storing the Same-Token on the Request
SaSameUtil.SAME_TOKEN;
1. Question: Where is this Token stored? Is there a risk of leakage? Is the Token permanently valid or temporarily valid?
Same-Token is stored in Redis by default along with Sa-Token data. Theoretically, there is no risk of leakage. Each Token has a default validity period of only one day (configuration item sa-token.same-token-timeout, default 86400 seconds).
2. How to actively refresh the Same-Token, for example: every five minutes or two hours?
The shorter the Same-Token refresh interval, the higher its security. The default validity period for each Token is one day. After one day, fetching it again will automatically generate a new Token.
One thing to note: The default self-refresh mechanism of Same-Token cannot achieve high concurrency availability. Multiple services triggering Token refresh simultaneously may cause millisecond-level transient service failures. It is only suitable for project development stages or low-concurrency business scenarios.
Therefore, in a microservice architecture, we need a dedicated mechanism to actively refresh the Same-Token to ensure its high availability.
For example, we can specifically set up a service that uses a scheduled task to refresh the Same-Token:
/**
* Same-Token, scheduled refresh
*/
@Configuration
public class SaSameTokenRefreshTask {
// Starting from minute 0, execute Same-Token refresh every 5 minutes
@Scheduled(cron = "0 0/5 * * * ? ")
public void refreshToken(){
SaSameUtil.refreshToken();
}
}
The cron expression refresh interval above can be configured to five minutes, ten minutes, or two hours, as long as it is less than the validity period of the Same-Token (default one day).
3. What if the request forwarded by the gateway carrying the token arrives at the sub-service node exactly when the token is refreshed, causing authentication to fail?
Each time the Same-Token module refreshes the Token, the old Token is stored as a secondary Token. As long as the Token carried by the gateway matches either the new or old Token, authentication will pass, until the next refresh, when the new Token replaces this as the secondary Token.
5. Integrating Same-Token in Dubbo
Same-Token can not only provide call authentication for Feign, but can also be used for authentication during Dubbo RPC calls. The plugin providing this capability is sa-token-dubbo (formerly named sa-token-context-dubbo).
1. First, let's talk about the problem to be solved
In the entire Dubbo call chain, the code is divided into the Consumer side and the Provider side. For ease of understanding, we can call them [Caller] and [Callee].
RPC mode calls allow us to complete service communication as if calling local methods. However, this convenience hides two problems:
- Loss of context environment.
- Loss of context parameters.
When this problem acts on the Sa-Token framework, calling Sa-Token related APIs on the [Callee] side will throw an exception: Invalid Context.
So the purpose of this plugin is to solve the above two problems:
- Provide a Dubbo-based context environment on the [Callee] side.
- Pass the Token to the [Callee] side during RPC calls, and simultaneously pass the Token back to the [Caller] side when the call ends.
2. Introduce the Plugin
On the basis of the project already having Dubbo introduced, continue to add dependencies (both the Consumer side and Provider side need to introduce them):
<!-- Sa-Token integrates Dubbo -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dubbo</artifactId>
<version>1.46.0</version>
</dependency>
Gradle method:
// Sa-Token integrates Dubbo
implementation 'cn.dev33:sa-token-dubbo:1.46.0'
Note: If using Dubbo 3, change sa-token-dubbo to sa-token-dubbo3.
Then we can happily do the following things:
- Safely call Sa-Token related APIs on the [Callee] side.
- The login state of a session logged in on the [Caller] side can be automatically passed to the [Callee] side.
- The login state of a session logged in on the [Callee] side will also be automatically passed back to the [Caller] side.
But we still have the following limitations:
- The
SaStoragedata of the [Caller] and [Callee] sides cannot be interoperable. - Code executed on the [Callee] side like
SaResponse.setHeader(),setStatus()is invalid.
You should reasonably avoid using the above APIs.
3. Dubbo RPC Call Authentication
Below we demonstrate how to integrate the Same-Token module in Dubbo. The idea is actually consistent with the Feign mode: append the Same-Token parameter on the [Caller] side, and verify this Same-Token parameter on the [Callee] side:
- Verification passes: Call succeeds.
- Verification fails: Call fails, throwing an exception.
We have two ways to complete the integration.
Method 1: Use Configuration (Recommended)
Just configure directly in application.yml:
sa-token:
# Enable RPC call authentication
check-same-token: true
# Enable RPC call authentication
sa-token.check-same-token=true
The plugin's built-in filter will automatically append the Same-Token on the caller side and automatically verify it on the callee side. The same applies to gRPC: after introducing sa-token-grpc, configure check-same-token: true similarly.
Method 2: Self-built Dubbo Filter for Verification
This method is slightly more cumbersome, but the advantage is that besides the Same-Token, we can also add other custom parameters (attachment).
- On the [Caller] side, create an
org.apache.dubbo.rpc.Filterfile in the\resources\META-INF\dubbo\directory:
dubboConsumerFilter=com.pj.DubboConsumerFilter
Create the DubboConsumerFilter.java filter:
package com.pj;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import cn.dev33.satoken.same.SaSameUtil;
/**
* Sa-Token integrates Dubbo Consumer-side filter
*/
@Activate(group = {CommonConstants.CONSUMER}, order = -10000)
public class DubboConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// Append Same-Token parameter
RpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
// If there are other custom additional data, such as tenant
// RpcContext.getContext().setAttachment("tenantContext", tenantContext);
// Start the call
return invoker.invoke(invocation);
}
}
For Dubbo 3, please replace RpcContext.getContext() with RpcContext.getServiceContext().
- On the [Callee] side, create an
org.apache.dubbo.rpc.Filterfile in the\resources\META-INF\dubbo\directory:
dubboProviderFilter=com.pj.DubboProviderFilter
Create the DubboProviderFilter.java filter:
package com.pj;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import cn.dev33.satoken.same.SaSameUtil;
/**
* Sa-Token integrates Dubbo Provider-side filter
*/
@Activate(group = {CommonConstants.PROVIDER}, order = -10000)
public class DubboProviderFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// Retrieve Same-Token for verification
String sameToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN);
// Some protocols may convert the attachment key to lowercase
if(sameToken == null) {
sameToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN.toLowerCase());
}
SaSameUtil.checkToken(sameToken);
// Retrieve other custom additional data
// TenantContext tenantContext = invocation.getAttachment("tenantContext");
// Start the call
return invoker.invoke(invocation);
}
}
Then we can make secure RPC calls. Any call without the Same-Token parameter will throw an exception and fail to succeed.
References
- Sa-Token Documentation: https://sa-token.com
- Gitee Repository Address: https://gitee.com/dromara/sa-token
- GitHub Repository Address: https://github.com/dromara/sa-token
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
why does the application service still need to reference satoken? shouldn't only the login and gateway modules reference satoken
same-token itself has nothing to do with login and is not the same thing as a session token. same-token is only responsible for verifying the call source; it won't let through anything that isn't a gateway or internal RPC call.