Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Java로 OpenID Consumer 서비스 구현하기 #4 : OpenID4Java로 추가정보 요청하기

이 글은 다음글에 이어진 글입니다.
Java로 OpenID Consumer 서비스 구현하기 #1 : OpenID란?
Java로 OpenID Consumer 서비스 구현하기 #2 : OpenID4Java로 인증요청하기
Java로 OpenID Consumer 서비스 구현하기 #3 : OpenID4Java로 인증확인하기



이제 간단하게 인증을 요청하고 인증확인을 구현하였는데 실제 서비스를 하려면 추가정보를 받아야 한다. 로그인처리를 하려면 추가정보가 필요하다. 오른아이디를 통해서 회원가입 처리를 하려고 해도 그렇고 아이디야 당연히 넘어오지만 이름, 닉네임, 전화번호등등의 정보를 받아와야 한다.

openid4java에 들어있는 SampleConsumer를 보면 아래와 같은 코드들이 있다.(앞에서 만든 SampleConsumer말고...)
FetchRequest fetch = FetchRequest.createFetchRequest();
fetch.addAttribute("email", "http://schema.openid.net/contact/email", true);
authReq.addExtension(fetch);
이 코드가 추가정보를 요청하는 코드인데 이 코드는 Attribute Exchange 스펙을 이용하는 코드이다. 스펙에 이름에서 알 수 있듯이 추가적인 정보를 교환하는 스펙이다. 나도 스펙들을 대충 보고 당연히 이걸 이용한다고 생각하고 열심히 삽질을 했는데 어찌된 일인지 정보를 받아올 수 있었다. 의외로 추가정보를 Attribute Exchange를 이용하는 것이 아니라 Simple Registration Extension 스펙(영문스펙)을 이용한다. Simple Registration Extension을 이용해서 유저의 추가정보를 받아 올 수가 있다.(Attribute Exchange가 왜 안되는지는 나도 잘 모르겠다. 안된다기 보다는 내가 못하는 거겠지만... 이래서 스펙을 볼 줄 알아야 하는건데....)


어쨌든 Simple Registration Extension를 이용해서 정보를 요청하자.


SRegRequest sreq = SRegRequest.createFetchRequest();
sreq.addAttribute("email", true);
sreq.addAttribute("nickname", true);
sreq.addAttribute("fullname", true);
sreq.addAttribute("dob", true);
sreq.addAttribute("gender", true);
authReq.addExtension(sreq);

일단 인증을 요청할 때 추가정보도 같이 요청해야 하기 때문에 authRequest 메서드안에

AuthRequest authReq = manager.authenticate(discovered, returnToUrl, trustRoot);

아래쪽에 위의 코드를 추가해 준다. (SRegRequest를 AuthRequest객체에 추가해 주어야 하므로..) 그리고 SReqRequest를 사용하려면 org.openid4java.message.sreg.SRegRequest를 import 해주어야 한다. 이렇게만 하고 다시 실행을 하고 인증을 요청하면

사용자 삽입 이미지

위의 사진처럼 OP측에서 사용자의 추가정보를 넘겨주는 것을 사용자에게 보여준다. 당연히 End-User는 여기서 정보를 변경해 줄 수가 있다. End-User가 승인을 하면 OP는 인증정보와 함께 요청한 추가정보를 같이 담아서 RP측이 받을 수 있도록 해준다. 요청하는 정보는 Simple Registration Extension의 스펙을 확인하면 되고 당연히 OP측에거 가지고 있는 정보만 요청해서 받을수 있다.





추가정보를 요청하고 OP가 정보를 넘겨주었으니 그럼 이제 받아보자.


if (verified != null) {               
    AuthSuccess authSuccess = (AuthSuccess) verification.getAuthResponse();

    if (authSuccess.hasExtension(SRegMessage.OPENID_NS_SREG))
    {
        MessageExtension ext = authSuccess.getExtension(SRegMessage.OPENID_NS_SREG);

        if (ext instanceof SRegResponse)
        {
            SRegResponse sregResp = (SRegResponse) ext;

            String nickName = sregResp.getAttributeValue("nickname");
            String email = sregResp.getAttributeValue("email");
            String dob = sregResp.getAttributeValue("dob");
            String gender = sregResp.getAttributeValue("gender");

            httpReq.setAttribute("nickName", nickName);
            httpReq.setAttribute("email", email);
            httpReq.setAttribute("dob", dob);
            httpReq.setAttribute("gender", gender);
        }
    }

    return verified;  // success           
}

기존 코드의 verifyResponse 메서드 안에 있는 if (verified != null) {} 의 부분을 위의 코드로 바꾸어준다. 앞에서는Simple Registration Extension의 정보를 확인하는 부분이 없었기 때문에...(다시 한번 말하지만 소스의 세세한 부분은 이해하지 못하고 있다.) 간단히 설명하자면 SRegMessage가 있는지 확인하고 있으면 SRegResponse로 받아서 각 정보를 받아온다. 나는 이 메서드를 호출한 JSP파일에서 다시 정보를 받아야 하기 때문에 verifyResponse를 호출할 때 파라미터로 넘겨준 Request에 각 정보를 다시 심어주는 방식을 택했다.

의 코드를 사용하려면 org.openid4java.message.sreg.SRegMessage 와 org.openid4java.message.sreg.SRegResponse 를 import 해주어야 한다.



이제 JSP에서 받아오자. resultOpenid.jsp를 다음과 같이 변경하자.


// resultOpenid.jsp
<%@ page contentType="text/html; charset=utf-8" %>
<% request.setCharacterEncoding("utf-8"); %>
<% response.setContentType("text/html; charset=utf-8"); %>
<%@ page import="org.openid4java.discovery.Identifier" %>
<%@ page import="kr.test.openid.SampleConsumer" %>
<%
    SampleConsumer sc = (SampleConsumer)application.getAttribute("cm");
    Identifier ver = sc.verifyResponse(request);

    if (ver != null) {
        out.println(ver.getIdentifier() + "님 환영합니다.<br>");
        out.println("닉네임 : " + request.getAttribute("nickName") + "<br>");
        out.println("이메일 : " + request.getAttribute("email") + "<br>");
        out.println("생년월일 : " + request.getAttribute("dob") + "<br>");
        out.println("성별 : " + request.getAttribute("gender") + "<br>");
    } else {
        out.println("로그인에 실패하였습니다.");
    }
%>

정상적으로 돌아간다면 위의 정보를 다 받아오고 화면에 출력해 준다.

사용자 삽입 이미지

근데 이상한건 fullname의 경우는 받아오지를 못한다. myid측에서는 인코딩의 문제점을 제기했는데 시간의 압박으로 그부분까지 확인해 보지는 못했다. 나같은 경우에는 fullname이 들어가면 인증자체가 실패해서 일단 fullname은 제외해 놓은 상태이다.(이름만 한글이 넘어왔으므로 인코딩 문제라는데 심증이 가기는 한다.) 참고로 End-User가 "승인"을 하면("이번만 승인"이 아닌..) 로그인 할 때 마다 요청한 정보가 넘어온다. 정보를 갱신하고자 한다면 로그인처리를 할 때 갱신에 대한 처리를 하면 End-User가 OP측에서 개인정보를 수정하면 새 정보를 받을 수 있을 것이다.

이것외에도 해야할 일은 많겠지만 일단 필요한 기능은 간단하게 다 구현했다. 난 여기까지만.... 소스가 많지 않아서 내용만 보아도 이해가 되겠지만 혹 더 도움이 될까봐 위의 결과로 나온 소스를 첨부로 올린다.



그리고 이부분은 해결은 안되었는데 다음과 같은 에러가 나는 경우가 있다.

사용자 삽입 이미지

URI is not absolute
라는데 URI가 절대경로가 아니라는건데 이건 엄청 찾아봤는데 죽어도 이유를 모르겠다. 절대경로인데 절대경로가 아니라니.. ㅡ..ㅡ 더군다나 java.lang에서 나는것처럼 보이니 이유를 모르겠다. 나같은 경우에는 Jeus환경에서 나타났고 웹에서 찾아보니 다른 분은 Resin환경에서 같은 문제가 발생했었다. 나는 자연스럽게 환경이 tomcat으로 바뀌면서 더이상 이문제를 가지고 고민할 필요가 없어지긴 했는데 일단 이문제는 tomcat에서는 발생하지 않는다.

그럼 이제 DB에 대한 부분이 남는다. DB구성을 어떻게 할 것인가. OpenID의 개념상에는 한명이 여러 OpenID를 사용할 수도 있고 그것들은 같은 유저로서 인식되어야 하는 것 같고 거기에 OpenID를 추가해고 삭제할 수도 있어야 하는데 사이트마다 적용하는 범위는 다를 수 있다. 어쨌든 A Recipe for OpenID-Enabling Your Site라는 글에 DB에 대한 내용이 자세히 나와있다. 이곳에서는 기존유저가 있다는 가정하에 OpenID를 추가하는 것을 전제로 하고 있지만 내가 찾아본 자료중에 DB구성에 대해서 자세히 나와 있는 포스팅은 이곳말고는 보지 못했다.

그리고 OpenID Book이라는 프로젝트가 있는데 OpenID에 대한 내용을 정리하는 것 같다. 해당 사이트에서 무료로 다운로드를 받을 수 있고 사이트에 가기 귀찮으신 분을 위해서 올려둔다. PDF파일로 되어 있으면 250page정도 되는데 아마 이걸 읽으면 거진다 이해할 수 있을듯 하다. 분량이 분량이니 만큼....(당연히 영문이고 나도 안읽었다. ㅎ)




이제 필요한 처리는 다 했다. 로그인처리도 했고 정보도 받아왔으니 이제 자신의 사이트에서 필요한 처리를 하면 된다. 로그인처리를 하려면 세션에 로그인정보를 심는다든가 회원가입을 받으려면 약관동의를 하고 디비에 넣는다든가 하는 각자 원하는 처리를 해주면 된다.

이제 정책적인 문제가 남는데 인증 확인후 회원가입등의 절차나 추가정보를 요청할 수 있다. 하지만 OpenID의 취지는 자신을 증명하는 것이기 때문에 사이트가 필요해서 요청하는 절차에 대해서 강제사항이 되지 않아야 한다. 즉 인증을 받은 뒤에 회원가입 요청을 한다고 하였을 때 회원가입을 하지 않는다 하더라고 로그인처리를 해주어 OpenID사용자가 사이트 사용에 무리가 없도록 해야한다. 다 구현하고 나서도 이때문에 고민을 많이 했는데 로그인과 회원가입을 같은 개념으로 두지 않아야 한다. OpenID는 회원가입을 하지 않아도 자신이 누군지를 증명해서 사이트를 이용할 수 있게 되는 것이다.(이부분은 비디오가게를 비회원으로 이용할 수도 있는 것과 같다.)

다음 문제는 로그아웃의 문제이다. 로그아웃은 어떻게 해야되는가... 여기서의 고민은 로그인이 RP와 OP에 둘다 된다는 것에 문제가 있다. RP측은 정보를 받아왔으니 RP에서 일반 회원이 로그인처리를 하는 것과 동일하게 처리하면 된다. 보통이면 세션에 정보를 심어서 처리할텐데 로그아웃할때는 세션을 날려버리면서 로그아웃을 처리한다. 하지만 테스트해보면 RP에서 로그아웃을 해도 OP쪽에는 로그인상태로 남아있는데 그런 RP가 로그아웃정보를 OP로 날려야 하는가 그럼 어떻게 날려야 하는가의 고민이 남았는데 다음 글을 참고하면 답을 얻을 수 있다.

http://cs.myid.net/issues/8737
http://cs.myid.net/issues/8747
http://cs.myid.net/issues/8748

결론부터 얘기하자면 RP측에서만 로그아웃을 처리해주면 되고 이에 큰 문제는 없어보인다. 위의 정책적인 부분들은(사실은 구현하면서 어렵던 부분들도 ㅎㅎ) myid측에서 많은 도움을 받았다. 달리 OpenID에 대해서 도움을 얻을 곳이 없고 그래도 myid에 계신분이 이해도가 제일 높다고 생각했기 때문에 여러차례 문의를 했는데 항상 빠르고 친절하게 답변을 주신데에 이자리를 빌어 감사드립니다. ^^


다음에 다시 OpenID를 구현할 때는 좀더 수준 높게 해볼 수 있게 되기를 바라면서.. ^^
2008/07/21 02:23 2008/07/21 02:23