shiro使用LDAP认证

最近在给公司搭建一个权限系统,在原有的测试管理平台上集成shiro框架,提供一个登录和权限控制功能。

之前是使用用户表,管理员直接创建用户,现在要使用员工的工号登录,公司员工是使用LDAP存储,

刚好shiro也提供LDAP的支持,调试了几天,总算调通了

 

使用通用的表设计,先看下权限系统的表设计

权限编码需要自己去实现,下面看下shiro的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/util 
           http://www.springframework.org/schema/util/spring-util-3.1.xsd"
    default-lazy-init="true">
    
    <!-- 用户授权信息Cache, 采用EhCache -->
    <!-- <bean id="ehcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean> -->
    <!-- cache 单机实现 -->
    <!--<import resource="classpath:shiro/shiro-ehcache.xml" />-->

    <!-- 自定义的Realm -->
    <bean id="authShiroRealm" class="com.xn.manage.shiro.WebAuthorizingRealm">
        <!--<property name="credentialsMatcher" ref="credentialsMatcher" />-->
        <property name="cachingEnabled" value="false" />
        <property name="authorizationCachingEnabled" value="false"/>
    </bean>

    <!-- 基于Form表单的身份验证过滤器 -->
    <bean id="formAuthenticationFilter"
          class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
        <property name="usernameParam" value="username"/>
        <property name="passwordParam" value="password" />
        <property name="rememberMeParam" value="rememberMe" />
        <property name="loginUrl" value="/login"/>
    </bean>

    <!-- 会话ID生成器 -->
    <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" />

    <!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
        当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
    <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <property name="httpOnly" value="true" />
        <property name="maxAge" value="-1" />
        <property name="name" value="sid" />
    </bean>

    <!--多个realm 的认证策略 -->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy " />
        </property>
    </bean>

    <!-- Shiro's main business-tier object for web-enabled applications -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!--<property name="realm" ref="authShiroRealm" />-->
        <!--<property name="realm" ref="ldapAuthorizingRealm" />-->
        <property name="authenticator" ref="authenticator"></property>
        <property name="realms">
            <list>
                <ref bean="ldapAuthorizingRealm" />
                <!--<ref bean="authShiroRealm" />-->
            </list>
        </property>
        <!--<property name="cacheManager" ref="cacheManager" />-->
        <property name="sessionManager" ref="sessionManager"/>
    </bean>

    <!-- session管理 -->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- session超时时间设置为8小时 -->
        <property name="globalSessionTimeout" value="28800000"></property>
        <property name="sessionIdCookie" ref="sessionIdCookie" />
        <property name="sessionIdCookieEnabled" value="true" />
    </bean>

    <!-- 重写ldap认证 这里的rootDN是搜索公司的员工的根目录-->
    <bean id="ldapAuthorizingRealm" class="com.xn.manage.shiro.LdapAuthorizingRealm">
        <property name="rootDN" value="OU=xxx公司,DC=xxx域,DC=com"/>
        <property name="userDnTemplate" value="{0}"/>
        <property name="contextFactory" ref="contextFactory"/>
    </bean>

    <!-- 配置ldap路径及配置一个默认的用户和密码 -->
    <bean id="contextFactory" class="org.apache.shiro.realm.ldap.JndiLdapContextFactory">
        <property name="url" value="ldap://LDAP的地址:端口"/>
        <property name="systemUsername" value="CN=xxx系统用户,OU=xxx公司,DC=xxx域,DC=com"/>
        <property name="systemPassword" value="密码123456"/>
    </bean>

    <!-- Shiro Filter -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/login" />
        <property name="successUrl" value="/index" />
        <property name="unauthorizedUrl" value="/403"/>
        <property name="filters">
            <util:map>
                <entry key="authc">
                    <bean class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"/>
                </entry>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /login = anon
                /sure_login = anon
                /logout = logout
                /picture/** = anon
                /vendor/** = anon
                /js/** = anon
                /css/** = anon
                /decorators/** = anon
                /common/** = anon
                /dist/** = anon
                /** = user
            </value>
        </property>
    </bean>

    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    
</beans>

LDAP有很多名词,DN、CN、OU、DC请自行百度

再看看LDAP的reaml

public class LdapAuthorizingRealm extends JndiLdapRealm {

    private static final Logger logger = LoggerFactory.getLogger(LdapAuthorizingRealm.class);
private String rootDN; public String getRootDN() { return rootDN; } public void setRootDN(String rootDN) { this.rootDN = rootDN; } /** * 登录时调用 * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = queryForAuthenticationInfo(token, getContextFactory()); getAuthorizationInfo(((UsernamePasswordToken) token).getUsername()); } catch (AuthenticationNotSupportedException e) { String msg = "Unsupported configured authentication mechanism"; throw new UnsupportedAuthenticationMechanismException(msg, e); } catch (javax.naming.AuthenticationException e) { String msg = "LDAP authentication failed."; throw new AuthenticationException(msg, e); } catch (NamingException e) { String msg = "LDAP naming error while attempting to authenticate user."; throw new AuthenticationException(msg, e); } catch (UnknownAccountException e) { String msg = "账号不存在!"; throw new UnknownAccountException(msg, e); } catch (IncorrectCredentialsException e) { String msg = "IncorrectCredentialsException"; throw new IncorrectCredentialsException(msg, e); } return info; } /** * 授权 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 因为非正常退出,即没有显式调用 SecurityUtils.getSubject().logout() // (可能是关闭浏览器,或超时),但此时缓存依旧存在(principals),所以会自己跑到授权方法里。 if (!SecurityUtils.getSubject().isAuthenticated()) { doClearCache(principalCollection); SecurityUtils.getSubject().logout(); return null; } // 获取当前登录的用户名 String username = (String) principalCollection.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Session session = SecurityUtils.getSubject().getSession(); authorizationInfo.setStringPermissions((Set<String>) session.getAttribute("permissions")); return authorizationInfo; } /** * 连接LDAP查询用户信息是否存在 * <p> * 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 token, LdapContextFactory ldapContextFactory) throws NamingException { 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);//搜索范围是包括子树 // String returnedAtts[] = { "uid","displayName","cn","company","department","mailNickname"}; // constraints.setReturningAttributes(returnedAtts); NamingEnumeration results = systemCtx.search(rootDN, "UID=" + principal , constraints); if (results != null && !results.hasMore()) { throw new UnknownAccountException(); } else { while (results.hasMore()) { SearchResult si = (SearchResult) results.next(); principal = si.getName() + "," + rootDN; logger.debug(si.getAttributes().get("company").toString()); logger.debug(si.getAttributes().get("department").toString()); } logger.info("DN=[" + principal + "]"); try { //根据查询到的用户与输入的密码连接LDAP,用户密码正确才能连接 ctx = ldapContextFactory.getLdapContext(principal, credentials); dealUser(userName, password); } catch (NamingException e) { throw new IncorrectCredentialsException(); } return new SimpleAuthenticationInfo(userName, MD5Util.MD5(userName + password).toLowerCase(), getName()); } } finally { //关闭连接 LdapUtils.closeContext(systemCtx); LdapUtils.closeContext(ctx); } } /** * 将LDAP查询到的用户保存到sys_user表 * * @param userName */ private void dealUser(String userName, String password) { if (StringUtil.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; } /** * 设定Password校验的Hash算法与迭代次数.这里使用了自定义的加密算法 */ @PostConstruct public void initCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("MD5") { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object credentials = token.getCredentials(); UsernamePasswordToken token1 = (UsernamePasswordToken) token; if (credentials == null) { String msg = "Argument for byte conversion cannot be null."; throw new IllegalArgumentException(msg); } byte[] bytes = null; if (credentials instanceof char[]) { bytes = CodecSupport.toBytes(new String((char[]) credentials), PREFERRED_ENCODING); } else { bytes = objectToBytes(credentials); } String tokenHashedCredentials = MD5Util.MD5(token1.getUsername() + new String(bytes)).toLowerCase(); String accountCredentials = getCredentials(info).toString(); return tokenHashedCredentials.equals(accountCredentials); } }; matcher.setHashIterations(1); setCredentialsMatcher(matcher); } }

这里涉及公司具体业务的代码出于保密没有写出来,大概就是这个样子

 

posted @ 2017-06-12 10:49  乱码出黑客  阅读(3287)  评论(0编辑  收藏  举报