本文介绍了CAS自定义身份验证处理程序主体JSON问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在自定义身份验证处理程序的重写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返回List的toString表示形式,而不是其JSON表示形式.简而言之,如何从该函数返回属性的正确JSON序列化?

注意:

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

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

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

当我将主体创建为Map<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)返回Map<String, Object>

对象中带有伪映射的新Principal

当我将HashMap添加到Map<String, Object>的对象"部分时,对于["key":"value"]映射,它作为{"left": "key", "right": "value"}返回到客户端.我已经调试了很长时间,当我请求/tickets URL时,我看到了CAS如何使用json-smart-2.3库.我看到当我在Map的Object中发送Map的属性时,json-smart库使用其BeansWriter序列化Map,该Map获取类的字段并用作键.因此,我发送了HashMap-> CAS将其转换为Java Pair(在下一步中描述)-> Pair具有属性"left"和"right",因此它向我不需要的JSON主体添加了left和right字段./p>

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

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

4)为Map<String, Object>(几乎是解决方案)在对象中返回带有JSON表示形式的新Principal

我也尝试在属性的对象"部分中直接返回JSON表示形式:

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

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

时卡在了某个地方

我不想要这个:

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

或者这个:

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

我想要:

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

或这个

{"name":"ExampleName"}
解决方案

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

为什么我的响应JSON中有针对/v1/tickets的请求的"left"和"right"属性?

1)您返回新的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 流式传输.因此,您的哈希图值类型现在已更改为配对.

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库不处理结对类型,它对不处理的类型使用默认编写器,例如BeansWriterASM.该编写器获取该类的所有属性,并将它们用作JSON的键及其值.

9),因此在这种情况下,CAS在步骤3中将值"name":"test"->转换为"left":"name", "right":"test"对.由于json-smart不处理Pair类,因此它将返回此JSON.

是的,话虽长,但我想清楚地分享我的经验. json-smart库的更新时间不足,并且nimbus-jose-jwt库已计划更改json-smart库( https://bitbucket.org/connect2id/nimbus-jose-jwt/pull-requests/50/wip-在下一个发行版中允许-replacing-json-smart-with/diff ),然后CAS可能也会对其进行更改,但对于两者而言,这似乎都是一条漫长的路.

解决方法/解决方案

1)不要在您的SimplePrincipal中返回Map的实例.而是在属性的根上使用集合.因为和上面第3步中一样,如果您的值是Collection的实例,CAS不会将您的值与Pairs一起包装.例如,对我来说,工作示例是:

    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响应,因此将其解析为客户端.

I have a JSON problem while returning custom attributes in my custom authentication handler's overriden authenticateUsernamePasswordInternal function:

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

which createPrincipal method accepts Map<String, Object>

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

When I put Map<String, List> in attributes, CAS returns toString representation of the List instead of its JSON representation. In short, how to return the correct JSON serialization of attributes from this function?

Notes:

  1. CAS version used: 5.3.8
  2. Custom Authentication extended via AbstractUsernamePasswordAuthenticationHandler
  3. JWT is implemented which uses CAS protocol

Here what I tried so far:

1) CAS converts List of HashMap as String while validating the service (may be root cause)

When I create Principal as Map<String, new ArrayList<new HashMap<>>, my HashMap is converted to toString representation of the HashMap. So It's type information is now turned from HashMap -> String, which makes CAS return not correct JSON to my client because String is serialized as it is for JSON. Here where it happens ->

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

Here serverResponse contains:

<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>

What I expect:

<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) Returning new Principal with a dummy Map in the Object for the Map<String, Object>

When I add a HashMap to the Object section of Map<String, Object>, it returns to the client as {"left": "key", "right": "value"} for ["key":"value"] map.I have been debugging for so long, I see how CAS uses json-smart-2.3 library when I request /tickets URL. I see that when I send Map in the Object of Map for the attributes, json-smart library uses its BeansWriter to serialize Map, which gets fields of the class and uses as keys. So I send HashMap -> CAS converts it to Java Pair (described in next step) -> Pair has attributes "left" and "right", so it adds left and right fields to the JSON body which I don't want.

3) Debugging CAS to understand how it serializes attributes as JSON for API call (token url)

  • I look for the CAS for how it handles Principal, it merges everything as LinkedList of Pair's. So for that reason whatever I add in the Object of Map section, it returns as an array in the JSON representation like []. Which means when I add attributes.put("name", "ExampleName"), it returns as "{"name":["ExampleName"]} Because CAS calls mergeAttributes function of DefaultAuthenticationResultBuilder class, everything in the Principal is returned as List in that function which we sent in Principal object creations Map<String, Object>. So this means every attribute is returned as List? Also downloaded CAS source code and see that their tests assert like principal.getAttributes()[0] which gives a hint that this is default behavior? I couldn't see any documentation anywhere but doesn't make sense.

4) Returning new Principal with a JSON representation in the Object for the Map<String, Object> (Almost a solution)

Also I tried directly return JSON representation in the Object section of attributes:

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

It returns as expected JSON for API calls to the /ticket URL because serialization library tries to serialize String, which I sent So it is a kind of confusing solution but still have some problems. If I login via /login page, CAS wraps every attributes again with []. When I debug I see that this time CAS doesn't use the serializer that it uses when I cal /ticket URL. I tried to debug more but stuck somewhere when CAS started to use cas-server-core-webflow-api

I don't want this:

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

or this:

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

I want like:

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

or this

{"name":"ExampleName"}
解决方案

Finally, I found the root cause. If you are here and looking for the reason why your Principal attributes have {"left": "key", "right": "value"} instead of["key":"value"] here I will try to show root cause first and my solution:

Why there are "left" and "right" attributes in my response JSON for requests to /v1/tickets?

1) You return new SimplePrincipal(id, new HashMap)

2) CAS merges all attributes into a collection. You can find it:

DefaultAuthenticationResultBuilder -> mergeAttributes()

then it calls

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

3) Inside the function look at those lines:

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()));
        }

if your attributes are Map, their values are streamed as Pair. So your hashmaps values type is changed to Pair now.

4) Than CAS starts to create your JSON. Look atJWTTokenTicketBuilder -> buildJwt function (it is being handled by another class which is JwtBuilder in CAS 6.X versions, but the problem is still same)

5) CAS uses nimbus-jose-jwt (v5.10) to create JWTClaims.

6) nimbus-jose-jwt uses json-smart (v2.3) to return JWTObject.

7) CAS calls object.toJSONString() (function of JWTObject) for serializing your attributes into JSON. This is the part where it happens but it is also related to previous steps that I write in detail.

8) json-smart library doesn't handle Pair types, it uses default writers for the types they don't handle which is the case BeansWriterASM. This writer gets all attributes of the class and use them as keys of your JSON, and their values.

9) So in this case your value "name":"test" -> turned into "left":"name", "right":"test" Pairs on step 3 by CAS. Since json-smart doesn't handle Pair classes, it returns this JSON.

Yes, long story but I wanted to share my experiences clearly. json-smart library is not being updated for so long and nimbus-jose-jwt library has a plan to change json-smart library (https://bitbucket.org/connect2id/nimbus-jose-jwt/pull-requests/50/wip-allow-replacing-json-smart-with/diff) in their next releases which then CAS may change it too but it seems long path for both.

Workarounds/Solutions

1) Don't return instances of Map in your SimplePrincipal. Instead, use collections on the root of your attributes. Because as in the step 3 above, CAS doesn't wrap your values with Pair's if your values are the instance of Collection. E.g working example for me is:

    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) );

This will make your JSON to have meaningless array on the root but as said before, this is workaround for now.

2) Wrap your attributes with JSONAware class which json-smart library allows you return your own JSONString representation. This is not safe solution since if you change your CAS version and if CAS changed any library implementations than this solution may give you a headache but anyway I will share my working example for this too:

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;
    }
}

This class will return its own JSON represantation when json-smart's serialiazation begins. Also you need to wrap your all attributes with this class like:

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

3) You can implement your own Writer like JsonPairWriter and register it to JsonWriter's writerList. I tried this one, it works too but it could be dummiest solution compared to the above because of lots of maintanence & buggy side effects, just keep in mind.

Last but not least, this doesn't happen when you call /login endpoint of CAS which means getting token via browser. As I understand so far it has different workflow to return attributes and json instead of the flow I described above. Not sure but service and all attribute etc information is taken via REST call and get some XML response, so this is parsed to the clients.

这篇关于CAS自定义身份验证处理程序主体JSON问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-22 20:15