OIDC协议作为以OAuth2为基础衍生的出新的认证授权协议,将OAuth2的授权协议与OpenId的认证协议相结合,从而生产的新的sso协议OIDC协议(OpenID Connect)。本文讲解的是基于CAS 5.1.X 实现的OIDC搭建。
*本文章需要读者自行搭建CAS服务端
官网文档给出了详细的介绍,整个过程如下图(http://openid.net/specs/openid-connect-basic-1_0.html)
(https://apereo.github.io/cas/5.1.x/installation/OIDC-Authentication.html)
一、pom文件添加OIDC插件包
<dependency>
<groupId>org.apereo.casgroupId>
<artifactId>cas-server-support-oidcartifactId>
<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服务端的注册过程,过程比较简单,但是其中有不少坑,读者可以结合官方文档实际操作。
一、认证授权流程说明
二、client搭建
推荐使用Springboot搭建客户端
完整pom
<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.0modelVersion>
<packaging>warpackaging>
<groupId>com.hbtv.casgroupId>
<artifactId>portalartifactId>
<version>1.0version>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<java.version>1.8java.version>
<httpclient.version>4.5.2httpclient.version>
properties>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>1.5.6.RELEASEversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.1.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>4.1.6version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.3.0version>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>jwks-rsaartifactId>
<version>0.3.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.38version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.4version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>${httpclient.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>1.5.6.RELEASEversion>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
三、oidc客户端获取ID_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服务端拉取用户信息,完成用户授权登录业务线的过程。