Implementing Backend CAPTCHA Validation with ASP.NET Core and Vue.js

Implemanting Backend CAPTCHA Validation with ASP.NET Core and Vue.js

While client-side CAPTCHA logic can be implemented, it often presents security concerns. A more robust approach involves generating and validating CAPTCHA codes on the server side. This method is essential for applications deployed in intranet environments where third-party services like Tencent or Baidu CAPTCHA are unavailable. This article details a backend-driven CAPTCHA system using ASP.NET Core for the API and Vue.js for the frontend interface.

Core Implementation Principle

The system operates through a coordinated request cycle between the client and server:

  1. The frontend requests a CAPTCHA image from the backend, sending a unique roomId.
  2. The backend generates a random 4-character verification code, stores it in a Redis cache associated with the provided roomId, and returns a corresponding image.
  3. The frontend displays the image. During login, the user submits the entered code along with the roomId.
  4. The login API retrieves the correct code from Redis using the roomId and compares it with the user's input for validation.

This ensures the vreification logic is secured and controlled on the backend.

Frontend Vue Component Code

The following Vue component handles the login form, including CAPTCHA image display and refresh logic.

<template>
  <div class="auth-container">
    <el-form ref="authForm" :model="formData" :rules="validationRules" class="auth-form">
      <div class="header">
        <h3 class="app-title">System Portal</h3>
      </div>

      <el-form-item prop="userName">
        <span class="input-icon">
          <user-icon />
        </span>
        <el-input ref="userInput" v-model="formData.userName" placeholder="Username" />
      </el-form-item>

      <el-form-item prop="passKey">
        <span class="input-icon">
          <lock-icon />
        </span>
        <el-input
          :type="inputFieldType"
          ref="passInput"
          v-model="formData.passKey"
          placeholder="Password"
          @keyup.enter.native="initiateAuth"
        />
        <span class="toggle-visibility" @click="toggleInputType">
          <visibility-icon :state="inputFieldType" />
        </span>
      </el-form-item>

      <el-form-item prop="captchaInput">
        <span class="input-icon">
          <shield-icon />
        </span>
        <el-input v-model="formData.captchaValue" maxlength="4" placeholder="CAPTCHA Code" />
        <div class="captcha-image-container" @click="loadNewCaptcha">
          <el-image :src="captchaImageSource"></el-image>
        </div>
      </el-form-item>

      <el-button :loading="isProcessing" type="primary" @click.native.prevent="initiateAuth">
        Authenticate
      </el-button>
    </el-form>
  </div>
</template>

<script>
import { v4 as uuidv4 } from 'uuid';

export default {
  name: 'LoginView',
  data() {
    const validateUser = (rule, value, callback) => {
      if (!value || value.trim().length === 0) {
        callback(new Error('Username is required'));
      } else {
        callback();
      }
    };
    const validatePass = (rule, value, callback) => {
      if (value.length < 6) {
        callback(new Error('Password must be at least 6 characters'));
      } else {
        callback();
      }
    };
    return {
      formData: {
        userName: 'admin',
        passKey: 'defaultPass',
        sessionId: '',
        captchaValue: ''
      },
      validationRules: {
        userName: [{ required: true, trigger: 'blur', validator: validateUser }],
        passKey: [{ required: true, trigger: 'blur', validator: validatePass }]
      },
      inputFieldType: 'password',
      isProcessing: false,
      captchaImageSource: ''
    };
  },
  created() {
    this.loadNewCaptcha();
  },
  methods: {
    toggleInputType() {
      this.inputFieldType = this.inputFieldType === 'password' ? 'text' : 'password';
    },
    loadNewCaptcha() {
      const newSessionId = uuidv4().replace(/-/g, '');
      this.formData.sessionId = newSessionId;
      this.captchaImageSource = `/api/Auth/getCaptchaImage/120/40/${newSessionId}`;
    },
    async initiateAuth() {
      const isValid = await this.$refs.authForm.validate();
      if (!isValid) {
        console.error('Form validation failed');
        return;
      }
      this.isProcessing = true;
      try {
        await this.$store.dispatch('user/authenticate', this.formData);
        this.$router.push({ path: this.redirectPath || '/' });
      } catch (error) {
        console.error('Authentication failed:', error);
        this.loadNewCaptcha();
      } finally {
        this.isProcessing = false;
      }
    }
  }
};
</script>

<style scoped>
.auth-container {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #2d3a4b;
}
.auth-form {
  width: 420px;
  padding: 40px;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 8px;
}
.app-title {
  text-align: center;
  color: #eee;
  margin-bottom: 30px;
}
.captcha-image-container {
  cursor: pointer;
  margin-left: 10px;
}
.toggle-visibility {
  cursor: pointer;
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
}
</style>

Backend ASP.NET Core API Endpoints

The backend provides two primary endpoints: one for CAPTCHA generation and another for user authentication with CAPTCHA validation.

CAPTCHA Image Generation Endpoint

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IDatabase _redisDb;

    public AuthController(IConnectionMultiplexer redis)
    {
        _redisDb = redis.GetDatabase();
    }

    [HttpGet("getCaptchaImage/{width}/{height}/{sessionId}")]
    public IActionResult GenerateCaptcha(int width, int height, string sessionId)
    {
        string generatedCode = CaptchaHelper.CreateRandomCode(4);
        byte[] imageData = CaptchaHelper.RenderImage(generatedCode, width, height);

        string redisKey = $"Captcha_{sessionId}";
        _redisDb.StringSet(redisKey, generatedCode, TimeSpan.FromMinutes(5));

        return File(imageData, "image/png");
    }

Login Endpoint with CAPTCHA Validation

    [HttpPost("login")]
    public ApiResponse Authenticate([FromBody] LoginRequest credentials)
    {
        if (string.IsNullOrWhiteSpace(credentials.UserName))
            return ApiResponse.Fail("Username cannot be empty");
        if (string.IsNullOrWhiteSpace(credentials.PassKey))
            return ApiResponse.Fail("Password cannot be empty");

        var user = _userRepository.GetActiveUser(credentials.UserName, credentials.PassKey);
        if (user == null)
            return ApiResponse.Fail("Invalid username or password");

        string captchaKey = $"Captcha_{credentials.SessionId}";
        string storedCode = _redisDb.StringGet(captchaKey);
        if (storedCode != credentials.CaptchaValue)
            return ApiResponse.Fail("Incorrect CAPTCHA code");

        string newAuthToken = Guid.NewGuid().ToString("N");
        _userRepository.UpdateUserToken(user.Id, newAuthToken);

        string userRedisKey = $"User_{newAuthToken}";
        _redisDb.StringSet(userRedisKey, user.Id.ToString(), TimeSpan.FromHours(24));

        return ApiResponse.Success(new { token = newAuthToken });
    }
}

Key Implementation Details

  • CAPTCHA Storage: The generated code is stored in Redis with a key composed of a prefix and the sessionId (called roomId in the original), setting an expiration time (e.g., 5 minutes) to ensure security and clean up unused data.
  • Validation Flow: During login, the backend retrieves the stored code using the provided sessionId and compares it with the user's input. A mismatch results in a failed authentication attempt.
  • Token Management: Upon successful login, a new GUID token is generated, stored in the user database record, and also cached in Redis to manage user sessions effectively.

This architecture decouples the CAPTCHA generation from validation, ensuring the verification logic is server-side and secure against client-side manipulation.

Tags: ASP.NET Core Vue.js CAPTCHA Redis Backend Validation

Posted on Wed, 20 May 2026 20:46:03 +0000 by kuri7548