AI 大模型统一集成|微服务 + 认证中心:如何保障大模型 API 的安全调用!

发布于:2025-03-20 ⋅ 阅读:(16) ⋅ 点赞:(0)

🌟 在这系列文章中,我们将一起探索如何搭建一个支持大模型集成项目 NexLM 的开发过程,从 架构设计代码实战,逐步搭建一个支持 多种大模型(GPT-4、DeepSeek 等)一站式大模型集成与管理平台,并集成 认证中心、微服务、流式对话 等核心功能。

系列目录规划:

  1. NexLM:从零开始打造你的专属大模型集成平台
  2. Spring Boot + OpenAI/DeepSeek:如何封装多个大模型 API 调用
  3. 支持流式对话 SSE & WebSocket:让 AI 互动更丝滑
  4. 微服务 + 认证中心:如何保障大模型 API 的安全调用
  5. 缓存与性能优化:提高 LLM API 响应速度
  6. NexLM 开源部署指南(Docker)

第五篇:微服务 + 认证中心:如何保障大模型 API 的安全调用!

在上一章中,我们使用 WebSocket 实现流式对话,使 AI 交互更丝滑。然而,在微服务架构下,如果 API 缺乏身份认证,就会面临滥用、数据泄露、权限绕过等风险。

今天,我们来继续完善微服务 + 认证中心架构方案,确保大模型 API 只能被认证登录已授权用户访问!

在这里插入图片描述

如果未登录直接访问大模型页面会被统一拦截跳转至登录页面,并且 WebSocket 的连接也需要身份合法性校验。

📌 1. 为什么微服务架构下需要认证中心?

微服务架构下,我们通常不会直接将用户认证逻辑嵌入到每个微服务中,而是通过认证中心进行统一管理。下面我们将详细讲解如何基于 Spring Security 设计和实现一个高效、安全的认证中心,实现大模型 API 的安全调用。

认证中心 负责统一管理用户身份验证,并提供 Token 颁发与鉴权能力,确保各个服务能够安全地进行访问控制。常见的 API 认证方式包括:

  • Session 认证:适用于单体应用,通过服务器会话存储用户状态。
  • JWT 认证:适用于分布式微服务架构,使用无状态 Token 进行身份验证。
  • OAuth 2.0 / OpenID Connect:适用于跨系统单点登录(SSO)和第三方授权认证。

我们今天先来了解一下如何使用 Spring Security 实现最基本的 Session 认证。后面我们也会继续讲解如何采用 JWT + Spring Security 来实现无状态认证,避免 Session 共享问题,提高系统扩展性。

📌 2. Spring Security 认证中心实现

📍 2.1 引入依赖

pom.xml 添加 Spring Security 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

📍 2.2 配置 Spring Security

创建 SecurityConfig 进行 Spring Security 认证配置

	@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 禁用httpBasic
                .httpBasic(AbstractHttpConfigurer::disable)
                //.csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/login").permitAll() // 允许所有用户访问
                .anyRequest().authenticated()
                .and()
                // 添加自定义登录处理
                .formLogin(formLogin -> formLogin
                        .usernameParameter("username")
                        .passwordParameter("password")
                        .loginProcessingUrl("/login")
                        .permitAll()
                        .successHandler(customAuthSuccessHandler)
                        .failureHandler(customAuthFailureHandler)
                )
                // 添加注销配置
                .logout(logout -> logout
                        .logoutUrl(AuthConstant.LOGOUT_URL) // 设置退出URL
                        .logoutSuccessHandler(customLogoutSuccessHandler) // 自定义退出成功逻辑
                        .addLogoutHandler(customLogoutHandler) // 处理自定义清理逻辑(如清除 Redis Token)
                        .invalidateHttpSession(true) // 使 Session 失效
                        .clearAuthentication(true) // 清空认证信息
                        .permitAll()
                );

        return http.build();
    }

禁用 HTTP Basic 认证(即基于 Authorization: Basic <Base64(username:password)> 方式的身份验证)。
开放 /auth/login 登录接口,其余 API 需要认证后访问。
✅ 自定义登录配置 (formLogin)
✅ 自定义注销配置 (logout)


📍 2.3 认证管理器添加自定义登录配置(自定义校验账号密码)

在 SecurityConfig 中注入 AuthenticationManager。并且声明自定义的 认证管理器提供者 CustomAuthenticationProvider。

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        CustomAuthenticationProvider customAuthenticationProvider = customAuthenticationProvider(passwordEncoder());
        return new ProviderManager(customAuthenticationProvider);
    }

CustomAuthenticationProvider 的具体实现如下,继承 AbstractUserDetailsAuthenticationProvider 重写必要的方法。

public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public CustomAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            throw new AuthenticationException("Invalid credentials") {
            };
        }
    }

    @Override
    protected UserDetails retrieveUser(String username,
                                       UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        if (userDetails == null) {
            throw new AuthenticationException("User not found") {
            };
        }

        return userDetails;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // Custom logic for authentication can go here
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        // 加载用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails == null) {
            throw new BadCredentialsException("用户名或密码错误");
        }

        // 验证密码
        if (password.equals(userDetails.getPassword())) {
            // 密码匹配,返回认证成功的 Token
            return new UsernamePasswordAuthenticationToken(
                    userDetails,
                    password,
                    userDetails.getAuthorities()
            );
        } else {
            throw new BadCredentialsException("用户名或密码错误");
        }
    }
}

认证管理器,自定义校验账号密码
加载用户信息,使用自定义逻辑进行密码校验,比对数据库中的账号密码


📍 2.4 用户密码准确性校验

模拟用户数据,可模拟校验数据库用户密码。实际项目中应从 数据库或 Redis 获取用户信息。

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (!"admin".equals(username)) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return User.builder()
            .username("admin")
            .password(new BCryptPasswordEncoder().encode("123456")) // 存储加密密码
            .roles("USER")
            .build();
    }
}

远程接口调用用户中心,如果是微服务则可以通过接口调用用户中心服务获取用户的信息进行对比校验。

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserApiClient userApiClient;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        BaseResponse<UserVO> userResponse = userApiClient.getUserByName(username);
        if (userResponse != null && userResponse.getData() != null) {
            UserVO user = userResponse.getData();
            return new org.springframework.security.core.userdetails.User(
                    user.getName(),
                    user.getPassword(),
                    Collections.emptyList() // TODO 可根据需求添加权限 URL权限
            );
        }
        throw new UsernameNotFoundException("用户名或密码错误");  // 这里抛出异常
    }

    public void checkPassword(Authentication authentication) {
        try {
            String credentials = (String) authentication.getCredentials();
            UserLoginRequest userLoginRequest = new UserLoginRequest();
            userLoginRequest.setUserAccount((String) authentication.getPrincipal());
            userLoginRequest.setUserPassword(credentials);
            userApiClient.checkLoginPwd(userLoginRequest);
        } catch (FeignException e) {
            throw new BadCredentialsException("用户名或密码错误");
        }
    }
}

📍 2.5 认证接口

在 SecurityConfig中 配置一个监听登录表单提交请求的接口 /login ,以及相关参数,定义好登录成功的逻辑(跳转首页)和登录失败的逻辑(提示反馈)。

                .formLogin(formLogin -> formLogin
                        .usernameParameter(AuthConstant.USERNAME) // 设置登录表单中的用户名字段的参数名称
                        .passwordParameter(AuthConstant.PASSWORD) // 设置登录表单中的密码字段的参数名称
                        .loginProcessingUrl(AuthConstant.LOGIN_URL) // 设置登录请求的 URL 地址(表单提交的 URL)
                        .permitAll() // 允许所有人访问登录页面,不需要认证
                        .successHandler(customAuthSuccessHandler) // 设置登录成功后的处理逻辑,可以定制成功后的跳转等
                        .failureHandler(customAuthFailureHandler) // 设置登录失败后的处理逻辑,可以定制失败后的提示等
                )

CustomAuthSuccessHandler 中进行定义登录成功之后的执行流程(跳转首页)。

public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 方式1: 重定向到首页
        // 获取当前应用的 contextPath (前缀例如 "/web")
        String contextPath = request.getContextPath();
        String redirectURL = contextPath + index;
        logger.info("登录成功,跳转至首页:{}", redirectURL);
    }
}

CustomAuthFailureHandler 中进行定义登录失败的执行流程(带上提示信息跳转登录页面)。

/**
 * 登录失败处理器
 * AuthenticationFailureHandler 只会处理 AuthenticationException 及其子类的异常,比如:
 * BadCredentialsException
 * DisabledException
 * LockedException
 * AccountExpiredException
 */
@Component
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {

    private final Logger logger = LoggerFactory.getLogger(CustomAuthFailureHandler.class);

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.error("登录失败...{}", exception.getMessage());
        // 获取当前应用的 contextPath (动态前缀,如 "/web")
        String contextPath = request.getContextPath();
        // 失败后重定向到登录页,并附带错误信息
        response.sendRedirect(contextPath + "/auth/login?error=password error");
    }
}

📍 2.6 会话配置

server:
  servlet:
    session:
      tracking-modes: COOKIE
      timeout: 60m
      cookie:
        httpOnly: false
        path: /
        name: JSESSIONID

这段配置是进行会话管理配置,定义了会话的存储方式、超时设置和 cookie 配置。Spring Security 会在此基础上进行用户认证、权限管理等高级安全功能。

  • tracking-modes: COOKIE:会话 ID 通过浏览器的 cookie 跟踪。
  • timeout: 60m:会话超时时间为 60 分钟,超过此时间会话失效。
  • cookie.httpOnly: false:会话 cookie 允许 JavaScript 访问(默认 true 时不可访问)。
  • cookie.path: 会话 cookie 在整个应用路径 “/” 范围内有效。
  • cookie.name: JSESSIONID:会话 cookie 的名称为 JSESSIONID。

📍 2.7 前端页面

<form class="form-signin" action="${request.contextPath}/login" method="post">
        <input type="hidden" name="client_id" value="nex">
        <input type="hidden" name="grant_type" value="password">
        <div class="space-y-6">
            <div class="">
                <input class="w-full text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 dark:text-gray-300 transition-colors"
                       type="text" placeholder="账号" name="username" required>
            </div>

            <div class="relative">
                <input placeholder="密码" type="password" name="password" required
                       class="w-full text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 dark:text-gray-300 transition-colors">
            </div>

            <#if error??>
                <div class="relative text-center">
                    <span class="text-red-600 dark:text-red-400 text-sm font-medium">${error}</span>
                </div>
            </#if>

            <button type="submit"
                    class="w-full flex justify-center bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 dark:bg-blue-700 dark:hover:bg-blue-600 text-white p-3 rounded-lg font-semibold cursor-pointer transition-all duration-300 transform hover:scale-105">
                立即登录
            </button>
        </div>

    </form>

✅ 至此,已完成一个基础的会话登录配置。用户通过表单页面提交账号密码进行验证,认证成功后,系统会自动生成 Session,并将其 Session ID 以 JSESSIONID 作为 Cookie 的 Key 存储到浏览器。后续请求如果携带该 Cookie,服务器会根据 JSESSIONID 关联的会话信息验证用户身份,从而判定用户已通过身份认证。

在这里插入图片描述
会话登录流程简单描述如下:

  [用户输入账号密码]  
         |  
         ▼  
  [Spring Security 校验]  
         |  
         ▼  
  [生成 Token(JSESSIONID)]  
         |  
         ▼  
  [返回 Token(JSESSIONID) 给前端]  
         |  
         ▼  
  [前端存储 Token(JSESSIONID),在后续请求中携带并校验]  

🎯 3. 总结

本期内容中,我们完成了一个基础的会话登录配置,用户通过表单提交账号密码进行验证,认证成功后,服务器会自动生成 Session,并将 JSESSIONID 存储到 Cookie 中。后续请求携带该 Cookie,服务器即可识别用户身份,实现会话维持。

会话(Session)认证方式在实际应用中较为常见,适用于传统 Web 应用,但也存在一些局限性,例如:

  • 依赖服务器存储:每个用户的会话信息都存储在服务器端,用户增多时,服务器的内存占用也会随之增加。
  • 跨域受限:由于浏览器的同源策略,Session 认证在跨域环境下可能会遇到 Cookie 共享等问题。
  • 水平扩展复杂:在分布式架构中,不同服务器节点可能无法共享会话数据,需要借助 Redis 等方案存储 Session,增加了系统复杂度。

💡 下一篇,我们将基于 JWT 替换 Token,并实现微服务间身份认证!它是一种无状态的认证方式,相较于 Session 认证,它具有以下特点:

✅ 无状态,支持分布式:无需在服务器端存储用户状态,适合微服务架构。
✅ 跨域友好:Token 可通过 HTTP Header 传递,适用于 API 认证。
✅ 可携带更多信息:JWT 允许在 Token 内部存储用户信息,减少额外查询需求。

当然,JWT 也有一些新的挑战,例如:
Token 无法撤销:一旦签发 Token,就无法强制使其失效,除非设置短有效期或维护黑名单。
Token 体积较大:相比于 Session ID,JWT 需要存储更多信息,可能影响网络传输效率。


📢 你对这个项目感兴趣吗?欢迎 Star & 关注! 📌 GitHub 项目地址 🌟 你的支持是我持续创作的动力,欢迎点赞、收藏、分享!