From 3dccb9023b96365f74afaae83e0596ff045a694d Mon Sep 17 00:00:00 2001 From: thinkgem Date: Wed, 7 Jul 2021 12:28:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9ELDAP=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jeesite/common/shiro/authc/LdapToken.java | 26 ++ .../common/shiro/filter/LdapFilter.java | 38 +++ .../shiro/realm/LdapAuthorizingRealm.java | 317 ++++++++++++++++++ .../jeesite/modules/config/ShiroConfig.java | 31 +- .../main/resources/config/jeesite-core.yml | 5 + .../resources/views/modules/sys/sysLogin.html | 5 +- 6 files changed, 419 insertions(+), 3 deletions(-) create mode 100644 modules/core/src/main/java/com/jeesite/common/shiro/authc/LdapToken.java create mode 100644 modules/core/src/main/java/com/jeesite/common/shiro/filter/LdapFilter.java create mode 100644 modules/core/src/main/java/com/jeesite/common/shiro/realm/LdapAuthorizingRealm.java diff --git a/modules/core/src/main/java/com/jeesite/common/shiro/authc/LdapToken.java b/modules/core/src/main/java/com/jeesite/common/shiro/authc/LdapToken.java new file mode 100644 index 00000000..b82e41b5 --- /dev/null +++ b/modules/core/src/main/java/com/jeesite/common/shiro/authc/LdapToken.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2013-Now http://jeesite.com All rights reserved. + */ +package com.jeesite.common.shiro.authc; + +import java.util.Map; + +/** + * LdapToken + * @author ThinkGem + * @version 2021-7-6 + */ +public class LdapToken extends FormToken { + + private static final long serialVersionUID = 1L; + + public LdapToken() { + super(); + } + + public LdapToken(String username, char[] password, boolean rememberMe, + String host, Map params) { + super(username, password, rememberMe, null, host, params); + } + +} diff --git a/modules/core/src/main/java/com/jeesite/common/shiro/filter/LdapFilter.java b/modules/core/src/main/java/com/jeesite/common/shiro/filter/LdapFilter.java new file mode 100644 index 00000000..bbe8303d --- /dev/null +++ b/modules/core/src/main/java/com/jeesite/common/shiro/filter/LdapFilter.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2013-Now http://jeesite.com All rights reserved. + */ +package com.jeesite.common.shiro.filter; + +import java.util.Map; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.shiro.authc.AuthenticationToken; + +import com.jeesite.common.shiro.authc.LdapToken; +import com.jeesite.common.web.http.ServletUtils; + +/** + * LDAP过滤器 + * @author ThinkGem + * @version 2021-7-6 + */ +public class LdapFilter extends FormFilter { + + @Override + protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { + String username = getUsername(request, response); // 用户名 + String password = getPassword(request); // 登录密码 + boolean rememberMe = isRememberMe(request); // 记住我(自动登录) + String host = getHost(request); // 登录主机 + Map paramMap = ServletUtils.getExtParams(request); // 登录附加参数 + return new LdapToken(username, password.toCharArray(), rememberMe, host, paramMap); + } + + @Override + protected boolean isLoginRequest(ServletRequest request, ServletResponse response) { + return true; + } + +} diff --git a/modules/core/src/main/java/com/jeesite/common/shiro/realm/LdapAuthorizingRealm.java b/modules/core/src/main/java/com/jeesite/common/shiro/realm/LdapAuthorizingRealm.java new file mode 100644 index 00000000..8bf73ba4 --- /dev/null +++ b/modules/core/src/main/java/com/jeesite/common/shiro/realm/LdapAuthorizingRealm.java @@ -0,0 +1,317 @@ +/** + * Copyright (c) 2013-Now http://jeesite.com All rights reserved. + */ +package com.jeesite.common.shiro.realm; + +import javax.naming.AuthenticationNotSupportedException; +import javax.naming.NamingException; +import javax.naming.ldap.LdapContext; +import javax.servlet.http.HttpServletRequest; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.ldap.UnsupportedAuthenticationMechanismException; +import org.apache.shiro.realm.ldap.DefaultLdapRealm; +import org.apache.shiro.realm.ldap.JndiLdapContextFactory; +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.realm.ldap.LdapUtils; +import org.apache.shiro.session.Session; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.jeesite.common.shiro.authc.FormToken; +import com.jeesite.common.shiro.authc.LdapToken; +import com.jeesite.common.utils.SpringUtils; +import com.jeesite.common.web.http.ServletUtils; +import com.jeesite.modules.sys.entity.Log; +import com.jeesite.modules.sys.entity.User; +import com.jeesite.modules.sys.service.EmpUserService; +import com.jeesite.modules.sys.service.UserService; +import com.jeesite.modules.sys.utils.LogUtils; +import com.jeesite.modules.sys.utils.UserUtils; + +/** + * 系统认证授权实现类 + * @author ThinkGem + * @version 2021-7-6 + */ +public class LdapAuthorizingRealm extends BaseAuthorizingRealm { + + private static final Logger log = LoggerFactory.getLogger(DefaultLdapRealm.class); + + //The zero index currently means nothing, but could be utilized in the future for other substitution techniques. + private static final String USERDN_SUBSTITUTION_TOKEN = "{0}"; + + private String userDnPrefix; + private String userDnSuffix; + + /** + * The LdapContextFactory instance used to acquire {@link javax.naming.ldap.LdapContext LdapContext}'s at runtime + * to acquire connections to the LDAP directory to perform authentication attempts and authorizatino queries. + */ + private LdapContextFactory contextFactory; + + private UserService userService; + private EmpUserService empUserService; + + /** + * Default no-argument constructor that defaults the internal {@link LdapContextFactory} instance to a + * {@link JndiLdapContextFactory}. + */ + public LdapAuthorizingRealm() { + super(); + //Credentials Matching is not necessary - the LDAP directory will do it automatically: + setCredentialsMatcher(new AllowAllCredentialsMatcher()); + //Any Object principal and Object credentials may be passed to the LDAP provider, so accept any token: + setAuthenticationTokenClass(LdapToken.class); + this.contextFactory = new JndiLdapContextFactory(); + } + + @Override + protected FormToken getFormToken(AuthenticationToken authcToken) { + HttpServletRequest request = ServletUtils.getRequest(); + if (authcToken == null){ + return null; + } + LdapToken ldapToken = (LdapToken) authcToken; + + // LDAP 身份认证 + LdapContext ctx = null; + try { + Object principal = getUserDn(ldapToken.getUsername()); + Object credentials = String.valueOf(ldapToken.getPassword()); + log.debug("Authenticating user '{}' through LDAP", principal); + ctx = getContextFactory().getLdapContext(principal, credentials); + } catch (AuthenticationNotSupportedException e) { + throw new UnsupportedAuthenticationMechanismException("msg:LDAP 不支持的授权类型", e); + } catch (javax.naming.AuthenticationException e) { + throw new AuthenticationException("msg:LDAP 授权失败:"+e.getMessage(), e); + } catch (NamingException e) { + throw new AuthenticationException("msg:LDAP 连接失败:"+e.getMessage(), e); + } catch (Exception e) { + throw new AuthenticationException("msg:LDAP 登录失败:"+e.getMessage(), e); + } finally { + LdapUtils.closeContext(ctx); + } + + // 生成登录信息对象 + FormToken token = new FormToken(request); + token.setUsername(ldapToken.getUsername()); + token.setPassword(ldapToken.getPassword()); + token.setParams(ldapToken.getParams()); + return token; + } + + @Override + protected User getUserInfo(FormToken token) { + User user = super.getUserInfo(token); + if (user == null){ + throw new AuthenticationException("msg:用户 “" + token.getUsername() + "” 在本系统中不存在, 请联系管理员."); + } + return user; + } + + @Override + protected void assertCredentialsMatch(AuthenticationToken authcToken, + AuthenticationInfo info) throws AuthenticationException { + // 已经在 getFormToken 认证过了,这里就不验证身份了 + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(LoginInfo loginInfo, Subject subject, Session session, User user) { + return super.doGetAuthorizationInfo(loginInfo, subject, session, user); + } + + @Override + public void onLoginSuccess(LoginInfo loginInfo, HttpServletRequest request) { + super.onLoginSuccess(loginInfo, request); + + //System.out.print("__sid: "+request.getSession().getId()); + //System.out.println(" == "+UserUtils.getSession().getId()); + + // 更新登录IP、时间、会话ID等 + User user = UserUtils.get(loginInfo.getId()); + getUserService().updateUserLoginInfo(user); + + // 记录用户登录日志 + LogUtils.saveLog(user, ServletUtils.getRequest(), "系统登录", Log.TYPE_LOGIN_LOGOUT); + } + + @Override + public void onLogoutSuccess(LoginInfo loginInfo, HttpServletRequest request) { + super.onLogoutSuccess(loginInfo, request); + + // 记录用户退出日志 + User user = UserUtils.get(loginInfo.getId()); + LogUtils.saveLog(user, request, "系统退出", Log.TYPE_LOGIN_LOGOUT); + } + + public UserService getUserService() { + if (userService == null){ + userService = SpringUtils.getBean(UserService.class); + } + return userService; + } + + public EmpUserService getEmpUserService() { + if (empUserService == null){ + empUserService = SpringUtils.getBean(EmpUserService.class); + } + return empUserService; + } + + /** + * Returns the User DN prefix to use when building a runtime User DN value or {@code null} if no + * {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that + * occurs before the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value. + * + * @return the the User DN prefix to use when building a runtime User DN value or {@code null} if no + * {@link #getUserDnTemplate() userDnTemplate} has been configured. + */ + protected String getUserDnPrefix() { + return userDnPrefix; + } + + /** + * Returns the User DN suffix to use when building a runtime User DN value. or {@code null} if no + * {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that + * occurs after the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value. + * + * @return the User DN suffix to use when building a runtime User DN value or {@code null} if no + * {@link #getUserDnTemplate() userDnTemplate} has been configured. + */ + protected String getUserDnSuffix() { + return userDnSuffix; + } + + /** + * Sets the User Distinguished Name (DN) template to use when creating User DNs at runtime. A User DN is an LDAP + * fully-qualified unique user identifier which is required to establish a connection with the LDAP + * directory to authenticate users and query for authorization information. + *

Usage

+ * User DN formats are unique to the LDAP directory's schema, and each environment differs - you will need to + * specify the format corresponding to your directory. You do this by specifying the full User DN as normal, but + * but you use a {@code {0}} placeholder token in the string representing the location where the + * user's submitted principal (usually a username or uid) will be substituted at runtime. + *

+ * For example, if your directory + * uses an LDAP {@code uid} attribute to represent usernames, the User DN for the {@code jsmith} user may look like + * this: + *

+ *

uid=jsmith,ou=users,dc=mycompany,dc=com
+ *

+ * in which case you would set this property with the following template value: + *

+ *

uid={0},ou=users,dc=mycompany,dc=com
+ *

+ * If no template is configured, the raw {@code AuthenticationToken} + * {@link AuthenticationToken#getPrincipal() principal} will be used as the LDAP principal. This is likely + * incorrect as most LDAP directories expect a fully-qualified User DN as opposed to the raw uid or username. So, + * ensure you set this property to match your environment! + * + * @param template the User Distinguished Name template to use for runtime substitution + * @throws IllegalArgumentException if the template is null, empty, or does not contain the + * {@code {0}} substitution token. + * @see LdapContextFactory#getLdapContext(Object,Object) + */ + public void setUserDnTemplate(String template) throws IllegalArgumentException { + if (!StringUtils.hasText(template)) { + return; + } + int index = template.indexOf(USERDN_SUBSTITUTION_TOKEN); + if (index < 0) { + String msg = "User DN template must contain the '" + + USERDN_SUBSTITUTION_TOKEN + "' replacement token to understand where to " + + "insert the runtime authentication principal."; + throw new IllegalArgumentException(msg); + } + String prefix = template.substring(0, index); + String suffix = template.substring(prefix.length() + USERDN_SUBSTITUTION_TOKEN.length()); + if (log.isDebugEnabled()) { + log.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix); + } + this.userDnPrefix = prefix; + this.userDnSuffix = suffix; + } + + /** + * Returns the User Distinguished Name (DN) template to use when creating User DNs at runtime - see the + * {@link #setUserDnTemplate(String) setUserDnTemplate} JavaDoc for a full explanation. + * + * @return the User Distinguished Name (DN) template to use when creating User DNs at runtime. + */ + public String getUserDnTemplate() { + return getUserDn(USERDN_SUBSTITUTION_TOKEN); + } + + /** + * Returns the LDAP User Distinguished Name (DN) to use when acquiring an + * {@link javax.naming.ldap.LdapContext LdapContext} from the {@link LdapContextFactory}. + *

+ * If the the {@link #getUserDnTemplate() userDnTemplate} property has been set, this implementation will construct + * the User DN by substituting the specified {@code principal} into the configured template. If the + * {@link #getUserDnTemplate() userDnTemplate} has not been set, the method argument will be returned directly + * (indicating that the submitted authentication token principal is the User DN). + * + * @param principal the principal to substitute into the configured {@link #getUserDnTemplate() userDnTemplate}. + * @return the constructed User DN to use at runtime when acquiring an {@link javax.naming.ldap.LdapContext}. + * @throws IllegalArgumentException if the method argument is null or empty + * @throws IllegalStateException if the {@link #getUserDnTemplate userDnTemplate} has not been set. + * @see LdapContextFactory#getLdapContext(Object, Object) + */ + protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException { + if (!StringUtils.hasText(principal)) { + throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction."); + } + String prefix = getUserDnPrefix(); + String suffix = getUserDnSuffix(); + if (prefix == null && suffix == null) { + log.debug("userDnTemplate property has not been configured, indicating the submitted " + + "AuthenticationToken's principal is the same as the User DN. Returning the method argument " + + "as is."); + return principal; + } + + int prefixLength = prefix != null ? prefix.length() : 0; + int suffixLength = suffix != null ? suffix.length() : 0; + StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength); + if (prefixLength > 0) { + sb.append(prefix); + } + sb.append(principal); + if (suffixLength > 0) { + sb.append(suffix); + } + return sb.toString(); + } + + /** + * Sets the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication + * attempts and authorization queries. Unless specified otherwise, the default is a {@link JndiLdapContextFactory} + * instance. + * + * @param contextFactory the LdapContextFactory instance used to acquire connections to the LDAP directory during + * authentication attempts and authorization queries + */ + public void setContextFactory(LdapContextFactory contextFactory) { + this.contextFactory = contextFactory; + } + + /** + * Returns the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication + * attempts and authorization queries. Unless specified otherwise, the default is a {@link JndiLdapContextFactory} + * instance. + * + * @return the LdapContextFactory instance used to acquire connections to the LDAP directory during + * authentication attempts and authorization queries + */ + public LdapContextFactory getContextFactory() { + return this.contextFactory; + } + +} diff --git a/modules/core/src/main/java/com/jeesite/modules/config/ShiroConfig.java b/modules/core/src/main/java/com/jeesite/modules/config/ShiroConfig.java index 98e96881..77f61455 100644 --- a/modules/core/src/main/java/com/jeesite/modules/config/ShiroConfig.java +++ b/modules/core/src/main/java/com/jeesite/modules/config/ShiroConfig.java @@ -11,6 +11,7 @@ import javax.servlet.Filter; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.cas.CasSubjectFactory; import org.apache.shiro.realm.Realm; +import org.apache.shiro.realm.ldap.JndiLdapContextFactory; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.web.filter.InvalidRequestFilter; @@ -29,12 +30,14 @@ import com.jeesite.common.shiro.config.FilterChainDefinitionMap; import com.jeesite.common.shiro.filter.CasFilter; import com.jeesite.common.shiro.filter.FormFilter; import com.jeesite.common.shiro.filter.InnerFilter; +import com.jeesite.common.shiro.filter.LdapFilter; import com.jeesite.common.shiro.filter.LogoutFilter; import com.jeesite.common.shiro.filter.PermissionsFilter; import com.jeesite.common.shiro.filter.RolesFilter; import com.jeesite.common.shiro.filter.UserFilter; import com.jeesite.common.shiro.realm.AuthorizingRealm; import com.jeesite.common.shiro.realm.CasAuthorizingRealm; +import com.jeesite.common.shiro.realm.LdapAuthorizingRealm; import com.jeesite.common.shiro.session.SessionDAO; import com.jeesite.common.shiro.session.SessionManager; import com.jeesite.common.shiro.web.ShiroFilterFactoryBean; @@ -77,6 +80,15 @@ public class ShiroConfig { bean.setAuthorizingRealm(casAuthorizingRealm); return bean; } + + /** + * LDAP登录过滤器 + */ + private LdapFilter shiroLdapFilter(LdapAuthorizingRealm ldapAuthorizingRealm) { + LdapFilter bean = new LdapFilter(); + bean.setAuthorizingRealm(ldapAuthorizingRealm); + return bean; + } /** * Form登录过滤器 @@ -131,7 +143,7 @@ public class ShiroConfig { */ @Bean public ShiroFilterFactoryBean shiroFilter(WebSecurityManager webSecurityManager, AuthorizingRealm authorizingRealm, - CasAuthorizingRealm casAuthorizingRealm) { + CasAuthorizingRealm casAuthorizingRealm, LdapAuthorizingRealm ldapAuthorizingRealm) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(webSecurityManager); bean.setLoginUrl(Global.getProperty("shiro.loginUrl")); @@ -139,6 +151,7 @@ public class ShiroConfig { Map filters = bean.getFilters(); filters.put("inner", shiroInnerFilter()); filters.put("cas", shiroCasFilter(casAuthorizingRealm)); + filters.put("ldap", shiroLdapFilter(ldapAuthorizingRealm)); filters.put("authc", shiroAuthcFilter(authorizingRealm)); filters.put("logout", shiroLogoutFilter(authorizingRealm)); filters.put("perms", shiroPermsFilter()); @@ -182,17 +195,31 @@ public class ShiroConfig { bean.setCasServerCallbackUrl(Global.getProperty("shiro.casClientUrl") + Global.getAdminPath() + "/login-cas"); return bean; } + + /** + * LDAP安全认证实现类 + */ + @Bean + public LdapAuthorizingRealm ldapAuthorizingRealm(SessionDAO sessionDAO, CasOutHandler casOutHandler) { + LdapAuthorizingRealm bean = new LdapAuthorizingRealm(); + JndiLdapContextFactory contextFactory = (JndiLdapContextFactory) bean.getContextFactory(); + contextFactory.setUrl(Global.getProperty("shiro.ldapUrl"/*, "ldap://127.0.0.1:389"*/)); + bean.setUserDnTemplate(Global.getProperty("shiro.ldapUserDn"/*, "uid={0},ou=users,dc=mycompany,dc=com"*/)); + bean.setSessionDAO(sessionDAO); + return bean; + } /** * 定义Shiro安全管理配置 */ @Bean public WebSecurityManager webSecurityManager(AuthorizingRealm authorizingRealm, CasAuthorizingRealm casAuthorizingRealm, - SessionManager sessionManager, CacheManager shiroCacheManager) { + LdapAuthorizingRealm ldapAuthorizingRealm, SessionManager sessionManager, CacheManager shiroCacheManager) { WebSecurityManager bean = new WebSecurityManager(); Collection realms = ListUtils.newArrayList(); realms.add(authorizingRealm); // 第一个为权限授权控制类 realms.add(casAuthorizingRealm); + realms.add(ldapAuthorizingRealm); bean.setRealms(realms); bean.setSessionManager(sessionManager); bean.setCacheManager(shiroCacheManager); diff --git a/modules/core/src/main/resources/config/jeesite-core.yml b/modules/core/src/main/resources/config/jeesite-core.yml index 2b6a8715..66b8a407 100644 --- a/modules/core/src/main/resources/config/jeesite-core.yml +++ b/modules/core/src/main/resources/config/jeesite-core.yml @@ -332,6 +332,10 @@ shiro: # logoutUrl: ${shiro.casServerUrl}/logout?service=${shiro.loginUrl} # successUrl: ${shiro.casClientUrl}${adminPath}/index +# # LDAP 相关设置(标准版) +# ldapUrl: ldap://127.0.0.1:389 +# ldapUserDn: uid={0},ou=users,dc=mycompany,dc=com + # 简单 SSO 登录相关配置 sso: # 如果启用/sso/{username}/{token}单点登录,请修改此安全key并与单点登录系统key一致。 @@ -423,6 +427,7 @@ shiro: /druid/** = perms[sys:state:druid] /bpm/modeler/** = perms[bpm:modeler] ${adminPath}/login-cas = cas + ${adminPath}/login-ldap = ldap ${adminPath}/login = authc ${adminPath}/logout = logout ${adminPath}/file/** = user diff --git a/modules/core/src/main/resources/views/modules/sys/sysLogin.html b/modules/core/src/main/resources/views/modules/sys/sysLogin.html index 0f2ca550..414d0708 100644 --- a/modules/core/src/main/resources/views/modules/sys/sysLogin.html +++ b/modules/core/src/main/resources/views/modules/sys/sysLogin.html @@ -14,6 +14,9 @@

<% } %> <#form:form id="loginForm" model="${user!}" action="${ctx}/login" method="post" class="tab-content"> @@ -28,7 +31,7 @@ data-msg-required="${text('请填写登录账号.')}" placeholder="${text('登录账号')}" value="${cookie('rememberUserCode')}"/> -
+