原文链接:SMART on FHIR – adding OAuth2
相关文章链接:SMART on FHIR: Part1英文版
相关文章链接:SMART on FHIR: Part 1中文版
在SMART on FHIR: Part 1中文版 基础上 ,利用SMART版本 的OAuth2 标准为其提供安全保障.这里还是沿用 HAPI开源库 、Tomcat 和IntelliJ IDEA IDE.
这里的应用场景是假设我是已经登录了电子病历/区域平台的用户,我想利用一些外部的应用程序来实现一些其他功能,比如调用SMART团队开发的儿童生存曲线的APP。
这是一个web应用程序,发布在Tomcat上,其中暴露了FHIR data endpoints 和 OAuth endpoints . SMART app部署在其他服务器上,在我们的页面上以iFrame的形式来调用. Tomcat server 上的数据可以存储在本地,但是这里调用的其他外部的FHIR服务器,其实就相当于外部FHIR服务器的一个代理。
使用 Maven 来管理项目代码.安装配置略。
在IDE中创建一个 “maven-archetype-webapp” 的maven项目.
由于SMART客户端是一个运行在其他domain的JavaScript程序,对于我们的服务器而言,需要允许它访问我们的数据,为此需要添加对 CORS 的支持,按照HAPI中的说明 ,只需在pom.xml 添加dependency 来下载filter即可,修改web.xml file,修改其中的权限,可以直接从HAPI官网上的例子中复制.
接下来考虑如何SMART进行集成. SMART服务器端快速入门提供了很详细的资料 – 总结一下:
在EMR web 界面上, 新建一个 iframe, 将其指向 SMART应用程序, 进行一些基本的配置即可. 比如
SMART 应用程序从服务器上加载一致性声明/conformance statement,这样就知道如何获取授权以及token的endpoint
下一步构建 Authorization and Token end points, 告知SMART 应用程序如何找到它们。 利用对Conformance资源的扩展可以实现.
首先:
在我们演示用的电子病历系统中新增一个登录功能,用户发送登录信息到login endpoint,对登录信息进行校验,创建一个user token并保存在一个context对象当中,同时将其发送给本地的APP ,这样在启动时即可使用这个user token。由于系统用户同时也需要访问一些受限的资源,用户也会得到一个access token。
Login servlet示例:
@WebServlet(urlPatterns= {"/auth/login"}, displayName="Login").
public class Login extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("login");
PrintWriter out = response.getWriter();
//Here is where we check and validate username & password. We're going to cheat right now...
//create a user object. This would ultimately be a FHIR obect I suspect...
Person person = new Person();
person.userName = request.getParameter("username");
person.userToken = java.util.UUID.randomUUID().toString(); //generate a user token
//save the user details in the context - we previously created this map...
ServletContext context = getServletContext();
Map<String,Person> usertokens = (Map<String,Person>) context.getAttribute("usertokens");
//save the access token for later use - in production persistent store...
usertokens.put(person.userToken,person);
//create an access token for this person ...
Map<String,JsonObject> oauthtokens = (Map<String,JsonObject>) context.getAttribute("oauthtokens");
JsonObject json = Json.createObjectBuilder()
.add("access_token", person.userToken)
.add("token_type", "bearer")
.add("expires_in", 3600)
.add("scope", "patient/*.read")
.build();
oauthtokens.put(person.userToken,json);
response.addHeader("Content-Type","application/json+fhir");
out.println(person.getJson().toString());
}
}
新建一个end-point来启动SMART app 如下:
@WebServlet(urlPatterns= {"/auth/launch"}, displayName="Launch SMART application")
public class Launch extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("launch endpoint accessed");
String userToken = request.getParameter("usertoken");
String patientId = request.getParameter("patientid");
//make sure this is a logged in person (the have a valid token)
ServletContext context = getServletContext();
Map<String,Person> usertokens = (Map<String,Person>) context.getAttribute("usertokens");
if (usertokens.containsKey(userToken)) {
//yep, this is a valid user...
//retrieve the user object and update with the patient they have in context. We'll need this for the access token...
Person person = (Person) usertokens.get(userToken);
person.currentPatientId = patientId;
//the re-direct URL. In reality the url and 'iss' would come from config...
String url = "https://fhir.smartplatforms.org/apps/growth-chart/launch.html?";
url += "iss=http://localhost:8081/fhir";
//we'll use the user token as the launch token as we can use that to validate the Auth call..
url += "&launch=" + userToken;
response.sendRedirect(url);
} else {
response.setStatus(403); //forbidden.
PrintWriter out = response.getWriter();
out.println("<html><head></head><body><h1>User not logged in</h1></body></html>");
}
}
}
接下来是authorization end-point. 这里用用户token做启动token来确保请求方的可信,重定向到 redirect_url.
代码如下
@WebServlet(urlPatterns= {"/auth/authorize"}, displayName="Authorize endpoint for FHIR Server")
public class Authorize extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("auth check...");
String response_type = request.getParameter("response_type");
String client_id = request.getParameter("client_id"); //the id of the client
String redirect_uri = request.getParameter("redirect_uri");
String scope = request.getParameter("scope"); //what the app wants to do
String state = request.getParameter("state");
//the scope parameter includes the launch token - eg patient/*.read launch:7bceb3c6-66e9-46c9-8efd-9f87e76a5f9a
//so we would pull out both scope and token, check that the token matches the one we set (actually the patient token)
//and that the scope is acceptable to us. Should move this to a function somewhere...
String[] arScopes = scope.split(" ");
String launchToken = "";
for (int i = 0; i < arScopes.length; i++){
System.out.println(arScopes[i]);
if (arScopes[i].substring(0,7).equals("launch:")) {
launchToken = arScopes[i].substring(7);
}
}
ServletContext context = getServletContext();
Map<String,Person> usertokens = (Map<String,Person>) context.getAttribute("usertokens");
if (usertokens.containsKey(launchToken)) {
//we'll assume that the user is OK with this scope, but this is where we can check...
//so, now we create an auth_code and re-direct to the redirect_url...
String auth_code = java.util.UUID.randomUUID().toString();
//we'll save the auth code in a previously defined context variable. In real life you'd use a
//persistent store of some type, and likely save more details...
Map<String,Person> oauthcodes = (Map<String,Person>) context.getAttribute("oauthcodes");
Person person = (Person) usertokens.get(launchToken);
oauthcodes.put(auth_code,person);
//and re-direct to the 'authenticated' endpoint of the application
response.sendRedirect(redirect_uri + "?code="+auth_code+ "&state="+state);
} else {
response.setStatus(403); //forbidden.
}
}
}
我们可以使用一个注册过程来存储应用程序的ID和回调URL来进一步确保安全性
这样我们就可以交换授权token的授权码。 如下是Token endpoint的范例:
@WebServlet(urlPatterns= {"/auth/token"}, displayName="Token endpoint for FHIR Server")
public class Token extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
String code = request.getParameter("code");
//the map containing auth_code that was set during the authorization phase...
ServletContext context = getServletContext();
Map<String,Person> oauthcodes = (Map<String, Person>) context.getAttribute("oauthcodes");
//is this a valid access code?
if (oauthcodes.containsKey(code)) {
Person person = (Person) oauthcodes.get(code);
String access_token = java.util.UUID.randomUUID().toString();
JsonObject json = Json.createObjectBuilder()
.add("access_token", access_token)
.add("patient", person.currentPatientId)
.add("token_type", "bearer")
.add("expires_in", 3600)
.add("scope", "patient/*.read")
.build();
response.addHeader("Content-Type","application/json+fhir");
//save the access token for later use - like the codes, you'd use a persistent store...
Map<String,JsonObject> oauthtokens = (Map<String,JsonObject>) context.getAttribute("oauthtokens");
oauthtokens.put(access_token,json);
//and return the token to the applciation
out.println(json.toString());
} else {
//the auth codes don't match.
response.setStatus(403); //forbidden.
out.println("{}");
}
}
}
有了auth token之后就可以调用实际的FHIR endpoint.
@Override
public void handleRequest(SearchMethodBinding.RequestType theRequestType,
javax.servlet.http.HttpServletRequest theRequest,
javax.servlet.http.HttpServletResponse theResponse)
throws javax.servlet.ServletException,
IOException {
String uri = theRequest.getRequestURI();
//anyone can access metadata...
if (uri.equals("/fhir/metadata")) {
super.handleRequest(theRequestType,theRequest,theResponse);
} else {
//but you need to be authorized to access clincial data...
String auth = theRequest.getHeader("Authorization");
if (auth != null){
auth = auth.substring(7);//get rid of the 'Bearer ' at the front
ServletContext context = getServletContext();// request. .setAttribute("oauthtokens", oauthtokens);
Map<String,JsonObject> oauthtokens = (Map<String,JsonObject>) context.getAttribute("oauthtokens");
if (oauthtokens.containsKey(auth)) {
//we could pull out the actual access token, and apply security logic there...
super.handleRequest(theRequestType,theRequest,theResponse);
} else {
theResponse.setStatus(403); //forbidden.
}
} else {
theResponse.setStatus(403); //forbidden.
}
}
}
成功之后就会出现下图的效果 有点丑
后续也应该考虑如何判断 Auth tokens是否过期。