搜尋
首頁Javajava教程了解 Spring Security 和 OAuth

在本文中,我們將探索 Spring 安全性,並使用 OAuth 2.0 建構一個驗證系統。

Spring Security 是一個功能強大、高度可自訂的框架,用於在基於 Java 的應用程式中實現強大的身份驗證和存取控制機制。它是 Spring 生態系統的核心元件,廣泛用於保護 Web 應用程式、REST API 和其他後端服務的安全。透過 Spring Security,您可以為在應用程式中建立和實施安全實踐奠定堅實的基礎。


Spring Security 的工作原理

在深入了解 Spring Security 的運作方式之前,了解基於 Java 的 Web 伺服器中的請求處理生命週期至關重要。 Spring Security 無縫整合到此生命週期中以保護傳入請求。


使用 Spring Security 處理請求生命週期

使用 Spring Security 在基於 Spring 的應用程式中處理 HTTP 請求的生命週期涉及多個階段,每個階段在處理、驗證和保護請求方面都發揮關鍵作用。


1. 客戶請求

當客戶端(例如瀏覽器、行動應用程式或 Postman 等 API 工具)向伺服器發送 HTTP 請求時,生命週期就開始了。

範例:

GET /api/admin/dashboard HTTP/1.1


2. Servlet 容器

Servlet 容器(例如 Tomcat)接收請求並將其委託給 DispatcherServlet(Spring 應用程式中的前端控制器)。這是應用程式處理管道開始的地方。


3. Spring Security 過濾器鏈

在 DispatcherServlet 處理請求之前,Spring Security 的過濾器鏈 會攔截它。過濾器鍊是一系列過濾器,每個過濾器負責處理特定的安全任務。這些過濾器可確保請求在到達應用程式邏輯之前滿足身份驗證和授權要求。

鏈中的關鍵過濾器

  1. 驗證過濾器:

    這些過濾器驗證請求是否包含有效的憑證,例如使用者名稱/密碼、JWT 或會話 cookie。

  2. 授權過濾器

    身份驗證後,這些過濾器可確保經過身份驗證的使用者俱有存取所請求資源所需的角色或權限。

  3. 其他濾鏡

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

4. 安全上下文

如果驗證成功,Spring Security 將建立一個 Authentication 物件並將其儲存在 SecurityContext 中。該物件通常儲存在執行緒本地儲存中,在整個請求生命週期中都可以存取。

驗證物件

  • Principal:代表經過驗證的使用者(例如使用者名稱)。

  • 憑證:包含 JWT 令牌或密碼等驗證詳細資訊。

  • 權限:包含指派給使用者的角色和權限。

過濾器鏈中的範例流程

  • 請求通過驗證過濾器。

  • 如果憑證有效,則會建立 Authentication 物件並將其新增至 SecurityContext。

  • 如果憑證無效,ExceptionTranslationFilter 會傳送 401 未經授權的回應給客戶端。


5. DispatcherServlet

一旦要求成功通過 Spring Security 過濾器鏈,DispatcherServlet 就會接管:

  1. 處理程序映射:

    它根據 URL 和 HTTP 方法將傳入請求對應到適當的控制器方法。

  2. 控制器呼叫:

    映射的控制器通常在服務和儲存庫等其他 Spring 元件的幫助下處理請求並傳回適當的回應。

Spring Security 如何融入生命週期

Spring Security 透過其過濾器將自身整合到此生命週期中,在最早階段攔截請求。當請求到達應用程式邏輯時,它已經經過身份驗證和授權,確保核心應用程式僅處理合法流量。


Spring Security 的設計確保以聲明方式處理身份驗證、授權和其他安全措施,使開發人員能夠根據需要靈活地自訂或擴展其行為。它不僅強制執行最佳實踐,還簡化了現代應用程式中複雜安全要求的實作。

Understanding Spring Security and OAuth

Spring Security 元件:超越過濾器鏈

探索了 Spring Security 中的過濾器鏈之後,讓我們深入研究一些在身份驗證和授權過程中發揮關鍵作用的其他關鍵組件。

認證管理器

AuthenticationManager 是一個定義單一方法的接口,authenticate(Authenticationauthentication),用於驗證使用者的憑證並確定它們是否有效。您可以將 AuthenticationManager 視為一個協調器,您可以在其中註冊多個提供者,並且根據請求類型,它將向正確的提供者發送身份驗證請求。

認證提供者

AuthenticationProvider 是一個接口,它定義了一個合約,用於根據使用者的憑證對使用者進行身份驗證。它代表特定的身份驗證機制,例如使用者名稱/密碼、OAuth 或 LDAP。多個 AuthenticationProvider 實作可以共存,允許應用程式支援各種身份驗證策略。

核心概念

  1. 驗證物件:

    AuthenticationProvider 處理 Authentication 對象,該對象封裝了使用者的憑證(例如使用者名稱和密碼)。

  2. 驗證方法:

    每個 AuthenticationProvider 都實作了authenticate(Authenticationauthentication)方法,實際的身份驗證邏輯駐留在其中。此方法:

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
  1. 支持方法: support(Class>authentication) 方法指示 AuthenticationProvider 是否可以處理給定類型的驗證。這允許 Spring Security 確定正確的提供者來處理特定的身份驗證請求。

範例

  • 資料庫支援的 AuthenticationProvider 驗證使用者名稱和密碼。

  • 基於 OAuth 的 AuthenticationProvider 驗證外部身分提供者所頒發的令牌。

用戶詳情服務

UserDetailsS​​​​ervice 在 Spring 文件中被描述為加載用戶特定資料的核心接口,它包含一個方法 loadUserByUsername ,該方法接受用戶名作為參數並返回 ==User== 身份對象。基本上我們建立並實作了 UserDetailsS​​ervice 的類,在其中重寫了 loadUserByUsername 方法。

* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.

現在這三者如何協同工作的是 AuthenticationManager 會要求 AuthenticationProvider 根據指定的 Provider 類型進行身份驗證,而 UserDetailsS​​ervice 實作將幫助 AuthenticationProvider 證明使用者詳細資料。

現在,在進行設定和所有內容之前,這裡是基於 JWT 身份驗證的 Spring Security 的簡明流程:

1. 使用者請求

  • 使用者使用其憑證(使用者名稱和密碼)或 JWT 令牌(標頭中)向經過驗證的端點發送請求,並將請求傳遞到身份驗證過濾器

  • AuthenticationFilter(例如,UsernamePasswordAuthenticationFilter):

    • 根據提交的憑證(通常以使用者名稱和密碼的形式)處理使用者身份驗證。這就是 UsernamePasswordAuthenticationFilter 發揮作用的地方。
    • 它監聽請求,提取使用者名稱和密碼,並將它們傳遞給 AuthenticationManager。
    • 但是我們沒有傳遞使用者名稱和密碼,我們只給出令牌,因此在此AuthenticationFilter 之前應該有一個過濾器,它將告訴身份驗證過程使用者已通過身份驗證,無需檢查使用者名稱和密碼,這透過創建JWTFilter
    • 來完成

2.JWT過濾器

這個自訂過濾器擴充了 OncePerRequestFilter 並放置在 UsernamePasswordAuthenticationFilter 之前,它的作用是從請求中提取令牌並驗證它。

如果令牌有效,它會建立一個UsernamePasswordAuthenticationToken 並將該令牌設定到安全性上下文中,告訴Spring Security 該請求已通過身份驗證,並且當此請求傳遞到UsernamePasswordAuthenticationFilter 時,它只是傳遞,因為它具有UsernamePasswordAuthenticationToken

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

如果我們在 UserDetails 類別的幫助下驗證使用者名稱和密碼後傳遞了使用者名稱和密碼而不是令牌,則此 UsernamePasswordAuthenticationToken 是在 AuthenticationManager 和 AuthenticationProvider 的幫助下產生的。

3. 身份驗證管理器

  • AuthenticationManager:它接收身份驗證請求並將其委託給我們配置的適當的 AuthenticationProvider。
* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.

4. 身份驗證提供者

  • UserDetailsS​​ervice:AuthenticationProvider 使用 UserDetailsS​​ervice 根據使用者名稱載入使用者詳細資料。我們為此提供了 UserDetailsS​​ervice

  • 的實現
  • 憑證驗證:它將提供的密碼與儲存在使用者詳細資料中的密碼進行比較(通常使用密碼編碼器)。

package com.oauth.backend.services;

import com.oauth.backend.entities.User;
import com.oauth.backend.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;


@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username);
        if(user==null){
            throw new UsernameNotFoundException(username);
        }
        return new UserDetailsImpl(user);
    }
    public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email);
        if(user==null){
            throw new UsernameNotFoundException(email);
        }
        return new UserDetailsImpl(user);
    }
}

現在需要配置所有這些不同的過濾器和 bean,以便 Spring security 知道要做什麼,因此我們建立一個配置類,在其中指定所有配置。

@Component
public class JWTFilter extends OncePerRequestFilter {

    private final JWTService jwtService;
    private final UserDetailsService userDetailsService;
    public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if(authHeader == null || !authHeader.startsWith("Bearer")) {
            filterChain.doFilter(request,response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userName = jwtService.extractUserName(jwt);

        Authentication authentication
                = SecurityContextHolder.getContext().getAuthentication();

        if(userName !=null  && authentication == null) {
            //Authenticate
            UserDetails userDetails
                    = userDetailsService.loadUserByUsername(userName);

            if(jwtService.isTokenValid(jwt,userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken
                        = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );

                SecurityContextHolder.getContext()
                        .setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request,response);
    }



}

到目前為止,我們已經在 Spring Security 的幫助下理解並配置了我們的身份驗證,現在是時候測試它了。

我們將建立一個簡單的應用程序,其中包含兩個控制器 AuthController(處理登入和註冊)和 ProductController(虛擬受保護控制器)

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.
package com.oauth.backend.services;

import com.oauth.backend.entities.User;
import com.oauth.backend.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;


@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username);
        if(user==null){
            throw new UsernameNotFoundException(username);
        }
        return new UserDetailsImpl(user);
    }
    public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email);
        if(user==null){
            throw new UsernameNotFoundException(email);
        }
        return new UserDetailsImpl(user);
    }
}
@Component
public class JWTFilter extends OncePerRequestFilter {

    private final JWTService jwtService;
    private final UserDetailsService userDetailsService;
    public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if(authHeader == null || !authHeader.startsWith("Bearer")) {
            filterChain.doFilter(request,response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userName = jwtService.extractUserName(jwt);

        Authentication authentication
                = SecurityContextHolder.getContext().getAuthentication();

        if(userName !=null  && authentication == null) {
            //Authenticate
            UserDetails userDetails
                    = userDetailsService.loadUserByUsername(userName);

            if(jwtService.isTokenValid(jwt,userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken
                        = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );

                SecurityContextHolder.getContext()
                        .setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request,response);
    }



}
@Bean  
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{  
return config.getAuthenticationManager();  
}
@Bean  
public AuthenticationProvider authenticationProvider(){  
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();  
authenticationProvider.setUserDetailsService(userDetailsServiceImpl);  
authenticationProvider.setPasswordEncoder(passwordEncoder);  
return authenticationProvider;  
}

到目前為止,我們已經實現了註冊、登入和驗證,但是如果我還想添加 Login With Google/Github 功能,那麼我們可以藉助 OAuth2.0 來實現

OAuth 2.0

OAuth 2.0 是一種用於授權的協議,透過它,使用者可以授予第三方應用程式存取儲存在其他平台(例如 Google Drive、Github)上的資源的權限,而無需共享這些平台的憑證。

它主要用於啟用社交登錄,例如「使用 google 登入」、「使用 github 登入」。

Google、Facebook、Github 等平台提供了授權伺服器,該伺服器實現了 OAuth 2.0 協議,用於此社交登入或授權存取。

OAuth 2.0 的關鍵概念

  • 資源所有者

  • 客戶

  • 授權伺服器

  • 資源伺服器

  • 訪問令牌

  • 範圍

  • 補助金

現在我們將一一研究每個概念

資源所有者

資源擁有者是想要授權第三方應用程式(您的應用程式)的使用者。

客戶

這是您的(第三方)應用程式想要從資源伺服器存取資料或資源。

資源伺服器

它是儲存用戶資料的伺服器,供第三方應用程式存取。

授權伺服器

對資源擁有者進行驗證並向用戶端(例如 Google 帳戶)頒發存取權杖的伺服器。

訪問令牌

授權伺服器向客戶端頒發的憑證,允許客戶端代表使用者存取資源伺服器。它的生命週期通常很短,很快就會過期,因此也提供了刷新令牌來刷新此存取令牌,以便用戶不需要再次授權。

範圍

使用者授予的特定權限,定義客戶端可以和不能對使用者資料執行什麼操作。例如,對於授權,我們只需要用戶信息,如個人資料、姓名等,但對於文件訪問,需要不同的範圍。

補助金

客戶端應用程式從授權伺服器取得存取權杖的方法。授權定義了授權用戶端應用程式存取資源擁有者的受保護資料的流程和條件。

它是安全的,因為我們不需要向瀏覽器公開我們的客戶端金鑰和其他憑證

OAuth 2.0 提供了兩種最常用的 Grant 類型

  1. 授權碼授予

    它是最常用的授權/方法類型,最安全,並且適用於伺服器端應用程式

    在此,授權碼由客戶端提供給後端,後端將存取權杖提供給客戶端。

    流程:

    1. 客戶端將使用者重新導向至授權伺服器。
    2. 使用者登入並同意。
    3. 授權伺服器發出授權碼
    4. 客戶端與後端交換授權代碼以取得存取權杖
  2. 隱性授予

    由單頁應用程式 (SPA) 或沒有後端的應用程式使用。在此,存取權杖是在瀏覽器本身中直接產生和頒發的。

    流程:

    1. 客戶端將使用者重新導向至授權伺服器。
    2. 使用者登入並同意。
    3. 授權伺服器直接核發存取權杖

為了完全理解,我們將分別實現兩者,但首先我們將實現我們需要的授權代碼授予

  1. 授權伺服器

    它可以是任何一個平台(例如google 、 github),或者您也可以使用KeyCloak 創建自己的平台,或者也可以遵循OAuth 2.0 標準來建立您自己的平台(我們可能會在下一篇部落格中這樣做?

  2. Spring Boot 應用

    這將是我們的主要後端應用程式/服務,它將處理代碼交換、驗證、保存用戶詳細資訊和分配 JWT 令牌等所有操作

  3. React 應用程式(前端)

    這將是我們的客戶端,它將使用者重新導向到授權伺服器進行授權。

因此,在我們的實作中,我們要做的是前端(網路/應用程式)將我們的用戶重定向到Google登錄,並將uri 重定向到我們的後端端點,這將進一步控制我們稍後將討論它以及redirect_url我們也傳遞應用程式的客戶端ID,所有這些都將在查詢參數中發送。

否,當使用者成功登入Google時,授權伺服器(Google的)會將我們的請求重定向到後端端點,我們要做的就是與授權伺服器交換授權程式碼以獲取存取權杖和刷新令牌,然後我們可以根據需要處理身份驗證,最後我們會將回應發送回我們的前端,該前端將有一個cookie 並重定向到我們的儀表板,或者可能是受保護的頁面。

現在我們將研究程式碼,但請確保在 OAuth 用戶端的 Google 控制台儀表板中的授權重定向 URL 中新增後端端點的 URL。


* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

就是這樣,它可以正常工作,為了測試,您可以製作一個簡單的前端應用程序,它除了具有上下文之外什麼都沒有,而您不知道登入和註冊功能。

感謝您閱讀這麼久,如果您有任何建議,請在評論中提出

以上是了解 Spring Security 和 OAuth的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
JVM性能與其他語言JVM性能與其他語言May 14, 2025 am 12:16 AM

JVM'SperformanceIsCompetitiveWithOtherRuntimes,operingabalanceOfspeed,安全性和生產性。 1)JVMUSESJITCOMPILATIONFORDYNAMICOPTIMIZAIZATIONS.2)c提供NativePernativePerformanceButlanceButlactsjvm'ssafetyFeatures.3)

Java平台獨立性:使用示例Java平台獨立性:使用示例May 14, 2025 am 12:14 AM

JavaachievesPlatFormIndependencEthroughTheJavavIrtualMachine(JVM),允許CodeTorunonAnyPlatFormWithAjvm.1)codeisscompiledIntobytecode,notmachine-specificodificcode.2)bytecodeisisteredbytheybytheybytheybythejvm,enablingcross-platerssectectectectectross-eenablingcrossectectectectectection.2)

JVM架構:深入研究Java虛擬機JVM架構:深入研究Java虛擬機May 14, 2025 am 12:12 AM

TheJVMisanabstractcomputingmachinecrucialforrunningJavaprogramsduetoitsplatform-independentarchitecture.Itincludes:1)ClassLoaderforloadingclasses,2)RuntimeDataAreafordatastorage,3)ExecutionEnginewithInterpreter,JITCompiler,andGarbageCollectorforbytec

JVM:JVM與操作系統有關嗎?JVM:JVM與操作系統有關嗎?May 14, 2025 am 12:11 AM

JVMhasacloserelationshipwiththeOSasittranslatesJavabytecodeintomachine-specificinstructions,managesmemory,andhandlesgarbagecollection.ThisrelationshipallowsJavatorunonvariousOSenvironments,butitalsopresentschallengeslikedifferentJVMbehaviorsandOS-spe

Java:寫一次,在任何地方跑步(WORA) - 深入了解平台獨立性Java:寫一次,在任何地方跑步(WORA) - 深入了解平台獨立性May 14, 2025 am 12:05 AM

Java實現“一次編寫,到處運行”通過編譯成字節碼並在Java虛擬機(JVM)上運行。 1)編寫Java代碼並編譯成字節碼。 2)字節碼在任何安裝了JVM的平台上運行。 3)使用Java原生接口(JNI)處理平台特定功能。儘管存在挑戰,如JVM一致性和平台特定庫的使用,但WORA大大提高了開發效率和部署靈活性。

Java平台獨立性:與不同的操作系統的兼容性Java平台獨立性:與不同的操作系統的兼容性May 13, 2025 am 12:11 AM

JavaachievesPlatFormIndependencethroughTheJavavIrtualMachine(JVM),允許Codetorunondifferentoperatingsystemsswithoutmodification.thejvmcompilesjavacodeintoplatform-interploplatform-interpectentbybyteentbytybyteentbybytecode,whatittheninternterninterpretsandectectececutesoneonthepecificos,atrafficteyos,Afferctinginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginging

什麼功能使Java仍然強大什麼功能使Java仍然強大May 13, 2025 am 12:05 AM

JavaispoperfulduetoitsplatFormitiondence,對象與偏見,RichstandardLibrary,PerformanceCapabilities和StrongsecurityFeatures.1)Platform-dimplighandependectionceallowsenceallowsenceallowsenceallowsencationSapplicationStornanyDevicesupportingJava.2)

頂級Java功能:開發人員的綜合指南頂級Java功能:開發人員的綜合指南May 13, 2025 am 12:04 AM

Java的頂級功能包括:1)面向對象編程,支持多態性,提升代碼的靈活性和可維護性;2)異常處理機制,通過try-catch-finally塊提高代碼的魯棒性;3)垃圾回收,簡化內存管理;4)泛型,增強類型安全性;5)ambda表達式和函數式編程,使代碼更簡潔和表達性強;6)豐富的標準庫,提供優化過的數據結構和算法。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

SublimeText3 英文版

SublimeText3 英文版

推薦:為Win版本,支援程式碼提示!