You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring OAuth2自定义redirect_uri处理authorization_code换取access_token咨询

Spring OAuth2授权码模式对接VK的自定义回调处理

Spring授权码模式默认流程

  • 向资源服务器发起授权请求,获取authorization_code
  • 授权服务器将code重定向至配置的redirect_uri
  • Spring OAuth2自动捕获该code,向资源服务器请求换取access_token,并将token存入会话

问题场景

对接VK资源服务器时,Spring默认处理逻辑异常,需自定义实现redirect_uri的回调请求处理。以下是当前配置、现有代码及具体实现方案。


配置文件

registration:
  vk:
    client-id: xxx
    client-secret: xxx
    redirect_uri: http://localhost:8080/login/oauth2/callback/vk
    scope: notify,friends,photos,wall,ads,groups,stats,email
    authorization-grant-type: authorization_code

provider:
  vk:
    authorization-uri: https://oauth.vk.com/authorize
    token-uri: https://oauth.vk.com/access_token
    user-info-uri: https://api.vk.com/method/users.get?fields=screen_name,email
    user-name-attribute: response.screen_name

现有安全配置代码

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class OAuth2LoginSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/public").permitAll()
                        .requestMatchers("/login/oauth2/callback/**").permitAll()
                        .anyRequest().authenticated()
                );
        return http.build();
    }
}

已实现的回调接口(可获取authorization_code)

@RequestMapping(value = "/login/oauth2/callback/vk")
public @ResponseBody void generateReport(
        @RequestParam(value = "code", required = false) String code,
        @RequestParam(value = "state", required = false) String state,
        HttpServletRequest request,
        HttpServletResponse response) {

    System.out.println();
}

自定义回调处理实现方案

1. 调整安全配置

禁用Spring默认的OAuth2登录处理,避免和自定义回调逻辑冲突:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class OAuth2LoginSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/public", "/login/oauth2/callback/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2 -> oauth2.disable());
        return http.build();
    }
}

2. 实现完整的回调处理逻辑

创建控制器处理VK的回调请求,完成code换token、token存储等操作:

@Controller
public class VkOAuth2CallbackController {

    private final ClientRegistrationRepository clientRegistrationRepository;
    private final OAuth2AuthorizedClientService authorizedClientService;
    private final RestTemplate restTemplate;

    public VkOAuth2CallbackController(ClientRegistrationRepository clientRegistrationRepository,
                                      OAuth2AuthorizedClientService authorizedClientService) {
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.authorizedClientService = authorizedClientService;
        this.restTemplate = new RestTemplate();
    }

    @RequestMapping(value = "/login/oauth2/callback/vk")
    public String handleVkCallback(@RequestParam("code") String code,
                                   @RequestParam("state") String state,
                                   HttpServletRequest request,
                                   Authentication authentication) {
        // 获取VK客户端注册信息
        ClientRegistration vkRegistration = clientRegistrationRepository.findByRegistrationId("vk");
        if (vkRegistration == null) {
            throw new IllegalArgumentException("未找到VK客户端配置");
        }

        // 构造token请求参数
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("client_id", vkRegistration.getClientId());
        params.add("client_secret", vkRegistration.getClientSecret());
        params.add("code", code);
        params.add("redirect_uri", vkRegistration.getRedirectUri());
        params.add("grant_type", "authorization_code");

        // 发送请求获取access_token
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
        ResponseEntity<VkTokenResponse> tokenResponse = restTemplate.postForEntity(
                vkRegistration.getProviderDetails().getTokenUri(),
                requestEntity,
                VkTokenResponse.class
        );

        if (tokenResponse.getStatusCode().is2xxSuccessful() && tokenResponse.getBody() != null) {
            VkTokenResponse tokenBody = tokenResponse.getBody();
            // 封装OAuth2AccessToken
            OAuth2AccessToken accessToken = new OAuth2AccessToken(
                    OAuth2AccessToken.TokenType.BEARER,
                    tokenBody.getAccessToken(),
                    null,
                    Instant.now().plusSeconds(tokenBody.getExpiresIn())
            );

            // 可选:调用VK用户信息接口获取用户详情
            // String userInfoUrl = vkRegistration.getProviderDetails().getUserInfoEndpoint().getUri() + "&access_token=" + tokenBody.getAccessToken();
            // ResponseEntity<VkUserInfoResponse> userInfoResponse = restTemplate.getForEntity(userInfoUrl, VkUserInfoResponse.class);

            // 将token存入授权客户端服务,供后续业务使用
            OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
                    vkRegistration,
                    authentication != null ? authentication.getName() : tokenBody.getUserId(),
                    accessToken
            );
            authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);

            // 重定向至登录成功页面
            return "redirect:/home";
        } else {
            // 处理token获取失败场景
            return "redirect:/login?error=vk_token_fail";
        }
    }

    // VK token响应实体类
    public static class VkTokenResponse {
        @JsonProperty("access_token")
        private String accessToken;
        @JsonProperty("expires_in")
        private Integer expiresIn;
        @JsonProperty("user_id")
        private String userId;
        private String email;

        // Getter & Setter
        public String getAccessToken() { return accessToken; }
        public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
        public Integer getExpiresIn() { return expiresIn; }
        public void setExpiresIn(Integer expiresIn) { this.expiresIn = expiresIn; }
        public String getUserId() { return userId; }
        public void setUserId(String userId) { this.userId = userId; }
        public String getEmail() { return email; }
        public void setEmail(String email) { this.email = email; }
    }

    // 可选:VK用户信息响应实体类
    public static class VkUserInfoResponse {
        private List<VkUser> response;

        public List<VkUser> getResponse() { return response; }
        public void setResponse(List<VkUser> response) { this.response = response; }

        public static class VkUser {
            private Integer id;
            @JsonProperty("screen_name")
            private String screenName;
            private String email;

            // Getter & Setter
            public Integer getId() { return id; }
            public void setId(Integer id) { this.id = id; }
            public String getScreenName() { return screenName; }
            public void setScreenName(String screenName) { this.screenName = screenName; }
            public String getEmail() { return email; }
            public void setEmail(String email) { this.email = email; }
        }
    }
}

注意事项

  • 确保VK开发者后台配置的redirect_uri与代码、配置文件中的完全一致
  • VK的token响应格式与标准OAuth2略有差异,需通过@JsonProperty映射字段
  • 若需完成用户认证,可在获取用户信息后构造Authentication对象并存入SecurityContext
  • 需处理网络异常、code无效、token过期等异常场景

内容的提问来源于stack exchange,提问作者Roman Anokhin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 11:57:08