jeesite1.2.7 shiro如何集成LDAP实现多种校验共存

因为公司内部系统多,账号体系多,现需要实现统一账号账号登录不同系统,现采用LDAP来管理账号。
刚好shiro也提供LDAP的支持,结合网上资料写下如下内容。

总体方案

  1. jeesite现有账号体系用户自己维护email地址与LDAP中存储的email地址一一对应
  2. 自定义LdapAuthorizingRealm继承JndiLdapRealm类,重写里面方法。
  3. 在spring-context-shiro.xml添加自定义realm

代码实现

  1. 自定义LdapAuthorizingRealm

package com.xxx.modules.sys.security;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.naming.AuthenticationNotSupportedException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.ldap.UnsupportedAuthenticationMechanismException;
import org.apache.shiro.realm.ldap.JndiLdapRealm;
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.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



/**
 * LdapRealm
 * @author huangkai
 *
 */
public class LdapAuthorizingRealm extends JndiLdapRealm{
	private Logger logger = LoggerFactory.getLogger(getClass());
	
	private SystemService systemService;
	
	private String rootDN;

    public String getRootDN() {
        return rootDN;
    }

    public void setRootDN(String rootDN) {
        this.rootDN = rootDN;
    }

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken){
		UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
		
		// 校验登录验证码
		if (LoginController.isValidateCodeLogin(token.getUsername(), false, false)){
			Session session = UserUtils.getSession();
			String code = (String)session.getAttribute(ValidateCodeServlet.VALIDATE_CODE);
			if (token.getCaptcha() == null || !token.getCaptcha().toUpperCase().equals(code)){
				throw new AuthenticationException("msg:验证码错误, 请重试.");
			}
		}
		
		AuthenticationInfo info;
        try {
            info = queryForAuthenticationInfo(token, getContextFactory());
            //getAuthorizationInfo(token.getUsername());
        } catch (AuthenticationNotSupportedException e) {
            String msg = "msg:Unsupported configured authentication mechanism";
            throw new UnsupportedAuthenticationMechanismException(msg, e);
        } catch (javax.naming.AuthenticationException e) {
            String msg = "msg:LDAP authentication failed.";
            throw new AuthenticationException(msg, e);
        } catch (NamingException e) {
            String msg = "msg:LDAP naming error while attempting to authenticate user.";
            throw new AuthenticationException(msg, e);
        } catch (UnknownAccountException e) {
            String msg = "msg:账号不存在!";
            throw new UnknownAccountException(msg, e);
        } catch (IncorrectCredentialsException e) {
            String msg = "msg:密码错误";
            throw new IncorrectCredentialsException(msg, e);
        }

        return info;
	}
    
	
	
	/**
     * 连接LDAP查询用户信息是否存在
     * 

* 1. 从页面得到登陆名和密码。注意这里的登陆名和密码一开始并没有被用到。 * 2. 先匿名绑定到LDAP服务器,如果LDAP服务器没有启用匿名绑定,一般会提供一个默认的用户,用这个用户进行绑定即可。 * 3. 之前输入的登陆名在这里就有用了,当上一步绑定成功以后,需要执行一个搜索,而filter就是用登陆名来构造,形如: "CN=*(xn607659)" 。 * 搜索执行完毕后,需要对结果进行判断,如果只返回一个entry,这个就是包含了该用户信息的entry,可以得到该 entry的DN,后面使用。 * 如果返回不止一个或者没有返回,说明用户名输入有误,应该退出验证并返回错误信息。 * 4. 如果能进行到这一步,说明用相应的用户,而上一步执行时得到了用户信息所在的entry的DN,这里就需要用这个DN和第一步中得到的password重新绑定LDAP服务器。 * 5. 执行完上一步,验证的主要过程就结束了,如果能成功绑定,那么就说明验证成功,如果不行,则应该返回密码错误的信息。 * 这5大步就是基于LDAP的一个 “两次绑定” 验证方法 * * @param token * @param ldapContextFactory * @return * @throws NamingException */ @Override protected AuthenticationInfo queryForAuthenticationInfo( AuthenticationToken authcToken, LdapContextFactory ldapContextFactory) throws NamingException { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; Object principal = token.getPrincipal();//输入的用户名 Object credentials = token.getCredentials();//输入的密码 String userName = principal.toString(); String password = new String((char[]) credentials); LdapContext systemCtx = null; LdapContext ctx = null; try { //使用系统配置的用户连接LDAP systemCtx = ldapContextFactory.getSystemLdapContext(); SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);//搜索范围是包括子树 NamingEnumeration<SearchResult> results = systemCtx.search(rootDN, "cn=" + principal, constraints); if (results != null && !results.hasMore()) { throw new UnknownAccountException(); } else { String mail=null; while (results.hasMore()) { SearchResult si = (SearchResult) results.next(); principal = si.getName() + "," + rootDN; mail= si.getAttributes().get("mail").get(0).toString(); logger.debug(si.getAttributes().get("mail").toString()); } logger.info("DN=[" + principal + "]"); try { //根据查询到的用户与输入的密码连接LDAP,用户密码正确才能连接 ctx = ldapContextFactory.getLdapContext(principal, credentials); dealUser(userName, password); } catch (NamingException e) { throw new IncorrectCredentialsException(); } // 校验用户名密码 if(StringUtils.isNotBlank(mail)){ User user = getSystemService().getUserByMail(mail); if (user != null) { if (Global.NO.equals(user.getLoginFlag())){ throw new AuthenticationException("msg:该已帐号禁止登录."); } byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16)); return new SimpleAuthenticationInfo(new Principal(user, token.isMobileLogin()), user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName()); } else { throw new AuthenticationException("msg:"+mail+"未找到邮箱对应的账号"); } }else{ throw new AuthenticationException("msg:"+"ldap未配置用户的邮箱"); } } } finally { //关闭连接 LdapUtils.closeContext(systemCtx); LdapUtils.closeContext(ctx); } } /** * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Principal principal = (Principal) getAvailablePrincipal(principals); // 获取当前已登录的用户 if (!Global.TRUE.equals(Global.getConfig("user.multiAccountLogin"))){ Collection<Session> sessions = getSystemService().getSessionDao().getActiveSessions(true, principal, UserUtils.getSession()); if (sessions.size() > 0){ // 如果是登录进来的,则踢出已在线用户 if (UserUtils.getSubject().isAuthenticated()){ for (Session session : sessions){ getSystemService().getSessionDao().delete(session); } } // 记住我进来的,并且当前用户已登录,则退出当前用户提示信息。 else{ UserUtils.getSubject().logout(); throw new AuthenticationException("msg:账号已在其它地方登录,请重新登录。"); } } } User user = getSystemService().getUserByLoginName(principal.getLoginName()); if (user != null) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); List<Menu> list = UserUtils.getMenuList(); for (Menu menu : list){ if (StringUtils.isNotBlank(menu.getPermission())){ // 添加基于Permission的权限信息 for (String permission : StringUtils.split(menu.getPermission(),",")){ info.addStringPermission(permission); } } } // 添加用户权限 info.addStringPermission("user"); // 添加用户角色信息 for (Role role : user.getRoleList()){ info.addRole(role.getEnname()); } // 更新登录IP和时间 getSystemService().updateUserLoginInfo(user); // 记录登录日志 LogUtils.saveLog(Servlets.getRequest(), "系统登录"); return info; } else { return null; } } /** * 将LDAP查询到的用户保存到sys_user表 * * @param userName */ private void dealUser(String userName, String password) { if (StringUtils.isEmpty(userName)) { return; } //TO DO... } /** * 获取权限码 * * @param username * @return */ private Map<String, Set<String>> getAuthorizationInfo(String username) { Map<String, Set<String>> authorizationMap = new HashMap<String, Set<String>>(); Set<String> codeSet = new HashSet<String>(); Session session = SecurityUtils.getSubject().getSession(); //查询数据库的用户权限 //...... authorizationMap.put("permissions", codeSet); session.setAttribute("permissions", codeSet); logger.debug("当前登录账户:{}的权限集合:{}", username, codeSet); return authorizationMap; } /** * 获取系统业务对象 */ public SystemService getSystemService() { if (systemService == null){ systemService = SpringContextHolder.getBean(SystemService.class); } return systemService; } }

  1. spring-context-shiro.xml配置


	
		
		
		
		
		
            
                
                
            
        
	
	
        
            
        
    
	
	
    
        
        
        
    

    
    
        
        
        
    

Shiro 认证策略,如果有多个Realm,怎样才算是认证成功,这就需要认证策略。
AuthenticationStrategy接口的默认实现:

  • FirstSuccessfulStrategy:只要有一个Realm 验证成功即可,只返回第一个Realm 身份验证成功的认证信息,其他的忽略;
  • AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将返回所有Realm身份验证成功的认证信息;
  • AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。
  • ModularRealmAuthenticator默认是AtLeastOneSuccessfulStrategy策略

参考链接

  1. Shiro-多Realm验证
  2. shiro使用LDAP认证

你可能感兴趣的:(shiro)