User authentication scheme under spring cloud

Keywords: Spring Cloud Microservices Cloud Native

The commonly used Security frameworks under ava mainly include Spring Security and shiro, which can provide very powerful functions, but the learning cost is high. Under microservice, authentication will be more or less invasive to the service. In order to reduce dependence, reduce intrusion and make the authentication function transparent to the application service, we use the gateway to intercept resource requests for authentication.

1, Overall architecture

The user authentication module is located in the API GateWay service, and all API resource requests need to pass from here.

do If the authentication is passed, the user authority data will be cached. If not, the user authority data will be returned 401
do For user authentication, compare whether the current access resources (URI and Method) are in the cached user permission data. If they are, they will forward the request to the corresponding application service, and if they are not, they will return 403

2, Implementation steps

1. User login

public LoginUser login(String userName, String password){
    // Check password
    User user = userService.checkUser(userName, password);

    LoginUser loginUser = LoginUser.builder()
            .userName(userName)
            .realName(user.getRealName())
            .userToken(UUID.randomUUID().toString())
            .loginTime(new Date())
            .build();

    // Save session
    session.saveSession(loginUser);

    // Query authority
    List<Permission> permissions = permissionRepository.findByUserName(userName);
    // Save user permissions to cache
    session.saveUserPermissions(userName, permissions);

    return loginUser;
}

// ...
// Cache user permissions to Redis
public void saveUserPermissions(String userName, List<Permission> permissions) {
    String key = String.format("login:permission:%s", userName);

    HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
    hashOperations.putAll(key, permissions.stream().collect(
            Collectors.toMap(p -> p.getMethod().concat(":").concat(p.getUri()),
                    Permission::getName, (k1, k2) -> k2)));

    if (expireTime != null) {
        redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
    }
}
  • After the user passes the authentication, issue the userToken to save the current login information and cache the user authorization list
  • When caching the authorization list, in order to facilitate reading, use the hash method to save it as a list. Do not directly save the array object as an object

2. Intercept request

@Slf4j
@Component
public class AuthorizationFilter extends AbstractGatewayFilterFactory {

    @Autowired
    private Session session;

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {

            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            String uri = request.getURI().getPath();
            String method = request.getMethodValue();

            // 1. Get userName from AuthenticationFilter
            String key = "X-User-Name";
            if (!request.getHeaders().containsKey(key)) {
                response.setStatusCode(HttpStatus.FORBIDDEN);
                return response.setComplete();
            }

            String userName = Objects.requireNonNull(request.getHeaders().get(key)).get(0);

            // 2. Verify permissions
            if (!session.checkPermissions(userName, uri, method)) {
                log.info("User:{}, No permission", userName);
                response.setStatusCode(HttpStatus.FORBIDDEN);
                return response.setComplete();
            }

            return chain.filter(exchange);
        };
    }
}
  • Step 1: take out the X-User-Name passed by the identity authentication module
  • The second step is to check whether there are corresponding permissions in the cache
public boolean checkPermissions(String userName, String uri, String method) {
    String key = String.format("login:permission:%s", userName);
    String hashKey = String.format("%s:%s", method, uri);

    if (redisTemplate.opsForHash().hasKey(key, hashKey)){
        return  true;
    }

    String allKey = "login:permission:all";
    // If not in the permission list, pass
    return !redisTemplate.opsForHash().hasKey(allKey, hashKey);
}
  • If not in the permission list, pass   It is mainly to let go of some unnecessary public resources, which can be accessed by default
  • login:permission:all   All configured permission lists need to be put into the cache when the program starts, and the data needs to be kept updated

3. Authentication Filter configuration

spring:
  cloud:
    gateway:
      routes:
        - id: cloud-user
          uri: lb://Cloud user # backend service name
          predicates:
            - Path=/user/**   # Routing address
          filters:
            - name: AuthenticationFilter  # identity authentication 
            - name: AuthorizationFilter   # User authentication
            - StripPrefix=1 # Remove prefix
  • Pay special attention to the order of filter s. Identity authentication must be done before authentication
  • If more routes need to be configured, you can use the default filters default Filter configuration

3, Other issues

During unit testing, if the following errors are encountered

nested exception is java.lang.NoClassDefFoundError: javax/validation/ValidationException

Please upgrade the dependent package version:

<!--upgrade validation-api Version of-->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.5.Final</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

Posted by CrashRoX on Wed, 10 Nov 2021 19:03:13 -0800