java实现用户通过opt动态口令登录系统

        在Java中实现一个基于动态口令(OTP, One-Time Password)的用户登录系统,通常涉及到几个关键组件:用户身份验证、OTP生成与验证、以及安全存储用户信息。下面简单介绍一下如何实现用户通过动态口令进行登录。

一、生成秘钥

        通过工具类生成秘钥,场景:在系统中新增注册用户时,调用工具类相关方法生成秘钥,同用户信息一起存储至用户信息表中。

示例用户信息表:

二、秘钥与用户存储绑定或关联

        上图中的表中,key为秘钥、name为用户名,即生成用户信息和密钥信息,秘钥和账号信息关系为一对一关系,不可重复。

三、选择口令生成器

        如没有设计口令生成器,就用“数字验证码”这个小程序作为我们的口令生成器,

四、账号绑定口令生成器

秘钥和账号绑定流程如下图:

动态口令默认有效时间为30秒,系统时间偏差不能大于6秒。只要在有效期内负责动态口令到数字资源系统进行登录即可实现登录,即动态口令就是系统登录界面对应的密码。

五、示例代码

(1)依赖

<dependency>
  <groupId>javax.xml.ws</groupId>
  <artifactId>jaxws-api</artifactId>
  <version>2.3.1</version>
</dependency>
<dependency>
  <groupId>com.sun.xml.ws</groupId>
  <artifactId>rt</artifactId>
  <version>2.3.1</version>
</dependency>

(2)秘钥生成工具代码

Base32String :

package common.opt;

import java.util.HashMap;
import java.util.Locale;


public class Base32String {
    // singleton

    private static final Base32String INSTANCE =
            new Base32String("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"); // RFC 4648/3548

    static Base32String getInstance() {
        return INSTANCE;
    }

    // 32 alpha-numeric characters.
    private String ALPHABET;
    private char[] DIGITS;
    private int MASK;
    private int SHIFT;
    private HashMap<Character, Integer> CHAR_MAP;

    static final String SEPARATOR = "-";

    protected Base32String(String alphabet) {
        this.ALPHABET = alphabet;
        DIGITS = ALPHABET.toCharArray();
        MASK = DIGITS.length - 1;
        SHIFT = Integer.numberOfTrailingZeros(DIGITS.length);
        CHAR_MAP = new HashMap<Character, Integer>();
        for (int i = 0; i < DIGITS.length; i++) {
            CHAR_MAP.put(DIGITS[i], i);
        }
    }

    public static byte[] decode(String encoded) throws DecodingException {
        return getInstance().decodeInternal(encoded);
    }

    protected byte[] decodeInternal(String encoded) throws DecodingException {
        // Remove whitespace and separators
        encoded = encoded.trim().replaceAll(SEPARATOR, "").replaceAll(" ", "");

        // Remove padding. Note: the padding is used as hint to determine how many
        // bits to decode from the last incomplete chunk (which is commented out
        // below, so this may have been wrong to start with).
        encoded = encoded.replaceFirst("[=]*$", "");

        // Canonicalize to all upper case
        encoded = encoded.toUpperCase(Locale.US);
        if (encoded.length() == 0) {
            return new byte[0];
        }
        int encodedLength = encoded.length();
        int outLength = encodedLength * SHIFT / 8;
        byte[] result = new byte[outLength];
        int buffer = 0;
        int next = 0;
        int bitsLeft = 0;
        for (char c : encoded.toCharArray()) {
            if (!CHAR_MAP.containsKey(c)) {
                throw new DecodingException("Illegal character: " + c);
            }
            buffer <<= SHIFT;
            buffer |= CHAR_MAP.get(c) & MASK;
            bitsLeft += SHIFT;
            if (bitsLeft >= 8) {
                result[next++] = (byte) (buffer >> (bitsLeft - 8));
                bitsLeft -= 8;
            }
        }
        // We'll ignore leftover bits for now.
        //
        // if (next != outLength || bitsLeft >= SHIFT) {
        //  throw new DecodingException("Bits left: " + bitsLeft);
        // }
        return result;
    }

    public static String encode(byte[] data) {
        return getInstance().encodeInternal(data);
    }

    protected String encodeInternal(byte[] data) {
        if (data.length == 0) {
            return "";
        }

        // SHIFT is the number of bits per output character, so the length of the
        // output is the length of the input multiplied by 8/SHIFT, rounded up.
        if (data.length >= (1 << 28)) {
            // The computation below will fail, so don't do it.
            throw new IllegalArgumentException();
        }

        int outputLength = (data.length * 8 + SHIFT - 1) / SHIFT;
        StringBuilder result = new StringBuilder(outputLength);

        int buffer = data[0];
        int next = 1;
        int bitsLeft = 8;
        while (bitsLeft > 0 || next < data.length) {
            if (bitsLeft < SHIFT) {
                if (next < data.length) {
                    buffer <<= 8;
                    buffer |= (data[next++] & 0xff);
                    bitsLeft += 8;
                } else {
                    int pad = SHIFT - bitsLeft;
                    buffer <<= pad;
                    bitsLeft += pad;
                }
            }
            int index = MASK & (buffer >> (bitsLeft - SHIFT));
            bitsLeft -= SHIFT;
            result.append(DIGITS[index]);
        }
        return result.toString();
    }

    @Override
    // enforce that this class is a singleton
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    public static class DecodingException extends Exception {
        public DecodingException(String message) {
            super(message);
        }
    }
}

SetKyeUtil :

package common.opt;

import java.security.SecureRandom;


public class SetKyeUtil {
    
    
    //生成秘钥
    public static String getRandomSecretBase32(int length){
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[length / 2];
        random.nextBytes(salt);
        return Base32String.encode(salt);
    }
}

 (3)验证动态口令代码

package common.opt;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;


public class TotpUtils {

    private static final Logger logger = LoggerFactory.getLogger(common.opt.TotpUtils.class);
    private TotpUtils() {}

    /**
     * 该方法使用JCE提供加密算法。
     * HMAC使用加密哈希算法作为参数计算哈希消息认证码。
     * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
     *                             HmacSHA512)
     * @param keyBytes: 用于HMAC密钥的字节
     * @param text: 用于HMAC密钥的字节数
     */
    private static byte[] hmac_sha(String crypto, byte[] keyBytes,byte[] text){
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);
            SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }

    /**
     * This method converts a HEX string to Byte[]
     * @param hex: the HEX string
     * @return: a byte array
     */
    private static byte[] hexStr2Bytes(String hex){
        // Adding one byte to get the right conversion Values starting with "0" can be converted
        byte[] bArray = new BigInteger("10" + hex,16).toByteArray();
        // Copy all the REAL bytes, not the "first"
        byte[] ret = new byte[bArray.length - 1];
        for (int i = 0; i < ret.length; i++)
            ret[i] = bArray[i+1];
        return ret;
    }

    private static final int[] DIGITS_POWER
            // 0 1  2   3    4     5      6       7        8
            = {1,10,100,1000,10000,100000,1000000,10000000,100000000 };


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     * @param crypto: the crypto function to use
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP(String key,String time,String returnDigits,String crypto){
        int codeDigits = Integer.decode(returnDigits).intValue();
        String result = null;
        while (time.length() < 16 )
            time = "0" + time;
        byte[] msg = hexStr2Bytes(time);
        byte[] k = hexStr2Bytes(key);
        byte[] hash = hmac_sha(crypto, k, msg);
        int offset = hash[hash.length - 1] & 0xf;
        int binary =
                ((hash[offset] & 0x7f) << 24) |
                        ((hash[offset + 1] & 0xff) << 16) |
                        ((hash[offset + 2] & 0xff) << 8) |
                        (hash[offset + 3] & 0xff);
        int otp = binary % DIGITS_POWER[codeDigits];
        result = Integer.toString(otp);
        while (result.length() < codeDigits) {
            result = "0" + result;
        }
        return result;
    }


    /**
     * 根据密钥生成动态口令
     * @param secretBase32 base32编码格式的密钥
     * @return
     */
    public static String generate(String secretBase32){
        String secretHex = "";
        try {
            secretHex = HexEncoding.encode(Base32String.decode(secretBase32));
        } catch (Base32String.DecodingException e) {
            logger.error("解码" + secretBase32 + "出错,", e);
            throw new RuntimeException("解码Base32出错");
        }
        long X = 30;
        String steps = "0";
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        df.setTimeZone(TimeZone.getTimeZone("UTC"));
        long currentTime = System.currentTimeMillis() / 1000L;
        try {
            long t = currentTime / X;
            steps = Long.toHexString(t).toUpperCase();
            while (steps.length() < 16) steps = "0" + steps;
            return generateTOTP(secretHex, steps, "6", "HmacSHA1");
        } catch (final Exception e) {
            logger.error("生成动态口令出错:" + secretBase32, e);
            throw new RuntimeException("生成动态口令出错");
        }
    }

}

(4)测试效果

生成的动态口令:

测试代码:

public static void main(String[] args) {
    //账号的key
    String key = "BAEIS2HQPJZ7Q45TTDF3VNRCHGTD34YG5EK2EHOZGDZ6JHAO53GGG";
    //秘钥对应的动态口令
    String optPwd = TotpUtils.generate(key);
    //动态口令生成器生成的秘钥
    String trendsPwd = "706381";
    //验证秘钥是否匹配
    System.out.println("账号admin的秘钥:"+optPwd);
    System.out.println("数字小程序生成的动态口令:"+trendsPwd);
}

测试结果:

通过测试,顺利验证了动态口令的匹配结果,至此,功能就完成了

import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Text extends JFrame implements ActionListener,TextListener { private JLabel username,password,email,telephone; private JTextField usernameField,emailField; private TextField teleField; private JPasswordField passwordField; private JButton registButton,cancelButton; public Text() { super("用户注册"); Container container = getContentPane(); container.setLayout(new FlowLayout()); username = new JLabel("用户名"); username.setToolTipText("请输入用户名"); password = new JLabel("密 码"); password.setToolTipText("密码不能少于6位"); email = new JLabel("邮 箱"); email.setToolTipText("邮箱中必须包含@字符"); telephone = new JLabel("电 话"); telephone.setToolTipText("电话只能为数字"); usernameField = new JTextField(15); usernameField.addActionListener(this); passwordField = new JPasswordField(15); passwordField.addActionListener(this); emailField = new JTextField(15); emailField.addActionListener(this); teleField = new TextField(21); teleField.addActionListener(this); teleField.addTextListener(this); registButton = new JButton("注册"); cancelButton = new JButton("取消"); container.add(username); container.add(usernameField); container.add(password); container.add(passwordField); container.add(email); container.add(emailField); container.add(telephone); container.add(teleField); container.add(registButton); container.add(cancelButton); registButton.addActionListener(this); cancelButton.addActionListener(this); setSize(250,200); setVisible(true); setResizable(false); } public void textValueChanged(TextEvent event) { if (event.getSource() == teleField) { /*if (!checkNumber(teleField.getText())) { JOptionPane.showMessageDialog(this,"电话必须为数字","温馨提示",JOptionPane.INFORMATION_MESSAGE); teleField.setText(""); } */ } } public void actionPerformed(ActionEvent event) { if (event.getSource() == usernameField) { if (usernameField.getText().equals("")) { JOptionPane.showMessageDialog(null,"用户名不能为空","温馨提示",JOptionPane.INFORMATION_MESSAGE); } } if (event.getSource() == passwordField) { if (passwordField.getPassword().length == 0) { JOptionPane.showMessageDialog(this,"密码不能为空","温馨提示",JOptionPane.INFORMATION_MESSAGE); } else if (passwordField.getPassword().length < 6) { JOptionPane.showMessageDialog(this,"密码长度不能小于6位","温馨提示",JOptionPane.INFORMATION_MESSAGE); passwordField.setText(""); } } if (event.getSource() == emailField) { if (emailField.getText().indexOf("@") < 0 || emailField.getText().indexOf("@") >= (emailField.getText().length() - 1) || emailField.getText().equals("")) { JOptionPane.showMessageDialog(this,"您的邮箱格式不正确","温馨提示",JOptionPane.INFORMATION_MESSAGE); emailField.setText(""); } } if (event.getSource() == teleField) { if (teleField.getText().equals("")) { JOptionPane.showMessageDialog(this,"联系电话不能为空","温馨提示",JOptionPane.INFORMATION_MESSAGE); } else if (!checkNumber(teleField.getText())) { JOptionPane.showMessageDialog(this,"电话必须为数字","温馨提示",JOptionPane.INFORMATION_MESSAGE); teleField.setText(""); } } if (event.getSource() == registButton) { if (usernameField.getText().equals("") || passwordField.getPassword().length == 0 || emailField.getText().equals("") || teleField.getText().equals("")) { JOptionPane.showMessageDialog(this, "您填写的信息不完整","温馨提示",JOptionPane.INFORMATION_MESSAGE); usernameField.setText(""); passwordField.setText(""); emailField.setText(""); teleField.setText(""); } else if (passwordField.getPassword().length < 6) { JOptionPane.showMessageDialog(this,"密码长度不能小于6位","温馨提示",JOptionPane.INFORMATION_MESSAGE); passwordField.setText(""); } else if (emailField.getText().indexOf("@") < 0 || emailField.getText().indexOf("@") >= (emailField.getText().length() - 1)) { JOptionPane.showMessageDialog(this,"您的邮箱格式不正确","温馨提示",JOptionPane.INFORMATION_MESSAGE); emailField.setText(""); } else if (!checkNumber(teleField.getText())) { JOptionPane.showMessageDialog(this,"电话必须为数字","温馨提示",JOptionPane.INFORMATION_MESSAGE); teleField.setText(""); } else JOptionPane.showMessageDialog(this,"恭喜您,注册成功","温馨提示",JOptionPane.INFORMATION_MESSAGE); } if (event.getSource() == cancelButton) { usernameField.setText(""); passwordField.setText(""); emailField.setText(""); teleField.setText(""); } } public boolean checkNumber(String str) { for (int i=0;i<str.length();i++) { return false; } return true; } public static void main(String args[]) { Text application = new Text(); application.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值