Spring OAuth 2.0 教程

三方登录认证

  • OAuth 2.0 是一个授权框架,Spring OAuth2 封装了三方认证功能

    1. 作为三方服务器,实现了【不感知用户密码前提下,实现三方认证】
    2. 作为授权服务器,实现了【三方认证请求处理】(TODO)
  • 四种OAuth模式

    1. 授权码模式【首选】:前端获得三方授权码 → 后端携带授权码请求三方登录 → 前后端用Redis通信
    2. 密码模式【老项目】:前端直接传密码 → 后端携带密码请求三方登录 → 前后端用Redis通信
    3. 服务器凭证模式【内部子系统】:本地登录,后端定时同步三方用户数据
    4. 简化模式【慎重使用】:全程前端实现

前端 后端 GitHub 1. GET /oauth2/authorization/github 2. 302重定向到GitHub登录页 3. 用户登录GitHub(输入密码) 认证失败 4. github回调后端接口 /login/oauth2/code/github?code=xxx 5. 用code + client_secret换token 6. 返回access_token 7. 用access_token获取用户信息 8. 返回用户信息 9. 返回JWT/session给前端 前端 后端 GitHub Github OAuth2 授权码流程图
前端 后端 Google 1. GET /oauth2/authorization/google 2. 302重定向到Google授权页面 3. 用户登录Google(输入密码) 认证失败 5. 回调到 /login/oauth2/code/google?code=xxx&state=yyy 6. 用code + client_secret换token(包含access_token和id_token) 7. 返回id_token(JWT封装,直接包含了用户信息) 8. 验证id_token签名 9. 返回id_token签名对应公钥 10. 根据公钥直接从id_token提取用户信息 11. 返回JWT/session给前端 前端 后端 Google Google OAuth2 授权码流程图

  • 技术选择

    1. 标准 OAuth 2.0 服务商(GitHub、Google、Facebook)推荐使用此框架
    2. 非标准 OAuth 服务商(微信、QQ)使用OAuth 2.0属于过度设计,建议手动实现
yaml 复制代码
# 标准 OAuth 2.0 服务商必须提供这些端点:

必需端点:
  1. 授权端点 (Authorization Endpoint):
     - 路径: 通常是 /oauth/authorize
     - 方法: GET
     - 标准参数:
       * response_type: "code" (授权码模式)
       * client_id: 客户端ID
       * redirect_uri: 回调地址
       * scope: 权限范围
       * state: CSRF防护

  2. 令牌端点 (Token Endpoint):
     - 路径: 通常是 /oauth/token
     - 方法: POST
     - 标准参数:
       * grant_type: "authorization_code" 或 "refresh_token"
       * code: 授权码
       * redirect_uri: 必须与授权时一致
       * client_id, client_secret: 客户端凭证

  3. 用户信息端点 (UserInfo Endpoint):
     - 路径: 通常是 /oauth/userinfo 或 /userinfo
     - 方法: GET
     - 需要: Authorization: Bearer <access_token>

  4. 发现端点 (Discovery Endpoint):
     - 路径: /.well-known/oauth-authorization-server
     - 返回: 所有端点和配置信息

  5. JWK 端点 (JWKS Endpoint):
     - 路径: /.well-known/jwks.json
     - 返回: JWT 签名公钥
yaml 复制代码
// GitHub (标准)
github: {
    authorizationRequest: {
        url: "https://github.com/login/oauth/authorize",
        params: {
            client_id: "YOUR_CLIENT_ID",      // ✅ 标准
            redirect_uri: "https://...",      // ✅ 标准
            scope: "user:email read:user",    // ✅ 标准
            state: "RANDOM_STRING",           // ✅ 标准
            response_type: "code"              // ✅ 标准
        }
    },
    tokenRequest: {
        url: "https://github.com/login/oauth/access_token",
        headers: {
            "Accept": "application/json",      // ✅ 标准
            "Content-Type": "application/x-www-form-urlencoded"
        },
        body: {
            client_id: "YOUR_CLIENT_ID",      // ✅ 标准
            client_secret: "YOUR_SECRET",     // ✅ 标准
            code: "AUTHORIZATION_CODE",       // ✅ 标准
            redirect_uri: "https://...",      // ✅ 标准
            grant_type: "authorization_code"  // ✅ 标准
        }
    }
}

// 微信 (非标准)
wechat: {
    authorizationRequest: {
        url: "https://open.weixin.qq.com/connect/oauth2/authorize",
        params: {
            appid: "YOUR_APPID",              // ❌ 非标准(appid非client_id)
            redirect_uri: "https://...",
            response_type: "code",
            scope: "snsapi_userinfo",
            state: "RANDOM_STRING",
            #wechat_redirect: ""              // ❌ 非标准(URL fragment)
        }
    },
    tokenRequest: {
        url: "https://api.weixin.qq.com/sns/oauth2/access_token",
        params: {                             // ❌ GET而非POST
            appid: "YOUR_APPID",              // ❌ 非标准
            secret: "YOUR_SECRET",            // ❌ 非标准
            code: "AUTHORIZATION_CODE",
            grant_type: "authorization_code"
        }
    }
}
  • 引入依赖
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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

配置OAuth本地客户端

  • 以Github登录为例,需要先去授权服务器注册本服务白名单

    1. 访问 GitHub → Settings → Developer settings → OAuth Apps
    2. 点击 "New OAuth App"
    3. 填写你的应用相关信息(应用名、IP、Authorization callback URL):生成 Client ID 和 Client Secret
java 复制代码
@Configuration
public class OAuth2ClientConfig {

    @Autowired
    private Github github;
    
    @Autowired
    private Google google;

    /**
     * 创建客户端注册仓库
     */
    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        List<ClientRegistration> registrations = new ArrayList<>();
        // 添加 GitHub 配置
        if (github.isEnabled()) {
            registrations.add(githubClientRegistration());
        }
        // 添加其他平台配置...
        if (google.isEnabled()) {
            registrations.add(googleClientRegistration());
        }
        return new InMemoryClientRegistrationRepository(registrations);
    }

    /**
     * GitHub 客户端注册
     */
    private ClientRegistration githubClientRegistration() {
        OAuth2Properties.Github github = properties.getGithub();
        return ClientRegistration.withRegistrationId("github")
                .clientId(github.getClientId())
                .clientSecret(github.getClientSecret())
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationUri("https://github.com/login/oauth/authorize") //  GitHub的授权页面URL(用户登录页)
                .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") // GitHub授权成功后,会重定向到这个地址
                .scope("user:email", "read:user") // 申请的权限范围
                .tokenUri("https://github.com/login/oauth/access_token") // 授权码换access_token URL
                .userInfoUri("https://api.github.com/user") // access_token查用户信息
                .userNameAttributeName("id") // 用户信息中作为用户名的字段(GitHub返回的JSON中的"id"字段)
                .clientName("GitHub")
                .build();
    }
    
    /**
     * Google 客户端注册
     */
    private ClientRegistration googleClientRegistration() {
        OAuth2Properties.Google google = properties.getGoogle();

        return ClientRegistration.withRegistrationId("google")
                // 基本配置
                .clientId(google.getClientId())  // 从Google Cloud Console获取
                .clientSecret(google.getClientSecret())  // 从Google Cloud Console获取
                // 客户端认证方式(Google通常用BASIC)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 授权类型(标准授权码模式)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                // 重定向URI(Google授权成功后回调的地址)
                .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
                // 申请的权限范围(Google使用OpenID Connect)
                .scope("openid", "profile", "email")  // OpenID Connect标准scope
                // Google OAuth2 端点配置
                .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")  // 授权端点
                .tokenUri("https://oauth2.googleapis.com/token")                  // 令牌端点
                .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")         // JWK端点(用于验证JWT)
                // OpenID Connect 配置(可选)
                .issuerUri("https://accounts.google.com")                         // 颁发者URI
                // 用户信息端点配置
                .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")     // 用户信息端点
                // 用户信息映射
                .userNameAttributeName("sub")  // OpenID Connect标准sub(Subject)字段
                // 客户端名称(显示给用户的)
                .clientName("Google")
                .build();
    }

    
    @Component
    @ConfigurationProperties(prefix = "oauth2") // Client ID 和 Client Secret 需要去三方授权服务器获取
    @Data
    public static class Github {
        private boolean enabled = true;
        private String clientId = "";
        private String clientSecret = "";
        private String[] scope = {"user:email", "read:user"};
    }
    
    @Component
    @ConfigurationProperties(prefix = "oauth2") // Client ID 和 Client Secret 需要去三方授权服务器获取
    @Data
    public static class Google {
        private boolean enabled = true;
        private String clientId = "";
        private String clientSecret = "";
        private String[] scope = {"user:email", "read:user"};
    }
}

整合Security客户端

  • OAuth通常和Security配合使用

    1. 对于非标准OAuth厂商/本地登录,由于身份认证逻辑不通用,需要在业务层/过滤器/Provider实现认证逻辑
    2. 对于标准OAuth厂商,Security封装了登录所有流程,无需再编写三方登录接口
    3. 用户原始的OAuth2登录请求(即.authorizationEndpoint())不会被Security拦截,但后续的处理仍然受Security控制
  • OAuth认证处理器

    1. 三方认证失败:由.failureHandler(oAuth2AuthenticationFailureHandler())处理
    2. 认证成功后置逻辑:由.successHandler(oAuth2AuthenticationSuccessHandler())处理,例如存入Redis实现分布式会话
  • OAuth不影响[【本地登录 | 鉴权 | 服务资源保护】](Spring Security)功能

java 复制代码
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true) // 启用@PreAuthorize
public class SecurityConfig {

    @Autowired
    private JwtRefreshFilter jwtRefreshFilter; // 自定义过滤器

    @Autowired
    private AuthenticationExceptionHandler authenticationExceptionHandler; // 认证异常处理器

    @Autowired
    private LogoutHandler logoutHandler; // 注销处理器

    @Autowired
    private AccessDeniedExceptionHandler accessDeniedExceptionHandler; // 鉴权异常处理器

    @Autowired 
    private UserDetailsService userDetailsService; // 本地用户信息加载实现类
    
    @Autowired
    private CustomOAuth2UserService customOAuth2UserService; // OAuth加载用户信息实现类
    
    @Autowired
    private OAuth2AuthenticationFailureHandler accessDeniedExceptionHandler; // OAuth登录成功后置处理器
        
    @Autowired    
    private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; // OAuth登录失败处理器    
    
    @Autowired
    private RedisClient redisClient; // redis缓存

    /**
     * 密码编码器
     * Spring Security 必须通过 PasswordEncoder 加密密码,若不配置该 Bean,启动会直接报错
     * 采用 BCrypt 哈希算法,不可逆,只能密文比较
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        // 关闭csrf防护,用于前后端分离场景
        http.csrf(AbstractHttpConfigurer::disable); 
        // 关闭Session,用于分布式场景
        http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // 注册web接口
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.POST, "/user/login").permitAll()
                .requestMatchers("/test").permitAll()
                .requestMatchers(HttpMethod.POST, "/user/logout").permitAll()
                .anyRequest().authenticated()
        );
        
        // 注册OAuth2登录接口
        http.oauth2Login(oauth2 -> oauth2
            /**
            * 1. 接受前端发送原始登录请求,重定向到三方登录页
            *   请求为/oauth2/authorization/github 
            *	→ 得registrationId = "github"
            *	→ 从本地OAuth配置的 ClientRegistrationRepository 查找对应的平台
            *	→ 构建重定向URL到github平台登录页上
            */
            .authorizationEndpoint(authorization -> authorization
                .baseUri("/oauth2/authorization/{registrationId}")  // 默认值
            )
            /**
            * 2. 接受携带授权码的认证请求:/login/oauth2/code/*,后端开始认证
            *   重定向到三方登录页,授权成功后
            *	→ 自动重定向到/login/oauth2/code/github(三方会自动添加?code=xxx&state=yyy)
            *	→ 得registrationId = "github"
            *	→ 从本地OAuth配置的 ClientRegistrationRepository 查找对应的平台
            */
            .redirectionEndpoint(redirection -> redirection
                .baseUri("/login/oauth2/code/*")  // 默认值,例如/login/oauth2/code/github?code=xxx  
            )
            /**
            * 3. 后端查询用户信息,成功即认证完毕
            *   上一步已经匹配到对应平台
            *	→ 携带授权码访问对应平台的Token接口
            *	→ 收到返回的succeed token
            *	→ 携带succeed token再查平台的用户查询接口
            *	→ 实现OAuth2UserService:将查询的用户信息以OAuth2User接受
            */
            .userInfoEndpoint(userInfo -> userInfo
                .userService(customOAuth2UserService)  
            )
            // 4. 成功处理器:认证成功后的处理
            .successHandler(oAuth2AuthenticationSuccessHandler)
            // 5. 失败处理器:认证失败后的处理
            .failureHandler(oAuth2AuthenticationFailureHandler)
        );

        // 注册自定义过滤器
        http.addFilterBefore(jwtRefreshFilter, UsernamePasswordAuthenticationFilter.class); 

        // 注册自定义处理器
        http.logout(logout -> logout
                .logoutUrl("/user/logout") // 自定义注销路径(替代默认 /logout)
                .logoutSuccessHandler(logoutHandler) // 你的自定义注销成功处理器
        );

        // 注册异常处理器
        http.exceptionHandling(ex -> ex
                .accessDeniedHandler(accessDeniedExceptionHandler) // 权限不足
                .authenticationEntryPoint(authenticationExceptionHandler) // 未认证
        );
        return http.build();
    }

    /**
     * SpringSecurity 认证管理器
     */
    @Bean
    public AuthenticationManager configureAuthenticationManager(PasswordEncoder passwordEncoder) {
        // 1. 创建 DaoAuthenticationProvider(用于用户名密码登录)
        DaoAuthenticationProvider daoAuthProvider = new DaoAuthenticationProvider();
        daoAuthProvider.setUserDetailsService(userDetailsService);
        daoAuthProvider.setPasswordEncoder(passwordEncoder);
        // 2.创建 SmsCodeAuthenticationProvider (用户验证码登录)
        SmsCodeAuthenticationProvider smsAuthProvider = new SmsCodeAuthenticationProvider();
        smsAuthProvider.setUserDetailsService(userDetailsService);
        smsAuthProvider.setRedisClient(redisClient);
        // 2. 返回包含所有 Provider 的 AuthenticationManager
        return new ProviderManager(Arrays.asList(
                daoAuthProvider,   // 用户名密码登录
                smsAuthProvider    // 短信登录
        ));
    }
}

OAuth2User封装用户信息

  • UserDetails类似,Spring OAuth提供了OAuth2User接口,用于三方登录充当用户信息实体类
java 复制代码
/**
 * `OAuth2UserService`只支持`OAuth2User`接受用户信息
 */
public class CustomOAuth2User implements OAuth2User {
    
    private final String id;
    private final String name;
    private final String email;
    private final String avatar;
    private final Map<String, Object> attributes;
    private final Collection<? extends GrantedAuthority> authorities;
    
    public CustomOAuth2User(String id, String name, String email, String avatar, 
                           Map<String, Object> attributes, Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.avatar = avatar;
        this.attributes = attributes;
        this.authorities = authorities;
    }
    
    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    
    @Override
    public String getName() {
        return this.name;
    }
    
    public String getId() {
        return id;
    }
    
    public String getEmail() {
        return email;
    }
    
    public String getAvatar() {
        return avatar;
    }
    
    public String getSource() {
        return (String) attributes.get("source");
    }
}

OAuth2UserService加载用户信息

  • 在前端发送携带授权码的认证请求时调用
java 复制代码
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // 1. 获取OAuth2用户,这里会OAuth封装了【授权码换token+token查用户】的逻辑,见父类CustomOAuth2UserService
        OAuth2User oauth2User = super.loadUser(userRequest);
        
        // 2. 构建统一的用户属性
        Map<String, Object> attributes = new HashMap<>(oauth2User.getAttributes());
        
        // 3. 根据不同的第三方平台,提取用户信息
        String id = attributes.get("id").toString();
        String name = (String) attributes.get("name");
        String email = (String) attributes.get("email");
        String avatar = (String) attributes.get("avatar_url");
        String login = (String) attributes.get("login"); 
        // 如果name为空,使用login
        if (name == null || name.trim().isEmpty()) {
            name = login;
        }
        
        // 5. 返回自定义的OAuth2用户
        return new CustomOAuth2User(id, name, email, avatar, attributes, oauth2User.getAuthorities());
    }
}
  • 如果支持多端三方登录,可以进一步拓展
java 复制代码
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // 1. 获取默认的OAuth2用户
        OAuth2User oauth2User = super.loadUser(userRequest);
        
        // 2. 获取注册ID(github, google, wechat等),从已经配置的ClientRegistrationRepository中拿
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        
        // 3. 构建统一的用户属性
        Map<String, Object> attributes = new HashMap<>(oauth2User.getAttributes());
        
        // 4. 根据不同的第三方平台,提取用户信息
        OAuth2UserInfo userInfo = extractUserInfo(registrationId, attributes);
        
        // 5. 返回自定义的OAuth2用户
        return new CustomOAuth2User(
            userInfo.getId(),
            userInfo.getName(),
            userInfo.getEmail(),
            userInfo.getAvatar(),
            attributes,
            oauth2User.getAuthorities()
        );
    }
    
    private OAuth2UserInfo extractUserInfo(String registrationId, Map<String, Object> attributes) {
        switch (registrationId.toLowerCase()) {
            case "github":
                return extractGithubUserInfo(attributes);
            case "wechat":
                return extractWechatUserInfo(attributes);
            default:
                throw new OAuth2AuthenticationException("不支持的登录方式: " + registrationId);
        }
    }
    
    private OAuth2UserInfo extractGithubUserInfo(Map<String, Object> attributes) {
        String id = attributes.get("id").toString();
        String name = (String) attributes.get("name");
        String email = (String) attributes.get("email");
        String avatar = (String) attributes.get("avatar_url");
        String login = (String) attributes.get("login");
        
        // 如果name为空,使用login
        if (name == null || name.trim().isEmpty()) {
            name = login;
        }
        
        return OAuth2UserInfo.builder()
            .id(id)
            .name(name)
            .email(email)
            .avatar(avatar)
            .source("github")
            .build();
    }
    
    private OAuth2UserInfo extractWechatUserInfo(Map<String, Object> attributes) {
        String id = attributes.get("openid").toString();
        String name = (String) attributes.get("nickname");
        String avatar = (String) attributes.get("headimgurl");
        
        return OAuth2UserInfo.builder()
            .id(id)
            .name(name)
            .avatar(avatar)
            .source("wechat")
            .build();
    }
    
    /**
     * 用户信息DTO
     */
    @Data
    @Builder
    public static class OAuth2UserInfo {
        private String id;      // 用户在第三方平台的唯一ID
        private String name;    // 用户名
        private String email;   // 邮箱
        private String avatar;  // 头像
        private String source;  // 来源平台
    }
}

配置认证成功处理器

  • 主流做法是本地JWT封装,存入Redis,可以自主控制有效期和刷新信息逻辑
java 复制代码
@Component
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtService jwtService;
    
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                       HttpServletResponse response, 
                                       Authentication authentication) throws IOException, ServletException {
        
        if (authentication instanceof OAuth2AuthenticationToken) {
            OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
            OAuth2User oauth2User = oauthToken.getPrincipal();
            
            // 从OAuth2用户信息中提取数据
            Map<String, Object> attributes = oauth2User.getAttributes();
            String registrationId = oauthToken.getAuthorizedClientRegistrationId();
            
            // 提取用户信息
            String userId = extractUserId(registrationId, attributes);
            String username = extractUsername(registrationId, attributes);
            String email = extractEmail(registrationId, attributes);
            String avatar = extractAvatar(registrationId, attributes);
            
            // 生成JWT token
            String token = jwtService.generateToken(userId, username, email, avatar, registrationId);
            
            // 构建返回数据
            Map<String, Object> data = new HashMap<>();
            data.put("token", token);
            data.put("user", Map.of(
                "id", userId,
                "username", username,
                "email", email,
                "avatar", avatar,
                "source", registrationId
            ));
            
            // 返回JSON响应
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpServletResponse.SC_OK);
            
            ApiResponse<Map<String, Object>> apiResponse = ApiResponse.success(data);
            response.getWriter().write(objectMapper.writeValueAsString(apiResponse));
        }
    }
    
    private String extractUserId(String registrationId, Map<String, Object> attributes) {
        switch (registrationId) {
            case "github":
                return attributes.get("id").toString();
            case "google":
                return attributes.get("sub").toString();
            case "wechat":
                return attributes.get("openid").toString();
            default:
                return attributes.get("id") != null ? attributes.get("id").toString() : "unknown";
        }
    }
    
    private String extractUsername(String registrationId, Map<String, Object> attributes) {
        switch (registrationId) {
            case "github":
                return (String) attributes.getOrDefault("login", attributes.get("name"));
            case "google":
                return (String) attributes.get("name");
            case "wechat":
                return (String) attributes.get("nickname");
            default:
                return (String) attributes.getOrDefault("username", "unknown");
        }
    }
    
    private String extractEmail(String registrationId, Map<String, Object> attributes) {
        switch (registrationId) {
            case "github":
                return (String) attributes.get("email");
            case "google":
                return (String) attributes.get("email");
            default:
                return (String) attributes.getOrDefault("email", "");
        }
    }
    
    private String extractAvatar(String registrationId, Map<String, Object> attributes) {
        switch (registrationId) {
            case "github":
                return (String) attributes.get("avatar_url");
            case "google":
                return (String) attributes.get("picture");
            case "wechat":
                return (String) attributes.get("headimgurl");
            default:
                return (String) attributes.getOrDefault("avatar", "");
        }
    }
}

配置认证失败处理器

  • OAuth认证失败后置处理
java 复制代码
@Component
public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
                                       HttpServletResponse response, 
                                       AuthenticationException exception) throws IOException, ServletException {
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        
        ApiResponse<String> apiResponse = ApiResponse.error(
            HttpServletResponse.SC_UNAUTHORIZED,
            "第三方登录失败: " + exception.getMessage()
        );
        
        response.getWriter().write(objectMapper.writeValueAsString(apiResponse));
    }
}
相关推荐
Han.miracle2 小时前
Java 8 Lambda 表达式与方法引用的语法优化及实战应用研究
java·开发语言·jvm
库库林_沙琪马2 小时前
13、SpringBoot启动过程
java·spring boot·后端
风象南2 小时前
Spring Boot 实现单账号登录控制
后端
chenyuhao20242 小时前
Linux系统编程:进程控制
linux·运维·服务器·开发语言·c++·后端
代龙涛2 小时前
wordpress目录介绍
开发语言·后端·php
小年糕是糕手2 小时前
【C++】模板初阶
java·开发语言·javascript·数据结构·c++·算法·leetcode
Victor3562 小时前
Netty(4)Netty的Channel是什么?它有哪些类型?
后端
路边草随风2 小时前
java发送飞书消息卡片
java·飞书
是梦终空3 小时前
JAVA毕业设计253—基于Java+Springboot+vue3+协同过滤推荐算法的传统服饰文化平台(源代码+数据库+任务书+12000字论文)
java·spring boot·vue·毕业设计·课程设计·协同过滤推荐算法·传统服饰文化平台