Cấu hình và Kiểm tra Hành vi MFA

Cấu trúc Hệ thống

Khi người dùng đăng nhập hoặc thực hiện các thao tác nhạy cảm, hệ thống sử dụng cơ chế xác thực hai yếu tố để tăng cường bảo mật. Các biện pháp cụ thể bao gồm:

  1. Xác thực bằng Captcha: Sử dụng mã Captcha để ngăn chặn các cuộc tấn công tự động và kịch bản độc hại, đảm bảo an toàn trong quá trình đăng nhập.
  2. Xác thực Telegram cho người dùng quốc tế: Thêm lớp xác thực thông qua Telegram nhằm củng cố tính bảo mật.
  3. Xác thực Google: Phân chia thành hai loại: một là kiểm tra thao tác nhạy cảm, áp dụng yêu cầu bắt buộc thông qua chú thích; loại kia là kiểm tra hành vi định kỳ, thường yêu cầu nhập mã xác minh mỗi 30 phút.

Cơ chế này không chỉ bảo vệ việc đăng nhập mà còn cung cấp lớp bảo vệ bổ sung cho các hoạt động quan trọng, tạo nhiều lớp phòng thủ cho hệ thống. Bài viết này sẽ đề cập đến cấu hình phía sau, bộ lọc kiểm tra hành vi và cấu trúc bảng dữ liệu.

Các Hằng Số

Hai hằng số được định nghĩa để lưu trữ trạng thái liên kết của người dùng với Google Authenticator, lưu trữ trong bảng t_user_ref.

    _GOOGLE_MFA_BIND_FLAG("MFA_GOOGLE_BIND", "Google Bind Flag", "system.mfa.google.bind.flag"),
    _GOOGLE_SECRET_KEY("MFA_GOOGLE_SECRET", "Google Secret", "system.mfa.google.secret.key"),

Cấu trúc Bảng Dữ Liệu

Dữ liệu khóa bí mật từ Google sẽ được mã hóa bằng AES trước khi lưu trữ.

CREATE TABLE `t_user_ref` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `attr_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  `attr_value` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `created_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  `note` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`user_id`, `attr_name`) USING BTREE,
  KEY `idx_id` (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

Lớp API

Đoạn code dưới đây chỉ hiển thị phần nhỏ của lớp API, chi tiết có thể tìm thấy tại kho mã nguồn mở.

@RestController
@RequestMapping("/api/mfa")
@Api(tags = "API Xác Thực Hai Yếu Tố")
public class MfaController {

    @Autowired
    private UserService userService;
    @Value("${mfa.expiry_time:1800}")
    private Long expiryTime;

    @ApiOperation("Kiểm tra trạng thái liên kết Google Authenticator")
    @GetMapping("/check-google-binding")
    public ApiResponse<Map<String, Object>> checkGoogleBinding() throws Exception {
        User user = RequestContext.getCurrent().getUser();
        ApiResponse<Map<String, Object>> response = ApiResponse.success();
        UserRef ref = userService.getUserRef(user.getId(), _GOOGLE_MFA_BIND_FLAG.value());
        Map<String, Object> data = new LinkedHashMap<>();
        if (ref != null && "true".equals(ref.getAttrValue())) {
            data.put(_GOOGLE_MFA_BIND_FLAG.value(), true);
        } else {
            String googleSecretEnc = GoogleAuthUtil.getSecret(user.getUsername(), true);
            if (googleSecretEnc == null) {
                ref = GoogleAuthUtil.createUserRef(user);
                userService.create(ref);
                googleSecretEnc = ref.getAttrValue();
            }
            String googleSecret = AesUtil.decrypt(googleSecretEnc);
            data.put(_GOOGLE_MFA_BIND_FLAG.value(), false);
            data.put("description", "Nhập mã QR vào ứng dụng Google Authenticator");
            data.put("secret", googleSecret);
            data.put("qr_code", GoogleAuthUtil.getQrCode(user.getUsername(), googleSecret));
            RedisUtil.set(__MFA_USER_CACHE + user.getId(), googleSecretEnc);
        }
        response.setData(data);
        return response;
    }

    @ApiOperation("Liên kết Google Authenticator")
    @PostMapping("/bind-google-auth")
    public ApiResponse<UserRef> bindGoogleAuth(@RequestParam String token) throws Exception {
        String googleSecret = GoogleAuthUtil.getSecret(token);
        if (!GoogleAuthUtil.verifyToken(googleSecret, token)) {
            throw new AppException("Xác thực thất bại");
        }
        User user = RequestContext.getCurrent().getUser();
        ApiResponse<UserRef> response = ApiResponse.success();
        UserRef ref = userService.getUserRef(user.getId(), _GOOGLE_MFA_BIND_FLAG.value());
        if (ref == null) {
            ref = new UserRef();
            ref.setUserId(user.getId());
            ref.setAttrName(_GOOGLE_MFA_BIND_FLAG.value());
            ref.setAttrValue("true");
            ref.setNote("Đã liên kết với Google Authenticator");
            userService.create(ref);
        } else {
            ref.setAttrValue("true");
            userService.update(ref);
        }
        response.setData(ref);
        AsyncUtil.run(() -> RedisUtil.removeUserToken(user.getId()));
        return response;
    }
}

Bộ Lọc Kiểm Tra Hành Vi

Bộ lọc này thực hiện xác thực định kỳ dựa trên thời gian cấu hình, nếu xác thực đã hết hạn trong Redis, nó sẽ yêu cầu xác thực lại. Key hết hạn được liên kết với IP của người dùng.

package com.example.mfa.config;

import static org.cloud.constant.LoginType.ADMIN_USER;
import static org.cloud.constant.MfaStatus.GOOGLE_NOT_VERIFIED_OR_EXPIRED;

import com.example.util.GoogleAuthUtil;
import java.io.IOException;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.Setter;
import org.cloud.context.RequestContext;
import org.cloud.context.RequestContextHolder;
import org.cloud.core.redis.RedisUtil;
import org.cloud.entity.UserDetails;
import org.cloud.exception.AppException;
import org.cloud.utils.HttpUtil;
import org.cloud.utils.IpUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;

@ConfigurationProperties(prefix = "security.mfa")
@Configuration
@ConditionalOnProperty(prefix = "security.mfa", name = "enabled", matchIfMissing = true)
public class MfaFilterConfig {

    public final static String __USER_VERIFY_RESULT = "auth:mfa:user:verify:result:";
    public final static String __USER_SECRET_RESULT = "auth:mfa:user:secret:result:";

    @Setter
    private List<String> excludedUris;
    private final String authType = "GOOGLE";

    @Bean
    public FilterRegistrationBean<?> mfaWebFilter() {
        this.excludedUris.add("/v2/api-docs");
        this.excludedUris.add("/internal/**");
        FilterRegistrationBean<?> registration = new FilterRegistrationBean<>(new MfaWebFilter(excludedUris));
        registration.addUrlPatterns("/*");
        registration.setName("mfaWebFilter");
        registration.setOrder(100);
        return registration;
    }

    static class MfaWebFilter extends OncePerRequestFilter {

        private final List<String> noMfaCheckUrls;

        public MfaWebFilter(List<String> noMfaCheckUrls) {
            this.noMfaCheckUrls = noMfaCheckUrls;
        }

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
            if (HttpUtil.isExcluded(request, noMfaCheckUrls)) {
                chain.doFilter(request, response);
                return;
            }
            RequestContext context = RequestContextHolder.getContext();
            UserDetails user = context.getUser();
            if (user == null || !ADMIN_USER.equals(user.getType())) {
                chain.doFilter(request, response);
                return;
            }
            try {
                GoogleAuthUtil.verifyCurrentUser(user);
            } catch (AppException e) {
                HttpUtil.handleException(e, response);
                logger.error("Người dùng chưa xác thực Google");
                return;
            }
            String ipHash = RedisUtil.getMd5Key(IpUtil.getIp(request));
            Boolean isValid = RedisUtil.get(__USER_VERIFY_RESULT + user.getId() + ":" + ipHash);
            if (isValid != null && isValid) {
                chain.doFilter(request, response);
            } else {
                HttpUtil.handleException(new AppException(GOOGLE_NOT_VERIFIED_OR_EXPIRED.value()), response);
                logger.error("Xác thực Google đã hết hạn hoặc chưa được thực hiện");
            }
        }
    }
}

Thẻ: MFA GoogleAuthenticator aes Redis API

Đăng vào ngày 29 tháng 6 lúc 02:24