이 문서는 Sxip이 스폰서하고 오픈소스로 개발된 OpenID4Java 를 통해 OpenID Consumer 를 구축하는 가이드라인을 제공합니다.
일단 이것을 시작하기 전에 OpenID 적용 서비스 Best Practice 문서를 꼭 읽어보시기 바랍니다.
무엇보다 이 문서는 OpenID에 대한 최소한의 개념을 숙지했다는 것을 가정합니다. Consumer가 무엇인지 Provider에 대한 내용은 언급하지 않습니다. 그리고 차차 개선해나갈 것이지만 서버의 확장성을 고려하지도 않습니다.
여러가지 이슈들도 있을 수 있지만, OpenID Enabled 한 Consumer 사이트를 만드는 것이 부담되어 쉽게 시작하지 못하는 분들을 위한 가이드라인입니다.
JDK 와 Tomcat 설치에 대해서는 좋은 문서가 많으니 OpenID4Java 설치에 대한 내용만 쓰겠습니다.
겨우 파일만 몇개 복사한 것 뿐인데 설치가 과연 잘 되었을까요?
한 번 확인해볼 필요가 있습니다. 이 과정은 Consumer 로그인 과정 전부를 포함하지는 않습니다.
단지, 설치(Installation) 과정이 올바르게 수행되었는지 검사하는 차원에서 진행하는 것입니다.
이 과정은 간단한 JSP 파일 하나를 편집하고 바로 Provider 의 인증 페이지까지 보내줍니다.
물론 비밀번호를 입력하고 사이트를 승인하더라도, 아무런 일도 일어나지 않고 404 Not Found 만 보여질 것입니다. :-)
DocumentRoot 밑 아무곳에나 적당한 이름(test.jsp 정도)으로 파일을 하나 생성하고 아래의 코드를 입력하세요.
아래 테스트 코드의 설명이나, 간단하지만 완전한 Test 를 위해서는 아래 코드를 들고 바로 Simple Consumer 부분으로 뛰어넘으시길 바랍니다.
<%@ page language="java" contentType="text/html;charset=utf-8" %>
<%@ page import="java.util.List" %>
<%@ page import="org.openid4java.*" %>
<%@ page import="org.openid4java.consumer.*" %>
<%@ page import="org.openid4java.discovery.*" %>
<%@ page import="org.openid4java.message.*" %>
<%!
private static ConsumerManager mgr = null;
static {
try {
mgr = new ConsumerManager();
mgr.setAssociations( new InMemoryConsumerAssociationStore() );
mgr.setNonceVerifier( new InMemoryNonceVerifier(60*60) );
} catch( Exception e ) { throw new RuntimeException(e); }
}
%>
<%
String returnUrl = "http://localhost:8080/openid";String trustRoot = "http://localhost:8080/";
String userInput = "http://rath.myid.net";
Identifier identifier = Discovery.parseIdentifier(userInput);
userInput = identifier.getIdentifier();
List discoveries = mgr.discover(userInput);
DiscoveryInformation discovered = mgr.associate(discoveries);
session.setAttribute("openid-discover", discovered);
AuthRequest auth = mgr.authenticate(discovered, returnUrl, trustRoot);
response.sendRedirect( auth.getDestinationUrl(true) );
%>
위 부분에서 꼭 변경하셔야 할 것이 있습니다. 소스코드 중간 부분에 있는 userInput 변수에 http://rath.myid.net 을 대입하는 부분인데요.
자신의 OpenID를 입력하셔야 합니다. openid4java 가 정상적으로 설치되었는지 확인하기 위해 별도 HTML Form 을 만들지 않기 위해서 한 것입니다.
위의 소스코드를 붙여넣고 test.jsp 파일에 저장한 뒤, 해당 파일을 접근할 수 있는 URL을 주소창에 입력해보세요.
제 경우는 Tomcat 을 8080 에 띄웠기 때문에
http://localhost:8080/test.jsp
를 사용합니다. 주소를 입력하고 요청을 했을 때, 아래와 같이 Provider 의 페이지가 나오면 정상적으로 설치가 된 것입니다! :-)
편의상 OpenID Provider 인 myid.net 의 스크린샷을 첨부하였으며, idtail, idpia 등 다른 Provider를 사용할 경우 위 이미지는 다르게 표시될 수 있습니다.
위와 같이 Provider 사이트가 정상적으로 보면 First Test는 성공한 것입니다.
재미삼아 비밀번호를 입력해보세요. 그러면 위 테스트 JSP 코드 중간부분에 입력한 returnURL 부분의 승인을 요청하는 페이지가 표시될 것입니다.
승인은 필요없습니다. 실제로 Consumer 사이트가 localhost를 사용하진 않을테니까요. 하시더라도 '이번만 승인' 하도록 하세요.
축하합니다. 이제 OpenID Consumer 를 만들 준비가 완료되었고 First Test 를 성공적으로 마치셨습니다.
설치 확인을 위해 First Test 에서 작성했던 코드들을 간단히 설명하고, OpenID Provider의 인증 응답 정보를 검사하고 인증 과정을 완료해줄 간단한 서블릿을 만들어 볼 것입니다.
로그인 과정은 다음과 같이 진행됩니다.
JSP 코드의 변동이 없을 경우 1회만 실행되는 <%! %> 블럭 코드를 먼저 이해해야 합니다.
ConsumerManager 클래스는 OpenID Provider 와의 통신을 담당합니다.
Provider 사이트로 이동하기 직전에 ConsumerManager.authenticate 메서드로 인증 요청을 기억해뒀다가
Provider 사이트에서 인증 과정을 마치고 다시 돌아오면 ConsumerManager.verify 메서드로 인증 성공 메시지를 검증합니다.
그러기 위해서는 ConsumerManager 인스턴스의 라이프 싸이클이 page request / response 로 끝나버리면 안되겠지요?
만약 생성한 ConsumerManager 객체가 page scope 라면 Provider 사이트의 인증이 끝나고 돌아왔을 때 verify 가 동작하지 않는 끔찍한 경험을 할 수 있습니다.
<%!
private static ConsumerManager mgr = null;
static {
try {
mgr = new ConsumerManager();
mgr.setAssociations( new InMemoryConsumerAssociationStore() );
mgr.setNonceVerifier( new InMemoryNonceVerifier(60*60) );
} catch( Exception e ) { throw new RuntimeException(e); }
}
%>
그렇기 때문에 ConsumerManager를 static 으로 선언하고 1번만 생성해서 계속 사용할 수 있도록 한 것입니다.
그 다음 보이는 setAssociations 와 setNonceVerifier 가 궁금하실 겁니다.
예제 코드에서 ConsumerManager 생성 및 associate, nonce 설정 부분을 예외처리 한 것은 ConsumerManager 클래스 생성자에서 ConsumerManager를 던지기 때문이며, 별다른 이유는 없습니다.
<%! %> 블럭에 넣은 것이 아무래도 안좋게 보이실텐데요. First Test 를 쉽게 할 수 있게 억지로(?) 한 것 뿐입니다.
간단하게 이 부분을 깔끔하게 만드는 방법이 있습니다. ServletContextListener 를 구현하여 Web application 이 로드될 때 ConsumerManager를 생성하고 그것을 Application Scope 로 보존하면 다른 모든 JSP 페이지에서 편하게 접근하게 할 수 있겠지요.
ServletContextListener를 구현하여 ConsumerManager를 생성하고 application scope 에 보관하는 클래스입니다.
import javax.servlet.*;
import org.openid4java.consumer.*;
public class MyContextListener implements ServletContextListener {
private ConsumerManager manager = null;
public MyContextListener() {}
public void contextInitialized( ServletContextEvent sce ) {
try {
manager = new ConsumerManager();
manager.setAssociations( new InMemoryConsumerAssociationStore() );
manager.setNonceVerifier( new InMemoryNonceVerifier(60*60) );
sce.getServletContext().setAttribute( "openid.consumer.manager", manager );
} catch( ConsumerException e ) {
System.err.println( "Cannot create a ConsumerManager instance! oops.." );
System.exit(1);
}
}
public void contextDestroyed( ServletContextEvent sce ) {}
}
위 코드에서는 ConsumerManager를 application scope 에 openid.consumer.manager 란 이름으로 저장해놨으니, 이제 JSP 페이지에서는 application.getAttribute("openid.consumer.manager") 코드를 통해 여기서 생성한 ConsumerManager 인스턴스를 접근할 수 있게 됩니다.
이제 이 리스너 클래스를 web.xml 파일에 등록해줄 차례입니다. 패키지명은 없다고 가정하겠습니다.
<web-app ....>
...
<listener>
<listener-class>MyContextListener</listener-class>
</listener>
...
</web-app>
이제 다시 test.jsp 페이지의 소스코드로 돌아가 보겠습니다.
위의 과정을 모두 마치셨다면 /test.jsp 에서 <%! %> 블럭을 제거하고 consumer manager를 application scope 에서 가져오도록 코드를 수정하여 테스트 해보시는 것도 재미있을 겁니다.
String returnURL = "http: //localhost: 8080/openid";
String trustRoot = "http://localhost:8080/";
String userInput = "http: //rath.myid.net";
Identifier identifier = Discovery.parseIdentifier(userInput);
userInput = identifier.getIdentifier();
List discoveries = mgr.discover(userInput);
DiscoveryInformation discovered = mgr.associate(discoveries);
session.setAttribute("openid-discover", discovered);
AuthRequest auth = mgr.authenticate(discovered, returnUrl, trustRoot);
response.sendRedirect( auth.getDestinationUrl(true) );discover
returnURL은 OpenID Provider 에서 인증이 끝난 후 인증 응답을 검증하러 올 주소입니다. /verify.jsp 등으로 주소를 대체할 수도 있습니다. ConsumerManager만 공유된다면 서로 물리적으로 다른 서버가 Verify 를 수행해도 관계가 없습니다.
trustRoot는 이 인증이 미칠 범위를 설정합니다. url pattern 형식이 들어가는데, 도메인 주소가 foo.bar 일 경우 http://*.foo.bar/ 로 지정할 수도 있습니다. 이럴 경우 a.foo.bar b.foo.bar 모두 승인하겠다는 의미가 되며 물론 trustRoot는 returnUrl의 상위개념이 되어야만 합니다.
userInput은 사용자가 입력한 OpenID 입니다. test.jsp 코드에서는 테스트의 편의상 사용자의 OpenID 주소를 직접 하드코딩 한 것 뿐입니다.
실제 상황에서는 HTML form 으로 입력받아 submit 한 것을 받아 처리해야할 것 입니다.
Discovery.parseIdentifier 는 사용자가 입력한 String 형태의 OpenID 주소를 정형화된 Identifier 객체로 parse 해줍니다. 만약 사용자가 앞부분의 http: .. 를 입력하지 않고 rath.myid.net 만 입력했다 할지라도, parseIdentifier 메서드는 성공적으로 수행되어 Identifier.getIdentifier()를 불러 정형화된 String 형태의 http: //rath.myid.net 를 받아 다시 userInput 에 넣게 됩니다. 만약 사용자가 항상 정확한 OpenID 주소를 입력한다고 가정했을 경우
Identifier identifier = Discovery.parseIdentifier(userInput);
userInput = identifier.getIdentifier();
위 코드는 필요없어집니다.
그 다음에는 ConsumerManager.discover 메서드와 associate 메서드를 사용하여 DiscoveryInformation 얻어낸 뒤, 이 객체를 세션이 잠시 보관합니다.
세션에 저장할 이름은 마음대로 지정하셔도 됩니다. 여기서 지정한 이름을 Verifier 에서 동일하게 사용해주시기만 하면 됩니다.
마지막으로 Provider 로의 인증 요청 정보를 담고 있는 AuthRequest 객체를 얻어야 합니다. 여기서 ConsumerManager의 역할은 일단락 지어지며, 리턴되어진 AuthRequest 에 getDestinationUrl 메서드를 통해 리다이렉트 해야할 OpenID Provider 의 주소를 얻어옵니다.
이제 그 주소로 HttpResponse.sendRedirect 메서드를 통해 OpenID Provider 사이트로 이동하게 됩니다!
마지막으로 인증 요청시 넣었던 Return URL을 제공해야 합니다. 여기서 다루었던 test.jsp 에서의 http: //localhost: 8080/openid 를 실제로 만드는 작업이지요.
일단 web.xml 에서 다음과 같이 servlet-mapping 으로 url /openid 를 등록합니다.
<servlet>
<servlet-name>OpenID/Verify</servlet-name>
<servlet-class>test.VerifyResponse</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>OpenID/Verify</servlet-name>
<url-pattern>/openid</url-pattern>
</servlet-mapping>
* 따로 "/" 에서 검증 작업을 하실 분은 이 작업이 필요없습니다.
주의하실 부분은 굵게 표시된 url-pattern 부분만 주의해서 작업하시면 됩니다.
이렇게 web.xml 에 추가를 마치고 나면 /openid 요청을 test.VerifyResponse 서블릿이 처리하게 됩니다.
VerifyResponse.java
package test;
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.*;
import org.openid4java.consumer.*;
import org.openid4java.discovery.*;
import org.openid4java.message.*;
public class VerifyResponse extends HttpServlet {
public void service( HttpServletRequest req, HttpServletResponse res )
throws ServletException, IOException {
ConsumerManager manager = (ConsumerManager)
getServletContext().getAttribute("openid.consumer.manager");
ParameterList paramList = new ParameterList( req.getParameterMap() );
DiscoveryInformation di = (DiscoveryInformation)req.getSession().
getAttribute("openid-discover");zzzzzzzzzzzz
if( di==null )
return;
String receiveURL = req.getRequestURL() + "?" + req.getQueryString();
VerificationResult verification = null;
try {
verification = manager.verify(receiveURL, paramList, di);
if( verification!=null ) {
Identifier id = verification.getVerifiedId();
if( id!=null ) { // Consumer에 AuthRequest가 없었을 경우 id가 null일 수 있음.
// Cookie 발급이나 인증세션 초기화 등의 작업은 여기서 처리한다.
// 여기서는 코드의 간결함을 위해 Session에 넣었다.
req.getSession().setAttribute("user.openid", id.getIdentifier());
}
}
} catch( Exception e ) {
// AssociationException, DiscoveryException, MessageException
e.printStackTrace();
}
res.sendRedirect("/대문");
}
}
OpenID Provider 에서의 인증이 완료되면 결과를 처리하는 위의 /openid 서블릿이 호출됩니다.
소스코드는 예외처리와 형식상 어쩔 수 없이 해야하는 것들 (세션 접근, application scope 접근) 때문에 조금 길어졌지만 실제로 체크해야 하는 부분은
진하게 파란색으로 표시된 manager.verify 부분입니다.
먼저 Application scope 에서 ConsumerManager 를 얻어온 후, verify 메서드를 수행하는데 이것이 가장 핵심 부분입니다.
그럼 ConsumerManager.verify 에 넘겨지는 파라미터들을 순서대로 살펴보도록 하지요.
그 결과 VerificationResult 객체를 얻게 되고, 얻은 객체의 VerifiedId() 가 성공적으로 얻어온 경우, 인증 과정이 성공적으로 이루어진 것입니다.
String 형태의 OpenID 만 필요하다면 위의 코드처럼 VerificiationResult.getVerifiedId().getIdentifier() 를 불러 http: //rath.myid.net 처럼 사용자의 OpenID를 String 형태로 얻어올 수 있습니다.
이제 확인되고 검증된 OpenID 로 쿠키를 발급하거나 세션값을 넣어주는 등 여러가지 인증 직후 처리해야 할 일들을 처리하시면 됩니다!
예제 코드에서는 로그인이 끝났으니 user.openid 란 세션키로 OpenID 를 넣어준 후 대문 페이지로 리다이렉트 시키게 했습니다.
축하드립니다. 머나먼 여정이 끝났습니다.
java-openid-sxip 0.9.2를 통해 OpenID Consumer 를 만들어보는 것에 대한 짧은 노트를 다 읽으셨습니다.
확장성이 고려되야하는 부분은 ConsumerAssociationStore 와 NonceVerifier 를 재구현하여 '거의' 해결할 수 있습니다.
이 페이지에서 설명한 것은 OpenID 1.0 을 기준으로 만들어졌으며, 아직 Draft 상태인 OpenID 2.0 에 대한 내용은 다루지 않았습니다.
이 가이드라인의 피드백을 원합니다. 만약 Java 로 OpenID Consumer 사이트에 관심있는 분들이 많을 경우
AxMessage (Attribute Exchange Implementation) 를 이용하는 방법에 대해 조금 더 알아보고 공유해볼까 합니다.
OpenID Consumer 서비스가 많이 생겼으면 좋겠습니다 : -)