OIDC协议作为以OAuth2为基础衍生的出新的认证授权协议,将OAuth2的授权协议与OpenId的认证协议相结合,从而生产的新的sso协议OIDC协议(OpenID Connect)。本文讲解的是基于CAS 5.1.X 实现的OIDC搭建。
*本文章需要读者自行搭建CAS服务端
OIDC主要术语说明:
http://openid.net/specs/openid-connect-basic-1_0.html#Terminology
- EU:End User:一个人类用户。
- RP:Relying Party,用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方;
- OP:OpenID Provider,有能力提供EU认证的服务(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息
- IDToken:JWT格式的数据,包含EU身份认证的信息。
- UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用AccessToken访问时,返回授权用户的信息,此接口必须使用HTTPS。
OIDC工作流程:
官网文档给出了详细的介绍,整个过程如下图(
http://openid.net/specs/openid-connect-basic-1_0.html
)
- RP发送一个认证请求给OP;
- OP对EU进行身份认证,然后提供授权;
- OP把ID Token和Access Token(需要的话)返回给RP;
- RP使用Access Token发送一个请求UserInfo EndPoint;
-
UserInfo EndPoint返回EU的Claims。
CAS服务端集成OIDC
(
https://apereo.github.io/cas/5.1.x/installation/OIDC-Authentication.html
)
一、pom文件添加OIDC插件包
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-oidc</artifactId>
<version>${cas.version}</version>
</dependency>
二、JWK钥匙生成
官方提供的用于生产JWK文件工具
https://mkjwk.org/
或者使用本地JAR生产 jar下载地址:
https://download.csdn.net/download/becausesy/10396777
三、配置文件
#--------------------openId connect------------------
#签名文件路径
cas.authn.oidc.jwksFile=classpath:/static/keystore.jwks
#签发端地址
cas.authn.oidc.issuer=https://localhost:8888/cas/oidc/
#-------------------开启动态注册客户端------------------
cas.authn.oidc.dynamicClientRegistrationMode=OPEN
#-------------------自定义字段------------------
cas.authn.oidc.userDefinedScopes.hbtvprofiles=id,name,mobile,email,avatar
四、客户端注册
JSON文件形式
{
"@class" : "org.apereo.cas.services.OidcRegisteredService",
"clientId": "...",
"clientSecret": "...",
"serviceId" : "...",
"name": "OIDC Test",
"id": 10,
"scopes" : [ "java.util.HashSet",
[ "profile", "email", "address", "phone", "offline_access", "displayName", "eduPerson" ]
]
}
cas-management
以上完成了OIDC在CAS服务端的注册过程,过程比较简单,但是其中有不少坑,读者可以结合官方文档实际操作。
OIDC 客户端实现
一、认证授权流程说明
二、client搭建
推荐使用Springboot搭建客户端
完整pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<groupId>com.hbtv.cas</groupId>
<artifactId>portal</artifactId>
<version>1.0</version>
<properties>
<!-- 指定编码格式UTF-8 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 指定java版本1.8 -->
<java.version>1.8</java.version>
<!-- httpclient 版本 -->
<httpclient.version>4.5.2</httpclient.version>
</properties>
<!-- Spring Boot 1.5 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
</parent>
<dependencies>
<!-- 添加springboot WAR包插件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- 添加springboot web项目源生依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency>
<!-- thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- jwt 加密工具 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.3.0</version>
</dependency>
<!-- 阿里 fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
<!-- StringUtils相关工具类jar包 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<!-- httpclient utils -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
<!-- redis 依赖配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>1.5.6.RELEASE</version>
</dependency>
</dependencies>
<!-- 使用 maven 打jar包 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
三、oidc客户端获取ID_Token
- 业务线接入授权第一步需要访问cas服务器,使用rest接口来获取id_token 和 access_token
-
如果没有登录,接口会将请求重定向到cas登陆页, 用户输入账号密码后,cas回调到业务线, 返回id_token 和
access_token - 如果用户已经登录到cas,接口会直接回调业务线,返回id_token 和 access_token
.
四、使用界面接收cas回调信息
因为cas提供的回调参数是以hash方式进行传入的,所以在这里需要使用一个页面来接收这些参数,并调用业务线接口来认证传递过来的id_token 和 access_token的合法性,这里提供一个js的基本例子,如何接受和解析这些参数:
var params = {}, postBody = location.hash.substring(1), regex = /([^&=]+)=([^&]*)/g, m;
while (m = regex.exec(postBody)) {
params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
// And send the token over to the server
var req = new XMLHttpRequest();
// using POST so query isn't logged
req.open('POST', 'http://' + window.location.host
+ '/cas_client/redirect/catchResponse', true);
req.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded');
req.onreadystatechange = function(e) {
if (req.readyState == 4) {
if (req.status == 200) {
var returnObj = eval('(' + req.responseText + ')');
if (returnObj.status === 200) {
// If the response from the POST is 200 OK, perform a redirect
window.location = 'http://' + window.location.host
+ '/cas_client/redirect/main'
} else {
alert(returnObj.message);
}
}
// if the OAuth response is invalid, generate an error message
else if (req.status == 400) {
alert('There was an error processing the token')
} else {
alert('Something other than 200 was returned')
}
}
};
req.send(postBody);
五、业务线对ID_TOKEN 和 ACCESS_TOKEN进行认证
这里ID_TOKEN采用的是JWT格式的,上述页面在得到ID_TOKEN 和 ACCESS_TOKEN之后进行验证,验证步骤如下:
ID_Token的验证及Access_Token:
@RequestMapping(value = "/catchResponse", method = RequestMethod.POST)
@ResponseBody
public String catchResponse(HttpServletRequest request, HttpServletResponse response, String access_token,
String token_type, String expires_in, String id_token) throws Exception {
// 验证id_token的合法性
Map<String, Claim> claims = null;
try {
claims = JwtUtil.verifyToken(env, id_token);
} catch (Exception e) {
throw new CommonException(ResultCode.UNAUTHORIZED, "无效的idToken");
}
// 验证accessToken的合法性步骤:
// 1).
// 对accesstoken进行16进制hash编码,编码方式和id_token头里面的编码方式需保持一致,例如头中alg是RS256,则使用SHA256进行Hash编码
// 2).
// 取hash编码后的bytes数组的前半部分,进行base64编码
// 3).
// base64后和id_token中的at_hash一样则验证通过
if (!StringUtils.equals(Base64Util.encode(HashUtil.HEXSHA256(access_token)),
claims.get("at_hash").asString())) {
throw new CommonException(ResultCode.UNAUTHORIZED, "AccessToken无效");
}
// 返回access token
return ResUtil.getJsonStr(ResultCode.OK, "验证成功",
EncryUtils.encrypt(access_token + "," + claims.get("name").asString() + "," + new Date().getTime()));
}
具体的验证请参考官网:
https://openid.net/specs/openid-connect-implicit-1_0.html
验证通过后,业务线认可这是合法的ID_TOKEN和ACCESS_TOKEN,之后通过这两个参数去OIDC服务端拉取用户信息,完成用户授权登录业务线的过程。