基于CAS搭建OIDC认证授权协议

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

  1. EU:End User:一个人类用户。
  2. RP:Relying Party,用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方;
  3. OP:OpenID Provider,有能力提供EU认证的服务(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息
  4. IDToken:JWT格式的数据,包含EU身份认证的信息。
  5. UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用AccessToken访问时,返回授权用户的信息,此接口必须使用HTTPS。

OIDC工作流程:

官网文档给出了详细的介绍,整个过程如下图(http://openid.net/specs/openid-connect-basic-1_0.html)

  1. RP发送一个认证请求给OP;
  2. OP对EU进行身份认证,然后提供授权;
  3. OP把ID Token和Access Token(需要的话)返回给RP;
  4. RP使用Access Token发送一个请求UserInfo EndPoint;
  5. UserInfo EndPoint返回EU的Claims。
    基于CAS搭建OIDC认证授权协议_第1张图片

    CAS服务端集成OIDC

    (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
基于CAS搭建OIDC认证授权协议_第2张图片
以上完成了OIDC在CAS服务端的注册过程,过程比较简单,但是其中有不少坑,读者可以结合官方文档实际操作。

OIDC 客户端实现

一、认证授权流程说明
基于CAS搭建OIDC认证授权协议_第3张图片
二、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

  1. 业务线接入授权第一步需要访问cas服务器,使用rest接口来获取id_token 和 access_token
  2. 如果没有登录,接口会将请求重定向到cas登陆页, 用户输入账号密码后,cas回调到业务线, 返回id_token 和
    access_token
  3. 如果用户已经登录到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服务端拉取用户信息,完成用户授权登录业务线的过程。

你可能感兴趣的:(CAS)