IM开发【4】 - 使用第三方用户账号登陆Openfire

首页 »  django即时通信 » IM开发之使用django的用户帐号登录openfire

IM开发之使用django的用户帐号登录openfire

日期:2010-05-08作者:firefish分类:django即时通信阅读:2,861评论:4

openfire是一个基于XMPP协议开源的就即时通信服务器系统。利用它再加上同样开源的spark,可以轻松的为你的网站用户提供一个类似QQ这样的及时通讯软件,来扩展网站服务,增加用户黏度。openfire拥有强大功能的同时还拥有强劲的性能,据称单台普通配置的服务器可以支撑1W+的并发。另外它还拥有一个完备的插件系统,可以通过插件实现你需要的任何功能。总之好处多多,务须多言,如果你的网站或者应用系统有IM的需求,openfire是不二选择。

openfire架构

openfire架构

openfire的安装配置很简单,这里就不再赘述。本文要讲的内容是将openfire同既有系统结合的第一步:整合用户系统,即让网站现有用户使用自己的帐号进行登录。由于公司的网站基于django开发,使用的是django自带的用户系统,因此本文内容及提供的源代码适用于所有使用django开发的网站。

本文测试用的客户端使用的是spark,可以到这里下载。

一、使用外部用户系统的配置

安装openfire,启动服务控制台,进入管理界面。首次进入管理,会自动进入配置界面,配置很简单。注意一点,在选择数据库时,使用“外部数据库”,openfire支持常用的数据库,包括mysql、oracle、mssql等。我使用的是mysql,这样openfire就会自动在指定的mysql数据库上建表,并写入初始数据。

openfire自带有一套用户系统,实现了用户登录验证功能,同时还允许通过配置,使用JDBC访问指定的数据库,这样就可以通过配置访问外部的用户系统了。最简单的方式是直接修改数据库进行配置。

1.在mysql客户端中打开openfire的数据库,打开表ofproperty,在里面添加4条记录:

name propValue
jdbcProvider.driver com.mysql.jdbc.Driver
jdbcProvider.connectionString jdbc:mysql://[host]:3306/[database]?user=[user]&password=[pwd]
jdbcProvider.passwordSQL select password from auth_user where username=?
jdbcProvider.passwordType md5

2.修改name为provider.auth.className的记录的propValue字段值为:org.jivesoftware.openfire.auth.JDBCAuthProvider

3.重启openfire控制台,如果你网站的密码字段采用的是标准MD5算法(另外也支持明文和SHA算法),那么现在就可以启动spark使用网站的帐号进行登录了。

4.在上表添加记录:

admin.authorizedJIDs    ——    [username]@[openfire-domain]

username是网站用户表内存在的一个用户名,openfire-domain是你的openfire的域。这样再需要登录管理界面时,使用这个用户名即可。

二、django用户系统的问题

是不是很简单?但如果你跟我一样,使用django开发网站,并且使用了django提供的用户系统。那么很遗憾,经过上面的步骤,你依然无法登录。原因就在于django的密码字段并不是单纯保存了密码,而是采用了和unix的shadow文件一样的格式,像下面这样:

sha1$3d5fd$8bf87829f5749c57f8336007c5fc6cbb9cfbb1f3

这个字段里包含了3个信息,即:本密码的算法、盐、密文,使用$进行分隔。上面这个JDBCAuthProvider使用的是常规的密码验证方法,即:

1.读取指定用户名的密码字段值; 
2.使用passwordType指定的算法加密用户输入的密码; 
3.比较本次加密的的字符串和读取出来的密码字段值是否相等。

由此可以看出,用指定算法加密的密码不可能和django的用户表中的密码字段值相等。

三、搞定它

问题找到了,是JDBCAuthProvider的密码验证算法不能满足要求,那么就从它开刀了。下载openfire的源码(猛击这里),用eclipse打开,设置方法请参见这里,很nice的一篇文章,清晰、翔实、可操作性强,我这个java门外汉照着步骤做都很快搞定了,只提醒初学者一点,不要把你的源码放在名字有中文的目录下——高手请无视:)。

openfire源码很赞,这里就不再多说了。在org.jivesoftware.openfire.auth这个包里可以找到JDBCAuthProvider这个类,验证密码的函数就是authenticate。

那么,我们修改它吧?呃,这样子似乎有点太暴力了,另外,如果将来官方在新版本里修改了这个类,就杯具了……

正确的做法:新建一个类,像JDBCAuthProvider一样实现AuthProvider接口的相关方法,这样子验证函数就随便你怎么折腾了,下面是我的类,你可以拿去直接使用:

package org.firefishsoftware.openfire.plugin;

import org.jivesoftware.openfire.user.UserAlreadyExistsException; 
import org.jivesoftware.openfire.user.UserManager; 
import org.jivesoftware.openfire.user.UserNotFoundException;  
import org.jivesoftware.openfire.auth.*;  
import org.jivesoftware.openfire.auth.JDBCAuthProvider.PasswordType; 
import org.jivesoftware.openfire.XMPPServer;  
import org.jivesoftware.util.JiveGlobals;  
import org.jivesoftware.util.Log;  
import org.jivesoftware.util.StringUtils;  
import org.jivesoftware.database.DbConnectionManager;  
import java.io.UnsupportedEncodingException; 
import java.sql.*;  
import java.security.MessageDigest; 
import java.security.NoSuchAlgorithmException;

/** 
* Created by firefish 
* Date: 2010-5-6 
* Time: 11:06 
* 仿照JDBCAuthProvider 
*/  
public class DjangoAuthProvider implements AuthProvider {  
    private String connectionString;

    private String passwordSQL; 
    private String userIdSQL; 
    private PasswordType passwordType; 
    private boolean useConnectionProvider;

    /** 
     * Constructs a new JDBC authentication provider. 
     */ 
    public DjangoAuthProvider() { 
        // Convert XML based provider setup to Database based 
        JiveGlobals.migrateProperty(“jdbcProvider.driver”); 
        JiveGlobals.migrateProperty(“jdbcProvider.connectionString”); 
        JiveGlobals.migrateProperty(“djangoAuthProvider.passwordSQL”); 
        JiveGlobals.migrateProperty(“djangoAuthProvider.userIdSQL”); 
        useConnectionProvider = JiveGlobals.getBooleanProperty(“jdbcAuthProvider.useConnectionProvider”);

        if (!useConnectionProvider) { 
            // Load the JDBC driver and connection string. 
            String jdbcDriver = JiveGlobals.getProperty(“jdbcProvider.driver”); 
            try { 
               Class.forName(jdbcDriver).newInstance(); 
            } 
            catch (Exception e) { 
                Log.error(“Unable to load JDBC driver: ” + jdbcDriver, e); 
                return; 
            } 
            connectionString = JiveGlobals.getProperty(“jdbcProvider.connectionString”); 
        }

        // Load SQL statements. 
        passwordSQL = JiveGlobals.getProperty(“djangoAuthProvider.passwordSQL”); 
        userIdSQL = JiveGlobals.getProperty(“djangoAuthProvider.userIdSQL”); 
        passwordType = PasswordType.sha1; 
    }

    public void authenticate(String username, String password) throws UnauthorizedException { 
        if (username == null || password == null) { 
            throw new UnauthorizedException(); 
        } 
        username = username.trim().toLowerCase(); 
        if (username.contains(“@”)) { 
            // Check that the specified domain matches the server’s domain 
            int index = username.indexOf(“@”); 
            String domain = username.substring(index + 1); 
            if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) { 
                username = username.substring(0, index); 
            } else { 
                // Unknown domain. Return authentication failed. 
                throw new UnauthorizedException(); 
            } 
        } 
      
        String userPassword; 
        try { 
            userPassword = getPasswordValue(username); 
        } 
        catch (UserNotFoundException unfe) { 
            throw new UnauthorizedException(); 
        }

        String[] passwordData = userPassword.split(“\$”); 
        if (passwordData.length != 3){ 
            Log.info(“无效的密码数据:”+userPassword); 
            throw new UnauthorizedException(); 
        } 
        // If the user’s password doesn’t match the password passed in, authentication 
        // should fail. 
        MessageDigest digst; 
        try { 
            digst = MessageDigest.getInstance(“SHA-1″); 
            digst.update(passwordData[1].getBytes(“UTF8″));   
            digst.update(password.getBytes(“UTF8″));   
            password = StringUtils.encodeHex(digst.digest());        
        } catch (NoSuchAlgorithmException e) { 
            Log.info(“加密失败:”+e.toString()); 
        } catch (UnsupportedEncodingException e) { 
            Log.info(“加密失败:”+e.toString()); 
        }  

        if (!password.equals(passwordData[2])) { 
            throw new UnauthorizedException(); 
        }

        // Got this far, so the user must be authorized. 
        createUser(username); 
    }

    public void authenticate(String username, String token, String digest) 
            throws UnauthorizedException 
    {

    }

    public boolean isPlainSupported() { 
        // If the auth SQL is defined, plain text authentication is supported. 
        return (passwordSQL != null); 
    }

    public boolean isDigestSupported() { 
        // The auth SQL must be defined and the password type is supported. 
        return (passwordSQL != null && passwordType == PasswordType.plain); 
    }

    public String getPassword(String username) throws UserNotFoundException, 
            UnsupportedOperationException 
    {

        if (!supportsPasswordRetrieval()) { 
            throw new UnsupportedOperationException(); 
        } 
        if (username.contains(“@”)) { 
            // Check that the specified domain matches the server’s domain 
            int index = username.indexOf(“@”); 
            String domain = username.substring(index + 1); 
            if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) { 
                username = username.substring(0, index); 
            } else { 
                // Unknown domain. 
                throw new UserNotFoundException(); 
            } 
        } 
        return getPasswordValue(username); 
    }

    public void setPassword(String username, String password) 
             throws UserNotFoundException, UnsupportedOperationException 
    {

    }

    public boolean supportsPasswordRetrieval() { 
        return (passwordSQL != null && passwordType == PasswordType.plain); 
    }

    private Connection getConnection() throws SQLException { 
        if (useConnectionProvider) 
            return DbConnectionManager.getConnection(); 
        return DriverManager.getConnection(connectionString); 
    }

    /** 
     * Returns the value of the password field. It will be in plain text or hashed 
     * format, depending on the password type. 
     * 
     * @param username user to retrieve the password field for 
     * @return the password value. 
     * @throws UserNotFoundException if the given user could not be loaded. 
     */ 
    private String getPasswordValue(String username) throws UserNotFoundException { 
        String password = null; 
        Connection con = null; 
        PreparedStatement pstmt = null; 
        ResultSet rs = null;

        try { 
            con = getConnection(); 
            pstmt = con.prepareStatement(passwordSQL); 
            pstmt.setString(1, username);

            rs = pstmt.executeQuery();

            // If the query had no results, the username and password 
            // did not match a user record. Therefore, throw an exception. 
            if (!rs.next()) { 
                throw new UserNotFoundException(); 
            } 
            password = rs.getString(1); 
        } 
        catch (SQLException e) { 
            Log.error(“Exception in JDBCAuthProvider”, e); 
            throw new UserNotFoundException(); 
        } 
        finally { 
            DbConnectionManager.closeConnection(rs, pstmt, con); 
        } 
        return password; 
    }

    /** 
     * Checks to see if the user exists; if not, a new user is created. 
     * 
     * @param username the username. 
     */ 
    private static void createUser(String username) { 
        // See if the user exists in the database. If not, automatically create them. 
        UserManager userManager = UserManager.getInstance(); 
        try { 
            userManager.getUser(username); 
        } 
        catch (UserNotFoundException unfe) { 
            try { 
                Log.debug(“DjangoAuthProvider: Automatically creating new user account for ” + username);
                UserManager.getUserProvider().createUser(username, StringUtils.randomString(8), 
                        null, null); 
            } 
            catch (UserAlreadyExistsException uaee) { 
                // Ignore. 
            } 
        } 
    } 

删掉了一些不需要代码,修改了authenticate函数,将读出来的密码进行拆分,获取盐和密文。将用户输入的密码用sha算法加上盐进行加密后,比较密文是否相等。简单起见,本文只实现了sha算法,再做的完善一点可以使用密码字段中指定的算法进行加密。

最后修改ofproperty表的provider.auth.className记录对应的值为:org.firefishsoftware.openfire.plugin.DjangoAuthProvider,重启openfire控制台,再用客户端登录试试!

如果你的密码采用了自定义的算法进行加密,那么上面的方法同样适用你。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值