Microservice architecture integrates Sa-Token to implement gateway authentication and authentication services

Microservice architecture integrates Sa-Token to implement gateway authentication and authentication services

This project is a hands-on project I made, using the SpringCloudAlibaba microservice architecture. When I was working on the authentication module, I remembered the Sa-Token project I had seen online before. It was called the Light of Domestic Authentication, so I checked their documentation implementation. A set of authentication services
After using the whole set, I found that SaToken still caters to the coding habits of domestic programmers. Different from the cumbersome filter chain responsibility chain model of Spring Security, SaToken mainly relies on calling methods in the method class to implement login, logout, authentication, etc. a series of processes

Project structure

image.png

image.png

Code implementation

Introduce dependencies. Since it is a microservice project, our user information must be stored on Redis. SaToken has provided us with the dependencies for SaToken to integrate Redis. We do not need to manually implement the code. Note that because the GateWay gateway is implemented based on WebFlux, it is different from our usual MVC. The dependencies introduced by the project are different, which have also been mentioned in the official documentation of SaToken.

<!--Sa-token-->
<!-- Sa-Token authority authentication (Reactor responsive integration), online documentation: https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>

<!-- Sa-Token integrates Redis (using jackson serialization method) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-jackson</artifactId>
    <version>1.37.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

The authentication interceptor SaToken in the gateway has also left us with expansion points. We only need to implement our own business logic. The code is as follows

package com.titi.apigateway.config;

import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.titi.titicommon.enums.AppHttpCodeEnum;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

import java.util.stream.Collectors;

@Configuration
public class SaTokenFilterConfiguration {
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                //Intercept address
                .addInclude("/**") /* Intercept all paths */
                // open address
                .addExclude("/favicon.ico")
                //Authentication method: Enter for each visit
                .setAuth(obj -> {
                    // Login verification -- intercept all routes and exclude /auth/** for open login
                    SaRouter.match("/**", "/auth/**", r -> StpUtil.checkLogin());
                    // Permission authentication -- different modules, verify different permissions
                    SaRouter.match("/passenger/**", r -> StpUtil.checkPermission("passenger"));
                    SaRouter.match("/driver/**", r -> StpUtil.checkPermission("driver"));
                    SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// More matches ... */
                })
                //Exception handling method: Enter every time an exception occurs in the setAuth function
                .setError(e -> {
                    return SaResult.error(e.getMessage());
               });
    }

    /**
     * Since the gateway does not introduce springMVC dependencies, you need to manually assemble messageConverters when using feign
     * @param converters
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

In order to be compatible with different business scenarios, SaToken also opens the method of obtaining permissions and obtaining the current user role to our users. You need to implement the corresponding interface and then register it in Spring.
You can implement the logic here according to your own system. The general idea is to obtain the permissions or role information corresponding to the userId from the cache or the table in the database.
When you debug the source code for the permissions obtained here, you can see that when the request passes the SaToken authentication filter, the bottom layer will call these two methods to obtain user permissions for authentication.

package com.titi.apigateway.config;

import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import com.titi.feign.client.UserServiceClient;
import com.titi.titicommon.DTO.UserPermissionDto;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Custom permission verification interface extension
 */
@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    private UserServiceClient userServiceClient;

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // Return the list of permissions owned by this loginId
        //Call the remote service to obtain the permission list
        ResponseResult<List<UserPermissionDto>> userPermission = userServiceClient.getUserPermission(Long.parseLong(loginId.toString()));
        List<UserPermissionDto> permissList = userPermission.getData();
        if (CollUtil.isEmpty(permissList)){
            return null;
        }
        return permissList.stream().map(UserPermissionDto::getPermission).collect(Collectors.toList());
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // Return the list of roles owned by this loginId
        return null;
    }

}

Authorized login section

Here I abstract the Auth service for role login under multiple identities, because my system includes the passenger side, the driver side, and the administrator side. Normally, for the scalability of the system, I should add a role table. Permission verification, but since it is a practice project, it is as simple as possible. I just used a single permission table to do it.
In order to ensure that the Auth service is as lightweight as possible, I write most of the business logic in the User service and use Feign to make remote calls.
It can be seen from the code that after the login logic verification is passed, we only need to hand over the user’s userId to the StpUtil tool class SaToken, which will help us do things such as Token generation, Token timeliness settings, Token synchronization with Redis, and generate and return Cookies to store Tokens. For the front end, it is very convenient

package com.titi.auth.controller;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import com.titi.feign.client.SMSServiceClient;
import com.titi.feign.client.UserServiceClient;
import com.titi.titicommon.DO.TitiUser;
import com.titi.titicommon.DTO.SMSUserVerifyRequest;
import com.titi.titicommon.DTO.TitiUserDto;
import com.titi.titicommon.DTO.UserVerifyRequest;
import com.titi.titicommon.constants.AuthConstants;
import com.titi.titicommon.constants.RegexConstants;
import com.titi.titicommon.enums.AppHttpCodeEnum;
import com.titi.titicommon.exception.BussinessException;
import com.titi.titicommon.result.ResponseResult;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

@RestController
public class PassengerUserController {

    @Autowired
    private UserServiceClient userServiceClient;

    @Autowired
    private SMSServiceClient smsServiceClient;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * Login and register two-in-one interface
     * @param titiUserDto
     * @param httpServletRequest
     * @return
     */
    @PostMapping("/doLogin")
    public ResponseResult doLoginOrRegister(@RequestBody TitiUserDto titiUserDto, HttpServletRequest httpServletRequest){
        //Call user service to log in or register.
        ResponseResult login = userServiceClient.login(titiUserDto);
        if (!login.getCode().equals(200)){
            return ResponseResult.errorResult(login.getCode(),login.getErrorMessage());
        }
        Long userId = (Long) login.getData();
        StpUtil.login(userId);
        return ResponseResult.okResult(StpUtil.getTokenInfo());
    }

    @PostMapping("/logout")
    public ResponseResult doLogout(){
        if (StpUtil.isLogin()) {
            StpUtil.logout(StpUtil.getLoginId());
        }
        return ResponseResult.okResult(null);
    }

    @PostMapping("/sendVerifyCode")
    public ResponseResult sendVerifyCode(@RequestBody @Validated UserVerifyRequest userVerifyRequest, HttpServletRequest httpServletRequest){
        SMSUserVerifyRequest smsUserVerifyRequest = new SMSUserVerifyRequest();
        BeanUtils.copyProperties(userVerifyRequest,smsUserVerifyRequest);
        smsUserVerifyRequest.setIp(httpServletRequest.getRemoteAddr());
        String verifyTypeKey = AuthConstants.verifyMap.get(userVerifyRequest.getVerifyType());
        if (StrUtil.isBlank(verifyTypeKey)){
            throw new BussinessException(AppHttpCodeEnum.PARAM_INVALID);
        }
        smsUserVerifyRequest.setVerifyType(verifyTypeKey);
        //The verification code can only be received once within 60s, and is implemented using Redis locking.
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(verifyTypeKey + userVerifyRequest.getPhone()))) {
            throw new BussinessException(AppHttpCodeEnum.SMS_TIME_LIMIT);
        }
        //Call SMS service to send information
        return smsServiceClient.sendVerifyCode(smsUserVerifyRequest);
    }
}

GateWay rewrites the forwarding request and forwards the Token to the service

In some business scenarios, when we distribute the request to the service after the GateWay gateway authentication is completed, some scenarios in the service still need to use user information. However, at this time, because the Token transmitted from the front end of the GateWay forwarding request has been lost, at this time we can Add a global filter in GateWay to rewrite the forwarding request before forwarding the request, and carry the user information in the forwarding request. Here I directly carry the userId because it is an internal service call and there is no need to worry about security issues.
`

package com.titi.apigateway.filter;

import cn.dev33.satoken.stp.StpUtil;
import com.titi.titicommon.constants.AuthConstants;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest newRequest = exchange
                .getRequest()
                .mutate()
                .header(AuthConstants.HTTP_LOGIN_HEADER, StpUtil.getLoginId(-1L).toString())
                .build();
        ServerWebExchange finalRequest = exchange.mutate().request(newRequest).build();
        return chain.filter(finalRequest);
    }
}

Conclusion

The user experience of SaToken is still very good. It is indeed a domestic project. It is simple and easy to use and provides many expansion points for users to expand. You can give it a try.