苹果 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

相关推荐
凡人的AI工具箱几秒前
15分钟学 Python 第38天 :Python 爬虫入门(四)
开发语言·人工智能·后端·爬虫·python
丶213633 分钟前
【SQL】深入理解SQL:从基础概念到常用命令
数据库·后端·sql
木子020436 分钟前
Nacos的应用
后端
哎呦没36 分钟前
Spring Boot框架在医院管理中的应用
java·spring boot·后端
陈序缘1 小时前
Go语言实现长连接并发框架 - 消息
linux·服务器·开发语言·后端·golang
络71 小时前
Spring14——案例:利用AOP环绕通知计算业务层接口执行效率
java·后端·spring·mybatis·aop
2401_857600952 小时前
明星周边销售网站开发:SpringBoot技术全解析
spring boot·后端·php
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的在线学习交流平台
java·vue.js·spring boot·后端·学习·mysql·intellij-idea
qq_2518364572 小时前
基于SpringBoot vue 医院病房信息管理系统设计与实现
vue.js·spring boot·后端
qq_2518364573 小时前
基于springboot vue3 在线考试系统设计与实现 源码数据库 文档
数据库·spring boot·后端