Java Web应用程序中的Azure AD SSO,ADFS SSO配置
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/