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

如何使用Spring Authorization Server认证第一方移动应用?

第一方移动应用基于Spring Authorization Server的认证方案

核心结论

Spring Authorization Server严格遵循OAuth 2.1规范,已弃用password授权类型(该类型因直接暴露用户凭证给客户端,存在安全风险)。针对第一方原生移动应用,推荐采用Authorization Code + PKCE的认证流程,这是OAuth 2.1专为原生/单页应用设计的安全方案。

实现思路

  1. 配置Spring Authorization Server支持Authorization Code授权类型并强制启用PKCE
  2. 自定义移动端专属认证端点,让APP直接提交用户名密码完成认证,绕过默认的表单登录页
  3. 移动端按照PKCE流程生成挑战参数,通过自定义端点获取授权码后,再换取访问令牌

具体实现步骤

1. 配置客户端与授权服务器

首先注册移动端客户端,开启PKCE支持:

@Bean
public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient mobileClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("mobile-app-client")
            .clientSecret("{noop}mobile-secret") // 第一方APP可简化密钥校验,或直接省略(依赖PKCE保障安全)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUri("com.yourmobileapp://oauth/callback") // 原生APP的自定义跳转URI
            .scope("read")
            .scope("write")
            .clientSettings(ClientSettings.builder().requireProofKey(true).build()) // 强制开启PKCE
            .build();
    return new InMemoryRegisteredClientRepository(mobileClient);
}

配置授权服务器专属的安全过滤器链:

@Bean
@Order(1)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .authorizationEndpoint(authEndpoint -> authEndpoint
                    .authorizationResponseHandler(new OAuth2AuthorizationCodeResponseHandler())
            );
    // 配置资源服务器(JWT模式)
    http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}

2. 自定义移动端认证端点

创建一个REST接口,接收移动端的用户名密码、PKCE参数,完成认证后返回授权码:

@RestController
@RequestMapping("/login")
public class MobileAuthController {

    private final AuthenticationManager authenticationManager;
    private final AuthorizationCodeGenerator authorizationCodeGenerator;
    private final RegisteredClientRepository registeredClientRepository;
    private final OAuth2AuthorizationService authorizationService;

    // 构造函数注入依赖
    public MobileAuthController(AuthenticationManager authenticationManager,
                                AuthorizationCodeGenerator authorizationCodeGenerator,
                                RegisteredClientRepository registeredClientRepository,
                                OAuth2AuthorizationService authorizationService) {
        this.authenticationManager = authenticationManager;
        this.authorizationCodeGenerator = authorizationCodeGenerator;
        this.registeredClientRepository = registeredClientRepository;
        this.authorizationService = authorizationService;
    }

    @PostMapping("/mobile")
    public ResponseEntity<Map<String, String>> mobileLogin(
            @RequestParam String username,
            @RequestParam String password,
            @RequestParam String clientId,
            @RequestParam String redirectUri,
            @RequestParam String codeChallenge,
            @RequestParam String codeChallengeMethod) {

        // 1. 校验用户名密码
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);
        Authentication authentication = authenticationManager.authenticate(authToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 2. 校验客户端合法性
        RegisteredClient registeredClient = registeredClientRepository.findByClientId(clientId);
        if (registeredClient == null || !registeredClient.getRedirectUris().contains(redirectUri)) {
            return ResponseEntity.badRequest().body(Map.of("error", "无效的客户端或跳转URI"));
        }

        // 3. 生成并保存授权码
        OAuth2AuthorizationCodeRequest authCodeRequest = OAuth2AuthorizationCodeRequest.withRegisteredClient(registeredClient)
                .principal(authentication)
                .redirectUri(redirectUri)
                .scopes(registeredClient.getScopes())
                .codeChallenge(codeChallenge)
                .codeChallengeMethod(codeChallengeMethod)
                .build();

        String authorizationCode = authorizationCodeGenerator.generate();
        OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
                .principalName(authentication.getName())
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .attribute(OAuth2AuthorizationCodeRequest.class.getName(), authCodeRequest)
                .token(OAuth2Token.withTokenValue(authorizationCode)
                        .tokenType(OAuth2TokenType.AUTHORIZATION_CODE)
                        .issuedAt(Instant.now())
                        .expiresAt(Instant.now().plus(registeredClient.getAuthorizationCodeSettings().getTokenTimeToLive()))
                        .build())
                .build();
        authorizationService.save(authorization);

        // 4. 返回授权码
        return ResponseEntity.ok(Map.of("code", authorizationCode));
    }
}

3. 调整默认安全过滤器链

允许移动端认证端点的匿名访问:

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/login/mobile").permitAll()
                    .anyRequest().authenticated()
            )
            .formLogin(withDefaults()); // 保留网页端表单登录(若不需要可移除)
    return http.build();
}

4. 移动端PKCE流程实现(伪代码)

// 生成PKCE参数
fun generatePkceParams(): Pair<String, String> {
    val codeVerifier = RandomStringUtils.randomAlphanumeric(64)
    val codeChallenge = codeVerifier.sha256().encodeBase64Url().trimEnd('=')
    return codeVerifier to codeChallenge
}

// 步骤1:调用自定义登录接口获取授权码
val (codeVerifier, codeChallenge) = generatePkceParams()
val loginResponse = HttpClient.post("https://your-server.com/login/mobile")
    .formBody(mapOf(
        "username" to "user123",
        "password" to "password123",
        "clientId" to "mobile-app-client",
        "redirectUri" to "com.yourmobileapp://oauth/callback",
        "codeChallenge" to codeChallenge,
        "codeChallengeMethod" to "S256"
    ))
val authorizationCode = loginResponse.body()["code"] as String

// 步骤2:用授权码换取访问令牌
val tokenResponse = HttpClient.post("https://your-server.com/oauth2/token")
    .formBody(mapOf(
        "grant_type" to "authorization_code",
        "code" to authorizationCode,
        "redirect_uri" to "com.yourmobileapp://oauth/callback",
        "client_id" to "mobile-app-client",
        "client_secret" to "mobile-secret",
        "code_verifier" to codeVerifier
    ))
val accessToken = tokenResponse.body()["access_token"] as String

关键说明

  • PKCE通过code_verifier和code_challenge机制,即使授权码被窃取,攻击者也无法换取令牌,保障了原生应用的安全性
  • 自定义端点既满足了移动端直接提交用户名密码的需求,又符合OAuth 2.1规范,避免了password授权的安全隐患
  • 若移动端偏好JSON请求体,可将接口参数改为@RequestBody接收JSON格式的登录请求

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 04:27:23