苹果 Web 登录

1. OAuth2 流程

Web 三方登录有什么难的?不就是 OAuth2 吗?

嘿嘿,别着急,等苹果重拳出击。

理想情况是这样的:

  1. 先去苹果获取一对 client_id / client_secret,注册一个 redirect_uri
  2. 登录的时候跳到苹果页面去进行授权
  3. 用户授权成功后,重定向回我们的页面并带上code和其他参数
  4. 前端提交code和其他参数到后端
  5. 后端使用client_id+client_secret+code 换 acccess_token 和 id_token
  6. 获取苹果用户信息

实际上是这样的:

  1. 先去苹果获取一对 client_id / client_secret,注册一个 redirect_uri
  2. 登录的时候跳到苹果页面去进行授权
  3. 用户授权成功后,苹果用表单提交code和其他参数到我们后端接口(后端接口:这是在逼我用JSP?)
  4. 后端接口这时候要不就硬着头皮去验证获取access_token 和 id_token,要不就舔着脸把code和其他参数又重定向回前端页面(遵循OAuth2,但不多)

2. 准备

  • 苹果开发者账号
  • client_id/client_secret
  • redirect_uri
  1. 登录苹果开发者网站,搞个账号(收费的) Account - Apple Developer

  2. 获取 client_id 和 client_secret

进入开发者首页,找到 identifiers

增加一个Service ID

生成密钥

现在我们可以得到几个参数:

  • client_id: 对应service id identifier
  • client_secret: 对应 Key 那部分下载下来的p8文件
  • team_id:这里有 Account - Apple Developer
  • key_id:就是 keyID(狗头)

3. 方案分析

实际上是这样的:

  1. 先去苹果获取一对 client_id / client_secret,注册一个 redirect_uri
  2. 登录的时候跳到苹果页面去进行授权
  3. 用户授权成功后,苹果用表单提交code和其他参数到我们后端接口(后端接口:这是在逼我用JSP?)
  4. 后端接口这时候要不就硬着头皮去验证获取access_token 和 id_token,要不就舔着脸把code和其他参数又重定向回前端页面(遵循OAuth2,但不多)

根据这个实际情况,我们有两个方案

方案一:

方案一流程:

  1. 用户点击 Apple 登录
  2. 调用后端 authorize 接口,这个接口会生成一个跳转到苹果授权页面的URL并返回给前端
  3. 前端 redirect 到后端返回的这个URL
  4. 用户登录授权成功后,苹果用表单提交code和其他参数到我们后端接口
  5. 后端接口硬着头皮去验证获取access_token 和 id_token。获取用户成功后,又重定回前端页面, 注意这个时候要把cookie写回前端。

这个方案有个缺点,后端接口和前端页面必须同域,cookie才能生效。

方案二:

方案二流程:

  1. 用户点击 Apple 登录
  2. 调用后端 authorize 接口,这个接口会生成一个跳转到苹果授权页面的URL并返回给前端
  3. 前端 redirect 到后端返回的这个URL
  4. 用户登录授权成功后,苹果用表单提交code和其他参数到我们后端接口
  5. 后端接口得到code和其他参数之后,直接 Redirect 透传参数到前端的URL
  6. 前端页面得到code和其他参数后,调用后端的login接口进行登录
  7. 后端去验证并获取access_token 和 id_token。获取用户成功后,返回登录成功。

这个方案做的就是把苹果弄歪的 OAuth 流程给拉回来了 ,但有一个需要注意的点,重定向回前端URL的时候注意域名,别把Cookies写到钓鱼网站去了。

4. 实现

顺便选个方案来实现吧,难点不是这里。

  1. 导入依赖

使用 jjwt 就导入这几个依赖

xml 复制代码
<dependency>  
    <groupId>io.jsonwebtoken</groupId>  
    <artifactId>jjwt-impl</artifactId>  
    <version>0.12.3</version>  
    <scope>runtime</scope>  
</dependency>  
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->  
<dependency>  
    <groupId>io.jsonwebtoken</groupId>  
    <artifactId>jjwt-api</artifactId>  
    <version>0.12.3</version>  
</dependency>  
<dependency>  
    <groupId>io.jsonwebtoken</groupId>  
    <artifactId>jjwt-jackson</artifactId>  
    <version>0.12.3</version>  
</dependency>  

使用 jsoe4j 就用这个依赖

xml 复制代码
<dependency>  
    <groupId>org.bitbucket.b_c</groupId>  
    <artifactId>jose4j</artifactId>  
    <version>0.9.3</version>  
</dependency>

再加一个 okhttp 的依赖

xml 复制代码
<dependency>  
    <groupId>com.squareup.okhttp3</groupId>  
    <artifactId>okhttp</artifactId>  
    <version>4.11.0</version>  
</dependency>
  1. 添加配置
properties 复制代码
apple.oauth.team-id=xxxxx  
apple.oauth.client-id=xxxxxxx  
apple.oauth.client-secret=MIGTAgExxxxxxxxxxxx+o41iDkiJA  
apple.oauth.key-id=xxxxx  
apple.oauth.post-form-endpoint=https://example.com/post_form  
apple.oauth.authorization-endpoint=https://appleid.apple.com/auth/authorize  
apple.oauth.token-endpoint=https://appleid.apple.com/auth/token
java 复制代码
@Data  
@Configuration  
@ConfigurationProperties(prefix = "apple.oauth")  
public class AppleConfig {  
    private String teamId;  
    private String audience;  
    private String keyId;  
    private String clientId;  
    private String clientSecret;  
    private String postFormEndpoint;  
    private String authorizationEndpoint;  
    private String tokenEndpoint;  
}
  1. 生成重定向链接
java 复制代码
@ApiOperation(value = "获取登录信息")
@GetMapping("/authorize")
public ResponseEntity<?> authorize() {
    // 后端生成
    String state = generateState();
    // 标记这个state存在
    stateStore.put(state, (byte) 0);

    // https://appleid.apple.com/.well-known/openid-configuration authorization_endpoint
    String location = UriComponentsBuilder.fromHttpUrl(appleConfig.getAuthorizationEndpoint())
            // 授权码模式
            .queryParam("response_type", "code")
            // 苹果后台申请的
            .queryParam("client_id", appleConfig.getClientId())
            // 后端用来用来接收苹果post_form的接口
            .queryParam("redirect_uri", appleConfig.getPostFormEndpoint())
            // 后端生成
            .queryParam("state", state)
            // 这是苹果要求的,Web登录都是form_post
            .queryParam("response_mode", "form_post")
            // https://appleid.apple.com/.well-known/openid-configuration scopes_supported
            .queryParam("scope", "openid email name")
            .build().toUriString();

    // 重定向到苹果
    MultiValueMap<String, String> headers = new HttpHeaders();
    headers.add(HttpHeaders.LOCATION, location);
    return new ResponseEntity<>(headers, org.springframework.http.HttpStatus.FOUND);
}

测试完,发生重定向了,重定向地址是appleid授权页面

我们需要手动把 location 这里面的地址放到浏览器打开,这样才能完成授权。我这里因为要截图,所以才用了 Postman,简单点可以直接浏览器访问 localhost:port/test/authorize,浏览器会自动跳转到 Location。

  1. 处理post_form

授权完成后,苹果会使用表单提交,将code 以及 state 和用户信息(首次才有)提交到我们的apple.oauth.post-form-endpoint,我们应该提供接口来接收这次提交。

因为苹果后台要求必须配置 https 协议的链接,如果有条件的可以直接上云测试,像我这种没条件的,只能把参数记起来。

java 复制代码
@ApiOperation(value = "苹果登录回调")
@PostMapping(value = "/post_form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void postForm(OauthAppLoginParam form, HttpServletResponse response) throws IOException {

    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:10001/login");

    // 验证 state 存在
    if (!stateStore.containsKey(form.getState())) {
        response.sendRedirect(builder.queryParam("code", "invalid_state").toUriString());
        return ;
    }

    // 根据 code 和 status,去苹果换id_token/access_token
    OkHttpClient client = new OkHttpClient();

    // 构建请求体
    okhttp3.RequestBody formBody = new FormBody.Builder()
            .add("client_id", appleConfig.getClientId())
            .add("client_secret", generateClientSecretJose4j())
            .add("grant_type", "authorization_code")
            .add("code", form.getCode())
            .add("redirect_uri", appleConfig.getPostFormEndpoint())
            .build();

    // 创建 POST 请求
    Request request = new Request.Builder()
            .url("https://appleid.apple.com/auth/token")
            .post(formBody)  // 设置请求体
            .build();

    try (Response resp = client.newCall(request).execute()) {
        if (!resp.isSuccessful() || resp.body() == null) {
            response.sendRedirect(builder.queryParam("code", "invalid_request").toUriString());
            return ;
        }

        // 用户信息存储
        String body = resp.body().string();

        JsonNode jsonNode = JacksonUtils.parseJson(body);
        String idToken = JacksonUtils.getStringValue(jsonNode, "id_token");

        // idToken是个jwt,简单点就直接取中间段然后base64解开,然后json解析
        // 可靠点就使用jwk来解析出claim。jwks:https://appleid.apple.com/auth/keys
        String[] parts = idToken.split("\\.");
        String payload = BASE64Utils.decode(parts[1]);

        String token = generateToken();
        userStore.put(token, payload);
        response.addCookie(new Cookie("token", token));
    }

    response.sendRedirect(builder.queryParam("code", "ok").toUriString());
}

把浏览器的参数复制,调用后可以看到接口做了两个事情:

  • 设置了cookie:用于下一步获取用户信息提供token
  • 重定向到了一个地址:一般是跳到了前端页面,前后端需要约定好。
  1. 获取用户信息

上一步已经解析完了用户信息,这一步的代码你自己来吧,CRUD 我懒得写。

Ref

教你在 Node.js 项目中接入 Sign with Apple 第三方登录 - 知乎 (zhihu.com)

Authenticating users with Sign in with Apple | Apple Developer Documentation

相关推荐
Vfw3VsDKo21 小时前
Maui 实践:Go 接口以类型之名,给 runtime 传递方法参数
开发语言·后端·golang
是真的小外套1 天前
第十五章:XXE漏洞攻防与其他漏洞全解析
后端·计算机网络·php
ybwycx1 天前
SpringBoot下获取resources目录下文件的常用方法
java·spring boot·后端
小陈工1 天前
Python Web开发入门(十一):RESTful API设计原则与最佳实践——让你的API既优雅又好用
开发语言·前端·人工智能·后端·python·安全·restful
小阳哥AI工具1 天前
Seedance 2.0使用真人参考图生成视频的方法
后端
IeE1QQ3GT1 天前
使用ASP.NET Abstractions增强ASP.NET应用程序的可测试性
后端·asp.net
Full Stack Developme1 天前
SpringBoot多线程池配置
spring boot·后端·firefox
sxhcwgcy1 天前
SpringBoot 使用 spring.profiles.active 来区分不同环境配置
spring boot·后端·spring
稻草猫.1 天前
Spring事务操作全解析
java·数据库·后端·spring
希望永不加班1 天前
SpringBoot 整合 MongoDB
java·spring boot·后端·mongodb·spring