Java Web应用程序中的Azure AD SSO,ADFS SSO配置

时间:2020-02-23 14:33:56  来源:igfitidea点击:

Azure AD单点登录

单点登录功能由于其显着的优势而在开发多面应用程序的同时,在处理应用程序访问管理的开发人员中越来越受欢迎。
Azure Active Directory是用于目录管理,应用程序访问管理和安全身份管理的最受欢迎的解决方案之一,该解决方案还提供了有效的单一登录功能。

最近,我将Azure AD SSO与Java Web应用程序集成在一起,并将其与现有的Identity Management系统同步。
我使用Active Directory联合身份验证服务ADFS2015。
在此教程 中,我将在三个部分中共享集成过程。

  • 如何在Azure AD中注册Java应用程序
  • 如何在Java应用程序中实现ADAL库
  • 单点登录SSO的ADFS配置

如何在Azure AD中注册Java应用程序

按照下面的链接中给出的说明,使用Java Active Directory身份验证库(ADAL4J)向Azure AD注册Java应用程序,并获取JWT访问令牌。

https://docs.microsoft.com/zh-cn/azure/active-directory/develop/active-directory-integrating-applications

如何在Java Web应用程序中实现ADAL库

在Java应用程序中实现ADAL库的过程总共包括六个步骤:

  • 添加ADAL库依赖关系
  • 注册ADALFilter并在web.xml中添加上下文参数(向Java App通知Azure App配置)
  • 创建AdalFilter
  • 创建安全控制器
  • 创建AuthHelper类
  • 创建aad.jsp

将adal库依赖项添加到Java应用程序

<dependency>
	<groupId>com.microsoft.azure</groupId>
	<artifactId>adal4j</artifactId>
	<version>1.1.1</version>
</dependency>
<dependency>
	<groupId>com.nimbusds</groupId>
	<artifactId>oauth2-oidc-sdk</artifactId>
	<version>4.5</version>
</dependency>

注册ADALFilter并在web.xml中添加上下文参数

<context-param>
	<param-name>authority</param-name>
	<param-value>https://login.windows.net/</param-value>
</context-param>
<context-param>
	<param-name>tenant</param-name>
	<param-value>YOUR_TENANT_NAME</param-value>
</context-param>

<filter>
	<filter-name>AdalFilter</filter-name>
	<filter-class>com.azilen.aad.AdalFilter</filter-class>
	<init-param>
		<param-name>client_id</param-name>
		<param-value><YOUR-CLIENT-ID></param-value>
	</init-param>
	<init-param>
		<param-name>secret_key</param-name>
		<param-value><YOUR-SECRET-KEY></param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name> AdalFilter </filter-name>
	<url-pattern>/secure/*</url-pattern>
</filter-mapping>

按照上一步中的说明,在注册应用程序时将以下值替换为从Azure门户获得的实际值。

YOUR-CLIENT-ID和YOUR-SECRET-KEY(授权)(是您的天蓝色AD的登录页面),YOUR_TENANT_NAME是组织名称。

创建AdalFilter

AdalFilter会检查当前会话是否存储了有效的" PRINCIPAL_SESSION_NAME",否则,它将重定向到Azure登录页面(Authority)。

public class AdalFilter implements Filter {

	private String clientId = "";
	private String clientSecret = "";
	private String tenant = "";
	private String authority;

	public void destroy() {

	}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

		if (request instanceof HttpServletRequest) {
			HttpServletRequest httpRequest = (HttpServletRequest) request;
			HttpServletResponse httpResponse = (HttpServletResponse) response;
			try {

				String currentUri = request.getScheme() + "://" + request.getServerName()
						+ ("http".equals(request.getScheme()) && request.getServerPort() == 80
								|| "https".equals(request.getScheme()) && request.getServerPort() == 443 ? "" : ":" + request.getServerPort())
						+ httpRequest.getRequestURI();
				String fullUrl = currentUri + (httpRequest.getQueryString() != null ? "?" + httpRequest.getQueryString() : "");
				//check if user has a session
				if (!AuthHelper.isAuthenticated(httpRequest)) {
					if (AuthHelper.containsAuthenticationData(httpRequest)) {
						Map<String, String> params = new HashMap<String, String>();
						for (String key : request.getParameterMap().keySet()) {
							params.put(key, request.getParameterMap().get(key)[0]);
						}
						AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(fullUrl), params);
						if (AuthHelper.isAuthenticationSuccessful(authResponse)) {

							AuthenticationSuccessResponse oidcResponse = (AuthenticationSuccessResponse) authResponse;
							AuthenticationResult result = getAccessToken(oidcResponse.getAuthorizationCode(), currentUri);
							createSessionPrincipal(httpRequest, result);
						} else {
							AuthenticationErrorResponse oidcResponse = (AuthenticationErrorResponse) authResponse;
							throw new Exception(String.format("Request for auth code failed: %s - %s", oidcResponse.getErrorObject().getCode(),
									oidcResponse.getErrorObject().getDescription()));
						}
					} else {
						//not authenticated
						httpResponse.setStatus(302);
						httpResponse.sendRedirect(getRedirectUrl(currentUri));
						return;
					}
				} else {
					//if authenticated, how to check for valid session?
					AuthenticationResult result = AuthHelper.getAuthSessionObject(httpRequest);

					if (httpRequest.getParameter("refresh") != null) {
						result = getAccessTokenFromRefreshToken(result.getRefreshToken(), currentUri);
					} else {
						if (httpRequest.getParameter("cc") != null) {
							result = getAccessTokenFromClientCredentials();
						} else {
							if (result.getExpiresOnDate().before(new Date())) {
								result = getAccessTokenFromRefreshToken(result.getRefreshToken(), currentUri);
							}
						}
					}
					createSessionPrincipal(httpRequest, result);
				}
			} catch (Throwable exc) {
				httpResponse.setStatus(500);
				request.setAttribute("error", exc.getMessage());
				httpResponse.sendRedirect(((HttpServletRequest) request).getContextPath() + "/error.jsp");
			}
		}
		chain.doFilter(request, response);
	}

	private AuthenticationResult getAccessTokenFromClientCredentials() throws Throwable {
		AuthenticationContext context = null;
		AuthenticationResult result = null;
		ExecutorService service = null;
		try {
			service = Executors.newFixedThreadPool(1);
			context = new AuthenticationContext(authority + tenant + "/", true, service);
			Future<AuthenticationResult> future = context.acquireToken("https://graph.windows.net", new ClientCredential(clientId, clientSecret),
					null);
			result = future.get();
		} catch (ExecutionException e) {
			throw e.getCause();
		} finally {
			service.shutdown();
		}	

		if (result == null) {
			throw new ServiceUnavailableException("authentication result was null");
		}
		return result;
	}

	private AuthenticationResult getAccessTokenFromRefreshToken(String refreshToken, String currentUri) throws Throwable {
		AuthenticationContext context = null;
		AuthenticationResult result = null;
		ExecutorService service = null;
		try {
			service = Executors.newFixedThreadPool(1);
			context = new AuthenticationContext(authority + tenant + "/", true, service);
			Future<AuthenticationResult> future = context.acquireTokenByRefreshToken(refreshToken, new ClientCredential(clientId, clientSecret), null,
					null);
			result = future.get();
		} catch (ExecutionException e) {
			throw e.getCause();
		} finally {
			service.shutdown();
		}

		if (result == null) {
			throw new ServiceUnavailableException("authentication result was null");
		}
		return result;

	}

	private AuthenticationResult getAccessToken(AuthorizationCode authorizationCode, String currentUri) throws Throwable {
		String authCode = authorizationCode.getValue();
		ClientCredential credential = new ClientCredential(clientId, clientSecret);
		AuthenticationContext context = null;
		AuthenticationResult result = null;
		ExecutorService service = null;
		try {
			service = Executors.newFixedThreadPool(1);
			context = new AuthenticationContext(authority + tenant + "/", true, service);
			Future<AuthenticationResult> future = context.acquireTokenByAuthorizationCode(authCode, new URI(currentUri), credential, null);
			result = future.get();
		} catch (ExecutionException e) {
			throw e.getCause();
		} finally {
			service.shutdown();
		}

		if (result == null) {
			throw new ServiceUnavailableException("authentication result was null");
		}
		return result;
	}

	private void createSessionPrincipal(HttpServletRequest httpRequest, AuthenticationResult result) throws Exception {
		httpRequest.getSession().setAttribute(AuthHelper.PRINCIPAL_SESSION_NAME, result);
	}

	private String getRedirectUrl(String currentUri) throws UnsupportedEncodingException {
		String redirectUrl = authority + this.tenant
				+ "/oauth2/authorize?response_type=code%20id_token&scope=openid&response_mode=form_post&redirect_uri="
				+ URLEncoder.encode(currentUri, "UTF-8") + "&client_id=" + clientId + "&resource=https%3a%2f%2fgraph.windows.net" + "&nonce="
				+ UUID.randomUUID() + "&site_id=500879";
		return redirectUrl;
	}

	public void init(FilterConfig config) throws ServletException {
		clientId = config.getInitParameter("client_id");
		authority = config.getServletContext().getInitParameter("authority");
		tenant = config.getServletContext().getInitParameter("tenant");
		clientSecret = config.getInitParameter("secret_key");
	}

}

创建安全控制器

@Controller
@RequestMapping("/secure/aad")
public class AadController { 
@RequestMapping(method = { RequestMethod.GET, RequestMethod.POST })
public String getDirectoryObjects(ModelMap model, HttpServletRequest httpRequest) {
	HttpSession session = httpRequest.getSession();
	log.info("session: " + session);
	AuthenticationResult result = (AuthenticationResult) session.getAttribute(AuthHelper.PRINCIPAL_SESSION_NAME);
	if (result == null) {
		model.addAttribute("error", new Exception("AuthenticationResult not found in session."));
		return "/error";
	} else {
		try {
			log.info("JWT token details:-");
			JWT jwt = JWTParser.parse(result.getIdToken());
			for (String key : jwt.getJWTClaimsSet().getAllClaims().keySet()) {
				log.info(key + ":" + jwt.getJWTClaimsSet().getAllClaims().get(key));
			}
			model.addAttribute("user", jwt.getJWTClaimsSet().getStringClaim("unique_name"));
		} catch (ParseException e) {
			log.error("Exception:", e);
		}
	}
	return "/secure/aad";
}
}

创建AuthHelper类

public final class AuthHelper {

	public static final String PRINCIPAL_SESSION_NAME = "principal";

	private AuthHelper() {
	}

	public static boolean isAuthenticated(HttpServletRequest request) {
		return request.getSession().getAttribute(PRINCIPAL_SESSION_NAME) != null;
	}

	public static AuthenticationResult getAuthSessionObject(HttpServletRequest request) {
		return (AuthenticationResult) request.getSession().getAttribute(PRINCIPAL_SESSION_NAME);
	}

	public static boolean containsAuthenticationData(HttpServletRequest httpRequest) {
		Map<String, String[]> map = httpRequest.getParameterMap();
		return (httpRequest.getMethod().equalsIgnoreCase("POST") 
					|| httpRequest.getMethod().equalsIgnoreCase("GET")) 
				&& (httpRequest.getParameterMap().containsKey(AuthParameterNames.ERROR)
						|| httpRequest.getParameterMap().containsKey(AuthParameterNames.ID_TOKEN)
						|| httpRequest.getParameterMap().containsKey(AuthParameterNames.CODE));
	}

	public static boolean isAuthenticationSuccessful(AuthenticationResponse authResponse) {
		return authResponse instanceof AuthenticationSuccessResponse;
	}
}

创建一个aad.jsp

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>AAD Secure Page</title>
</head>
<body>

	<p>Welcome, ${user}</p>

	<ul>
		<li><a href="<%=request.getContextPath()%>/index.jsp">Go Home</a></li>
	</ul> 
</body>
</html>

Azure AD SSO Java Web应用程序测试

下图显示了身份验证成功后的Azure AD登录页面和成功页面。

您可以从我们的GitHub存储库下载完整的项目。

单点登录(SSO)的ADFS配置

要配置ADFS 2015,首先请确保该应用程序在SSL上运行。
配置ADFS以启用Azure AD与现有Identity Management系统之间的同步的过程包括三个步骤:

  • 添加依赖方信任
  • 添加客户端ID
  • 在ADFS服务器的PowerShell中执行Authority命令

添加依赖方信任

  • 在"欢迎"页面上,选择"声明感知"

  • 在"选择数据源"上,选择"手动输入有关依赖方的数据"

  • 提供任何显示名称

  • 配置证书添加自签名证书

  • 配置URL:根据您的ADFS协议选择

  • 选中"启用对WS-Federation Passive协议的支持"复选框。
    在"依赖方WS-Federation被动协议URL"下,键入此依赖方信任的URL。

  • 选中"启用对SAML 2.0 WebSSO协议的支持"复选框。
    在"依赖方2.0 SSO服务URL"下,为此依赖方信任键入安全性声明标记语言(SAML)服务终结点URL。

  • 配置标识符:在依赖方信任标识符中添加您的应用程序URL。

  • 选择访问控制策略:选择全部允许(根据您的环境选择)

  • 准备添加信任:查看此设置

  • 完。

参考:https://docs.microsoft.com/zh-cn/windows-server/identity/ad-fs/operations/create-a-relying-party-trust

使用以下命令添加客户端ID

Add-ADFSClient -Name "SampleApplication" -ClientId "<CLIENTID>" -RedirectUri @("REDIRECTURI") -Description "OAuth 2.0 client for our Test application"

参考:https://docs.microsoft.com/zh-cn/powershell/module/adfs/add-adfsclient?view=win10-ps

在ADFS服务器的PowerShell中执行以下Authority命令

Grant-ADFSApplicationPermission -ClientRoleIdentifier "<CLIENTID>" -ServerRoleIdentifier "<AUTHORITY>" -ScopeNames "allatclaims","openid"
AUTHORITY eg: https://login.windows.net/