feat: add Internet-Only captcha rule (#3919)

This commit is contained in:
Robin Ye
2025-07-03 02:39:06 +08:00
committed by GitHub
parent 3ccc0339c7
commit bf91ad6c97
34 changed files with 122 additions and 35 deletions

View File

@ -555,8 +555,11 @@ func (c *ApiController) Login() {
c.ResponseError(c.T("auth:The login method: login with LDAP is not enabled for the application")) c.ResponseError(c.T("auth:The login method: login with LDAP is not enabled for the application"))
return return
} }
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
var enableCaptcha bool var enableCaptcha bool
if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username); err != nil { if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username, clientIp); err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} else if enableCaptcha { } else if enableCaptcha {
@ -1222,27 +1225,26 @@ func (c *ApiController) GetQRCode() {
func (c *ApiController) GetCaptchaStatus() { func (c *ApiController) GetCaptchaStatus() {
organization := c.Input().Get("organization") organization := c.Input().Get("organization")
userId := c.Input().Get("userId") userId := c.Input().Get("userId")
user, err := object.GetUserByFields(organization, userId) applicationName := c.Input().Get("application")
application, err := object.GetApplication(fmt.Sprintf("admin/%s", applicationName))
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if application == nil {
captchaEnabled := false c.ResponseError("application not found")
if user != nil { return
var failedSigninLimit int
failedSigninLimit, _, err = object.GetFailedSigninConfigByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
if user.SigninWrongTimes >= failedSigninLimit {
captchaEnabled = true
}
} }
clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
captchaEnabled, err := object.CheckToEnableCaptcha(application, organization, userId, clientIp)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(captchaEnabled) c.ResponseOk(captchaEnabled)
return
} }
// Callback // Callback

View File

@ -593,7 +593,7 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
return "" return ""
} }
func CheckToEnableCaptcha(application *Application, organization, username string) (bool, error) { func CheckToEnableCaptcha(application *Application, organization, username string, clientIp string) (bool, error) {
if len(application.Providers) == 0 { if len(application.Providers) == 0 {
return false, nil return false, nil
} }
@ -603,6 +603,12 @@ func CheckToEnableCaptcha(application *Application, organization, username strin
continue continue
} }
if providerItem.Rule == "Internet-Only" {
if util.IsInternetIp(clientIp) {
return true, nil
}
}
if providerItem.Rule == "Dynamic" { if providerItem.Rule == "Dynamic" {
user, err := GetUserByFields(organization, username) user, err := GetUserByFields(organization, username)
if err != nil { if err != nil {

View File

@ -185,17 +185,3 @@ func removePort(s string) string {
} }
return ipStr return ipStr
} }
func isHostIntranet(s string) bool {
ipStr, _, err := net.SplitHostPort(s)
if err != nil {
ipStr = s
}
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}

View File

@ -83,7 +83,7 @@ func CorsFilter(ctx *context.Context) {
setCorsHeaders(ctx, origin) setCorsHeaders(ctx, origin)
} else if originHostname == host { } else if originHostname == host {
setCorsHeaders(ctx, origin) setCorsHeaders(ctx, origin)
} else if isHostIntranet(host) { } else if util.IsHostIntranet(host) {
setCorsHeaders(ctx, origin) setCorsHeaders(ctx, origin)
} else { } else {
ok, err := object.IsOriginAllowed(origin) ok, err := object.IsOriginAllowed(origin)

47
util/network.go Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"net"
)
func IsInternetIp(ip string) bool {
ipStr, _, err := net.SplitHostPort(ip)
if err != nil {
ipStr = ip
}
parsedIP := net.ParseIP(ipStr)
if parsedIP == nil {
return false
}
return !parsedIP.IsPrivate() && !parsedIP.IsLoopback() && !parsedIP.IsMulticast() && !parsedIP.IsUnspecified()
}
func IsHostIntranet(ip string) bool {
ipStr, _, err := net.SplitHostPort(ip)
if err != nil {
ipStr = ip
}
parsedIP := net.ParseIP(ipStr)
if parsedIP == nil {
return false
}
return parsedIP.IsPrivate() || parsedIP.IsLoopback() || parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast()
}

View File

@ -163,7 +163,7 @@ export function getWechatQRCode(providerId) {
} }
export function getCaptchaStatus(values) { export function getCaptchaStatus(values) {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}`, { return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}&application=${values["application"]}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@ -134,6 +134,8 @@ class LoginPage extends React.Component {
return CaptchaRule.Always; return CaptchaRule.Always;
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) { } else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) {
return CaptchaRule.Dynamic; return CaptchaRule.Dynamic;
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Internet-Only")) {
return CaptchaRule.InternetOnly;
} else { } else {
return CaptchaRule.Never; return CaptchaRule.Never;
} }
@ -443,6 +445,9 @@ class LoginPage extends React.Component {
} else if (captchaRule === CaptchaRule.Dynamic) { } else if (captchaRule === CaptchaRule.Dynamic) {
this.checkCaptchaStatus(values); this.checkCaptchaStatus(values);
return; return;
} else if (captchaRule === CaptchaRule.InternetOnly) {
this.checkCaptchaStatus(values);
return;
} }
} }
this.login(values); this.login(values);
@ -961,9 +966,23 @@ class LoginPage extends React.Component {
const captchaProviderItems = this.getCaptchaProviderItems(application); const captchaProviderItems = this.getCaptchaProviderItems(application);
const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always"); const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always");
const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic"); const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic");
const provider = alwaysProviderItems.length > 0 const internetOnlyProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Internet-Only");
? alwaysProviderItems[0].provider
: dynamicProviderItems[0].provider; // Select provider based on the active captcha rule, not fixed priority
const captchaRule = this.getCaptchaRule(this.getApplicationObj());
let provider = null;
if (captchaRule === CaptchaRule.Always && alwaysProviderItems.length > 0) {
provider = alwaysProviderItems[0].provider;
} else if (captchaRule === CaptchaRule.Dynamic && dynamicProviderItems.length > 0) {
provider = dynamicProviderItems[0].provider;
} else if (captchaRule === CaptchaRule.InternetOnly && internetOnlyProviderItems.length > 0) {
provider = internetOnlyProviderItems[0].provider;
}
if (!provider) {
return null;
}
return <CaptchaModal return <CaptchaModal
owner={provider.owner} owner={provider.owner}

View File

@ -181,4 +181,5 @@ export const CaptchaRule = {
Always: "Always", Always: "Always",
Never: "Never", Never: "Never",
Dynamic: "Dynamic", Dynamic: "Dynamic",
InternetOnly: "Internet-Only",
}; };

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Přizpůsobit hlavičku vstupní stránky vaší aplikace", "Header HTML - Tooltip": "Přizpůsobit hlavičku vstupní stránky vaší aplikace",
"Incremental": "Inkrementální", "Incremental": "Inkrementální",
"Input": "Vstup", "Input": "Vstup",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Kód pozvánky", "Invitation code": "Kód pozvánky",
"Left": "Vlevo", "Left": "Vlevo",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Links", "Left": "Links",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Izquierda", "Left": "Izquierda",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "کد head صفحه ورود برنامه خود را سفارشی کنید", "Header HTML - Tooltip": "کد head صفحه ورود برنامه خود را سفارشی کنید",
"Incremental": "افزایشی", "Incremental": "افزایشی",
"Input": "ورودی", "Input": "ورودی",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "کد دعوت", "Invitation code": "کد دعوت",
"Left": "چپ", "Left": "چپ",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incrémentale", "Incremental": "Incrémentale",
"Input": "Saisie", "Input": "Saisie",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Code d'invitation", "Invitation code": "Code d'invitation",
"Left": "Gauche", "Left": "Gauche",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Kiri", "Left": "Kiri",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "左", "Left": "左",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "왼쪽", "Left": "왼쪽",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Código de convite", "Invitation code": "Código de convite",
"Left": "Esquerda", "Left": "Esquerda",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Последовательный", "Incremental": "Последовательный",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Код приглашения", "Invitation code": "Код приглашения",
"Left": "Левый", "Left": "Левый",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Vlastný HTML kód pre hlavičku vašej vstupnej stránky aplikácie", "Header HTML - Tooltip": "Vlastný HTML kód pre hlavičku vašej vstupnej stránky aplikácie",
"Incremental": "Postupný", "Incremental": "Postupný",
"Input": "Vstup", "Input": "Vstup",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Kód pozvania", "Invitation code": "Kód pozvania",
"Left": "Vľavo", "Left": "Vľavo",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Left", "Left": "Left",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Incremental", "Incremental": "Incremental",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Davet Kodu", "Invitation code": "Davet Kodu",
"Left": "Sol", "Left": "Sol",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Налаштуйте тег head на сторінці входу до програми", "Header HTML - Tooltip": "Налаштуйте тег head на сторінці входу до програми",
"Incremental": "Інкрементний", "Incremental": "Інкрементний",
"Input": "Введення", "Input": "Введення",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Код запрошення", "Invitation code": "Код запрошення",
"Left": "Ліворуч", "Left": "Ліворуч",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "Custom the head tag of your application entry page", "Header HTML - Tooltip": "Custom the head tag of your application entry page",
"Incremental": "Tăng", "Incremental": "Tăng",
"Input": "Input", "Input": "Input",
"Internet-Only": "Internet-Only",
"Invalid characters in application name": "Invalid characters in application name", "Invalid characters in application name": "Invalid characters in application name",
"Invitation code": "Invitation code", "Invitation code": "Invitation code",
"Left": "Trái", "Left": "Trái",

View File

@ -76,6 +76,7 @@
"Header HTML - Tooltip": "自定义应用页面的head标签", "Header HTML - Tooltip": "自定义应用页面的head标签",
"Incremental": "递增", "Incremental": "递增",
"Input": "输入", "Input": "输入",
"Internet-Only": "外网启用",
"Invalid characters in application name": "应用名称内有非法字符", "Invalid characters in application name": "应用名称内有非法字符",
"Invitation code": "邀请码", "Invitation code": "邀请码",
"Left": "居左", "Left": "居左",

View File

@ -255,6 +255,7 @@ class ProviderTable extends React.Component {
<Option key="None" value="None">{i18next.t("general:None")}</Option> <Option key="None" value="None">{i18next.t("general:None")}</Option>
<Option key="Dynamic" value="Dynamic">{i18next.t("application:Dynamic")}</Option> <Option key="Dynamic" value="Dynamic">{i18next.t("application:Dynamic")}</Option>
<Option key="Always" value="Always">{i18next.t("application:Always")}</Option> <Option key="Always" value="Always">{i18next.t("application:Always")}</Option>
<Option key="Internet-Only" value="Internet-Only">{i18next.t("application:Internet-Only")}</Option>
</Select> </Select>
); );
} else if (record.provider?.category === "SMS" || record.provider?.category === "Email") { } else if (record.provider?.category === "SMS" || record.provider?.category === "Email") {