4

我在自定义身份验证处理程序的覆盖的 authenticateUsernamePasswordInternal 函数中返回自定义属性时遇到 JSON 问题:

return createHandlerResult( credential,
 this.getPrincipalFactory( ).createPrincipal( credential.getId( ), attributes) );

createPrincipal 方法接受哪个Map<String, Object>

Principal createPrincipal(String id, Map<String, Object> attributes);

当我Map<String, List>输入属性时,CAS 返回列表的 toString 表示而不是它的 JSON 表示。简而言之,如何从这个函数返回正确的 JSON 属性序列化?

笔记:

  1. 使用的 CAS 版本:5.3.8
  2. 通过 AbstractUsernamePasswordAuthenticationHandler 扩展的自定义身份验证
  3. JWT 使用 CAS 协议实现

这是我到目前为止所尝试的:

1)CAS在验证服务时将List of HashMap转换为String(可能是根本原因)

当我创建 Principal asMap<String, new ArrayList<new HashMap<>>时,我的 HashMap 被转换为 HashMap 的 toString 表示。所以它的类型信息现在从 HashMap -> String 转换,这使得 CAS 向我的客户端返回不正确的 JSON,因为 String 像 JSON 一样被序列化。它发生的地方->

AbstractUrlBasedTicketValidator -> validate() -> final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);

这里 serverResponse 包含:

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationSuccess>
        <cas:user>test</cas:user>
        <cas:attributes>
            <cas:roles>(Test,[ADMIN])</cas:roles>
         </cas:attributes>
      </cas:authenticationSuccess>
</cas:serviceResponse>

我的期望:

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationSuccess>
        <cas:user>test</cas:user>
        <cas:attributes>
            <cas:roles>
               <cas:Test>ADMIN</cas:Test>
            </cas:roles>
         </cas:attributes>
      </cas:authenticationSuccess>
</cas:serviceResponse>

2) 在 Object 中返回带有虚拟 Map 的新 PrincipalMap<String, Object>

当我将 HashMap 添加到 的 Object 部分时,它会像mapMap<String, Object>一样返回给客户端{"left": "key", "right": "value"}["key":"value"]调试了这么久,看到CAS在请求/tickets URL时是如何使用json-smart-2.3库的。我看到当我在 Map 的 Object 中为属性发送 Map 时,json-smart 库使用它的 BeansWriter 序列化 Map,它获取类的字段并用作键。所以我发送 HashMap -> CAS 将其转换为 Java Pair(在下一步中描述) -> Pair 具有属性“left”和“right”,因此它将左右字段添加到我不想要的 JSON 正文中。

3) 调试 CAS 以了解它如何将属性序列化为 JSON 以进行 API 调用(令牌 url)

  • 我寻找 CAS 以了解它如何处理 Principal,它将所有内容合并为 Pair 的 LinkedList。因此,出于这个原因,无论我在 Map 对象部分添加什么,它都会以 JSON 表示形式的数组形式返回,如 []。这意味着当我添加时attributes.put("name", "ExampleName"),它返回为"{"name":["ExampleName"]}因为 CAS 调用类的mergeAttributes函数,DefaultAuthenticationResultBuilder所以 Principal 中的所有内容都作为我们在 Principal object creations 中发送的那个函数中的 List 返回Map<String, Object>。所以这意味着每个属性都作为列表返回?还下载了 CAS 源代码,看到他们的测试断言如 principal.getAttributes()[0] 暗示这是默认行为?我在任何地方都看不到任何文档,但没有任何意义。

4)在对象中返回带有JSON表示的新主体Map<String, Object>(几乎是一个解决方案)

我还尝试在属性的 Object 部分中直接返回 JSON 表示:

Map<String, Object> attributes = new HashMap<>();
String roles = "{"TestModule":["Name1"]}"; (didn't add escape quotes for simplicity)
attributes.put("roles", roles);

它按预期返回 JSON 用于对/ticket URL 的 API 调用,因为序列化库尝试序列化我发送的 String 所以这是一种令人困惑的解决方案,但仍然存在一些问题。如果我通过/login页面登录,CAS 会再次用 [] 包装每个属性。当我调试时,我看到这次 CAS 没有使用它在我 cal /ticket URL 时使用的序列化程序。我尝试进行更多调试,但在 CAS 开始使用cas-server-core-webflow-api时卡在某个地方

我不想要这个:

{"rolesPerModule":["{\"TestModuleForBouncer_LIVE\":[\"ADMIN\"]}"]}

或这个:

{"name":[ExampleName]} *(yes, no "" returned here)*

我想喜欢:

{"rolesPerModule":{"{\"TestModuleForBouncer_LIVE\":[\"ADMIN\"]}"}}

或这个

{"name":"ExampleName"}
4

1 回答 1

2

最后,我找到了根本原因。如果您在这里并寻找您的主要属性在{"left": "key", "right": "value"} instead of["key":"value"]这里的原因,我将首先尝试显示根本原因和我的解决方案:

为什么对 /v1/tickets 的请求的响应 JSON 中有“左”和“右”属性?

1)你返回 new SimplePrincipal(id, new HashMap)

2) CAS 将所有属性合并到一个集合中。你可以找到它:

DefaultAuthenticationResultBuilder -> mergeAttributes()

然后它调用

CollectionUtils.toCollection(entry.getValue(), ArrayList.class)

3)在函数内部查看这些行:

else if (obj instanceof Collection) {
            c.addAll((Collection<Object>) obj);
            LOGGER.trace("Converting multi-valued attribute [{}]", obj);
        } else if (obj instanceof Map) {
            final Set<Map.Entry> set = ((Map) obj).entrySet();
            c.addAll(set.stream().map(e -> Pair.of(e.getKey(), e.getValue())).collect(Collectors.toSet()));
        } 

如果您的属性是 Map,则它们的值将作为Pair流式传输。因此,您的 hashmaps 值类型现在更改为Pair

4) CAS 开始创建您的 JSON。查看 JWTTokenTicketBuilder -> buildJwt函数(在 CAS 6.X 版本中由另一个类 JwtBuilder 处理,但问题仍然相同)

5) CAS 使用 nimbus-jose-jwt (v5.10) 创建 JWTClaims。

6) nimbus-jose-jwt 使用 json-smart (v2.3) 返回 JWTObject。

7) CAS 调用 object.toJSONString()(JWTObject 的函数)将您的属性序列化为 JSON。这是它发生的部分,但它也与我之前详细编写的步骤有关。

8) json-smart 库不处理 Pair 类型,它使用默认编写器来处理它们不处理的类型,这是 BeansWriterASM 的情况。该编写器获取类的所有属性并将它们用作 JSON 的键和它们的值。

9)因此,在这种情况下,您的值"name":"test"-> CAS 在第 3 步变成了"left":"name", "right":"test"Pairs。由于 json-smart 不处理 Pair 类,它返回这个 JSON。

是的,说来话长,但我想清楚地分享我的经历。json-smart 库很久没有更新,nimbus-jose-jwt 库有计划更改 json-smart 库(https://bitbucket.org/connect2id/nimbus-jose-jwt/pull-requests/50/ wip-allow-replacing-json-smart-with/diff)在他们的下一个版本中,然后 CAS 也可能会改变它,但对于两者来说似乎很长的路要走。

变通方法/解决方案

1)不要在 SimplePrincipal 中返回 Map 的实例。相反,在属性的根上使用集合。因为与上面的第 3 步一样,如果您的值是 Collection 的实例,CAS 不会用 Pair 包装您的值。例如,我的工作示例是:

    final Map<String, Object> test= new HashMap<>( );
    test.put( "faultyJSON", yourAttributes); // don't do this
    test.put( "properJSON", Collections.singleton( yourAttributes ) ); // make this

    return createHandlerResult( credential,
        this.getPrincipalFactory( ).createPrincipal( credential.getId( ), test) );

这将使您的 JSON 在根目录上具有无意义的数组,但如前所述,这是目前的解决方法。

2)使用 JSONAware 类包装您的属性,该类 json-smart 库允许您返回自己的 JSONString 表示。这不是安全的解决方案,因为如果您更改 CAS 版本并且如果 CAS 更改了任何库实现,那么此解决方案可能会让您头疼,但无论如何我也会为此分享我的工作示例:

public class JsonWrapper<T> implements JSONAware, Serializable
{
    @JsonValue
    public T attributes;

    public JsonWrapper( T attributes )
    {
        this.attributes = attributes;
    }

    @Override public String toJSONString( )
    {
        String json = "{}";
        try
        {
            json = new ObjectMapper( ).writeValueAsString( attributes );
        }
        catch ( JsonProcessingException e )
        {
            LoggerFactory.getLogger( getClass( ) )
                .error( "Couldn't map attributes: {}. Returning default: {}", attributes, json );
        }
        return json;
    }
}

当 json-smart 的序列化开始时,此类将返回其自己的 JSON 表示。您还需要使用此类包装所有属性,例如:

yourAttributes.forEach( ( k, v ) -> yourAttributes.put( k, new JsonWrapper<> (v) ) )
return createHandlerResult( credential,
            this.getPrincipalFactory( ).createPrincipal( credential.getId( ), yourAttributes) );

3)你可以像JsonPairWriter一样实现自己的Writer,并将其注册到JsonWriter的writerList中。我试过这个,它也有效,但与上述相比,它可能是最愚蠢的解决方案,因为有很多维护和错误的副作用,请记住。

最后但同样重要的是,当您调用 CAS 的/login端点时不会发生这种情况,这意味着通过浏览器获取令牌。据我所知,它有不同的工作流程来返回属性和 json,而不是我上面描述的流程。不确定,但服务和所有属性等信息是通过 REST 调用获取的并获得一些 XML 响应,因此将其解析给客户端。

于 2019-09-06T09:46:20.800 回答