介绍

OIDC 是 OpenID Connect 的简称,OIDC = (Identity, Authentication) + OAuth 2.0。它在 OAuth 上构建了一个身份层,是一个基于 OAuth 协议的身份认证标准协议。我们都知道 OAuth 是一个授权协议,它无法提供完善的身份认证功能,OIDC 使用 OAuth 的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端。
OAuth 2.0 提供了 Access Token 来解决授权第三方客户端访问受保护资源的问题,OIDC 在这个基础上提供了 ID Token 来解决第三方客户端标识用户身份认证的问题。OIDC 的核心在于在 OAuth 2.0 的授权流程中,一并提供用户的身份认证信息(ID Token)给到第三方客户端,ID Token 使用 JWT 格式来包装,得益于 JWT 的自包含性紧凑性以及防篡改机制,使得 ID Token 可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了 UserInfo 的接口,用户获取用户的更完整的信息。OIDC规范

专业术语

名词 描述
OpenID Provider 指授权服务器,负责签发 Id Token,即认证中心
End User 终端用户,Id Token 的信息中会包含终端用户的信息
Relying Party 身份认证和授权信息的消费方,即请求 Id Token 的应用
Id Token 由 OpenID Provider 颁发,包含关于终端用户的信息字段

接口介绍

接口使用详情参考官网说明

URI 描述
/oidc/.well-known 返回有关用于 OIDC 身份验证的端点、支持的范围等的基本信息
/oidc/authorize 获取code或者id_token
/oidc/accessToken, /oidc/token 通过code换取access_token
/oidc/profile 通过access_token参数获取用户信息
/oidc/revoke 撤销access_token或refresh_token

错误描述

以下为接入OIDC协议可能遇到的一些错误描述

错误码 描述
empty_client_id 参数client_id为空
empty_client_secret 参数client_secret为空
empty_redirect_uri 参数redirect_uri为空
empty_response_type 参数response_type为空
empty_code code为空
app_unsupport_sso 应用不支持sso登录
app_unsupport_oauth 应用不支持OAuth认证
invalid_client_id 非法的client_id
invalid_response_type 非法的response_type
invalid_scope 非法的scope
invalid_grant_type 非法的grant_type
redirect_uri_mismatch 非法的redirect_uri
unsupported_response_type 不支持传递的response_type
invalid_code 非法的code
unsupported_refresh_token 不支持refresh_token的方式
access_token_exprise access_token过期
invalid_access_token 非法的access_token
invalid_refresh_token 非法的refresh_token
refresh_token_exprise refresh_token过期

流程

常见的 OIDC 授权流程如下:
oidc-oidc.svg

接入

客户端需要先注册到用户中心中,注册后通过编辑页面可以获取到接入的必要服务信息
image.png
获取id_token
客户端接入授权第一步需要访问认证中心,使用rest接口开获取id_token 和 access_token
如果没有登录,接口会将请求重定向到认证中心登陆页, 用户输入账号密码后,认证中心回调到客户端, 返回id_token 和 access_token
如果用户已经登录到认证中心,接口会直接回调客户端,返回 id_token 和 access_token
使用页面接收认证中心回调的参数
因为认证中心提供的回调参数是以hash方式进行传入的(以 # 将id_token 和 access_token 拼接到回调url中),例如
http://127.0.0.1:8081/oidc-demo/login.html#id_token=xxxx&access_token=xxxxx
所以在这里需要使用一个页面来接收这些参数,并调用客户端的接口(见下列的客户端/catchResponse接口)来处理传递过来的id_token 和 access_token的合法性,这里提供一个js的基本例子,如何接受和解析这些参数:

  1. // 从url中解析参数
  2. let params = {}, postBody = window.location.hash.substring(1), regex = /([^&=]+)=([^&]*)/g, m;
  3. while (m = regex.exec(postBody)) {
  4. params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
  5. }
  6. // 将参数id_token 和 access_token发送给客户端的处理接口
  7. axios.post(`/oidc-demo/catchResponse` , params)
  8. .then(res => {
  9. if (res.status === 200) {
  10. // 验证通过的自定义处理
  11. } else {
  12. this.$message.error('error')
  13. }
  14. });

添加客户端配置文件

  1. uc:
  2. oidc:
  3. # 应用编辑页面->认证中心服务信息->服务URL
  4. acUrlPrefix: http://192.168.1.1:30040/gc-starter-ac/
  5. # 用于解析hash参数的js页面地址
  6. demoServerUrl: http://127.0.0.1:8081/oidc-demo/login.html
  7. # 应用编辑页面->高级->clientId
  8. clientId: ff4f75a0137d4e5ca3c1075ed29a4f42

添加客户端配置类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "uc.oidc")
@Data
public class OidcDemoConfig {

    /**
     * 认证中心的服务地址
     */
    private String acUrlPrefix;

    /**
     * 用于解析hash参数的js页面地址
     */
    private String demoServerUrl;

    /**
     *  oidc 应用的clientId
     */
    private String clientId;

}

添加客户端登录接口
这个接口用于组装登录请求,并重定向至认证中心,其中 clientId 参数可在用户中心注册后,应用的编辑页面获取

import com.gccloud.oidcdemo.config.OidcDemoConfig;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@RestController
@RequestMapping
public class OidcController {

    @Resource
    private OidcDemoConfig config;

    @GetMapping(value = "/login")
    public void login(HttpServletResponse response) throws IOException {
        String clientId = config.getClientId();
        response.sendRedirect(config.getAcUrlPrefix() + "oidc/authorize?response_type=id_token token&client_id=" + clientId + "&scope=openid" +
                "&redirect_uri=" + config.getDemoServerUrl() );
    }

}

添加 catchResponse 接口
该接口用于处理上述页面 js 发送过来的回调参数,其中包含 id_token (可用于身份认证,jwt格式,包含用户信息),access_token(访问令牌,同oauth2.0,可用于向认证中心请求用户信息),获取到id_token 和access_token后即可认为用户已经完成认证,在catchResponse中进行相应的客户端登录处理即可,其中用户信息可从id_token中解析获取,或者使用access_token向认证中心的/oidc/profile端点请求获取

import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

@RestController
@RequestMapping()
public class OidcController {

    @RequestMapping(value = "/catchResponse", method = RequestMethod.POST)
    public void catchResponse(@RequestBody JSONObject jsonObject) throws Exception {
        // 解析jwt格式的id_token
        final Base64.Encoder encoder = Base64.getEncoder();
        String encodedText = encoder.encodeToString(jsonObject.getString("id_token").getBytes(StandardCharsets.UTF_8));
        System.out.println(encodedText);
        byte[] result = org.apache.commons.codec.binary.Base64.decodeBase64(jsonObject.getString("id_token").split("\\.")[1].getBytes());
        JSONObject tokenJson = JSONObject.parseObject(new String(result));
        // 获取access_token,可用于向认证中心的/oidc/profile端点请求用户信息
        String accessToken = jsonObject.getString("access_token");
    }

}