Springboot+SpringSecurity实现安全认证及原理

目录

一、简介

二、密码加密

三、RSA实现密码传输加密

        1.java后端

        2.web前端

四、安全认证及原理

五、总结

一、简介

        在Spring Security中,密码加密和密码传输加密是一个至关重要的安全措施。

二、密码加密

        在Spring Security中,密码加密的主要目的是保护用户密码的安全,防止在数据库泄露时,攻击者能够直接获取用户的明文密码。因此,我们需要对用户密码进行哈希处理,并使用强哈希算法(如BCrypt、SHA-256等)来确保密码的安全性。

        在Spring Security中,推荐使用BCrypt算法进行密码加密。BCrypt算法具有自适应的盐值(Salt)和多次哈希计算的特点,能够有效抵抗彩虹表攻击和暴力破解。

  代码如下:

/**
 * <p>
 * 自定义密码加密
 * </p>
 *
 * @author 刘易彦
 * @custom.date 2022/3/4 14:12
 */
@Slf4j
public class CustomEncoderPasswordUtils implements PasswordEncoder {

    /**
     * 建议使用security里的加密
     */
    private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

    @Override
    public String encode(CharSequence textPassword) {
        String encode = encoder.encode(textPassword);
        log.info("明文:[{}],加过密后的密文:[{}]", textPassword, encode);
        return encode;
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        boolean matches = encoder.matches(rawPassword, encodedPassword);
        log.info("密码比对结果:{}", matches);
        return matches;
    }

    public static void main(String[] args) {
        String textPassword = "123456";
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encode = encoder.encode(textPassword);
        System.out.println(encode);
    }

}

在存储用户密码时候,调用加密方法(因为在自定义方法里面已经把PasswordEncoder注入bean了,所以我这里可以直接拿到此对象,不清楚的也可以直接把上面的工具类方法改成静态然后调用):

代码如下:

    /**
     * 密码加密器
     */
    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public LayUiAdminResultVo addUser(UserSp userSp) {
        userSp.setPassword(passwordEncoder.encode(userSp.getPassword()));
        int insert = this.userSpMapper.insert(userSp);
        if(insert==1){
            return LayUiAdminResultVo.ok("新增成功");
        }
        return LayUiAdminResultVo.fail("新增失败");
    }

三、RSA实现密码传输加密

        我们的系统一般都有用户、密码,用户登录向后端传送密码,明文传输过程中很容易被抓包盗取。所以这里我们自定义一个加密方式实现全流程加密传输。

        我们以非对称加密RSA为例(我这里的秘钥是存在文件里面的,追求完美的可以存到redis缓存里面)。

1.java后端

解密私钥存在java后端文件。

常量代码如下:

/**
 * @author lyy
 * @version V1.0.0
 * @Description 加解密相关常量
 * @date 2024年12月3日下午2:09:36
 */
public final class SecretConstants {

    /**
     * 数据加密RSA公钥
     */
    public static final String DATA_RSA_PUBLIC_KEY = PropertiesUtils.readProperty("secret.properties",
            "response_data_rsa_public_key");

    /**
     * 数据解密RSA私钥
     */
    public static final String DATA_RSA_PRIVATE_KEY = PropertiesUtils.readProperty("secret.properties",
            "request_data_rsa_private_key");

}

RSA工具类代码如下:

/**
 * @author lyy
 * @description RSA加解密工具类
 * @date 2024年12月3日 下午4:46:33
 */
@Slf4j
public class RSAUtils {

    public static final String CHARSET = "UTF-8";

    public static final String RSA_ALGORITHM = "RSA";

    /**
     * @param keySize
     * @return Map<String, String>
     * @description 生成公钥和私钥
     */
    public static Map<String, String> createKeys(int keySize) {
        // 为RSA算法创建一个KeyPairGenerator对象
        KeyPairGenerator kpg;
        try {
            kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
        }

        // 初始化KeyPairGenerator对象,密钥长度
        kpg.initialize(keySize);
        // 生成密匙对
        KeyPair keyPair = kpg.generateKeyPair();
        // 得到公钥
        Key publicKey = keyPair.getPublic();
        String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
        // 得到私钥
        Key privateKey = keyPair.getPrivate();
        String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
        Map<String, String> keyPairMap = new HashMap<String, String>();
        keyPairMap.put("publicKey", publicKeyStr);
        keyPairMap.put("privateKey", privateKeyStr);

        return keyPairMap;
    }

    /**
     * @param publicKey 密钥字符串(经过base64编码)
     * @return RSAPublicKey
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @description 得到公钥
     */
    public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
        // 通过X509编码的Key指令获得公钥对象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
        RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
        return key;
    }

    /**
     * @param privateKey 密钥字符串(经过base64编码)
     * @return RSAPrivateKey
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @description 得到私钥
     */
    public static RSAPrivateKey getPrivateKey(String privateKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        // 通过PKCS#8编码的Key指令获得私钥对象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
        RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
        return key;
    }

    /**
     * @param data      数据
     * @param publicKey 公钥
     * @return String
     * @description 公钥加密
     */
    public static String publicEncrypt(String data, RSAPublicKey publicKey) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET),
                    publicKey.getModulus().bitLength()));
        } catch (Exception e) {
            throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
        }
    }

    /**
     * @param data       数据
     * @param privateKey 私钥
     * @return String
     * @description 私钥解密
     */
    public static String privateDecrypt(String data, RSAPrivateKey privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data),
                    privateKey.getModulus().bitLength()), CHARSET);
        } catch (Exception e) {
            throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
        }
    }

    /**
     * @param data       数据
     * @param privateKey 私钥
     * @return String
     * @description 私钥加密
     */
    public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, privateKey);
            return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET),
                    privateKey.getModulus().bitLength()));
        } catch (Exception e) {
            throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
        }
    }

    /**
     * @param data      数据
     * @param publicKey 公钥
     * @return String
     * @description 公钥解密
     */
    public static String publicDecrypt(String data, RSAPublicKey publicKey) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data),
                    publicKey.getModulus().bitLength()), CHARSET);
        } catch (Exception e) {
            throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
        }
    }

    /**
     * @param data       待签名数据
     * @param privateKey 私钥
     * @return String
     * @throws Exception
     * @description 签名
     */
    public static String sign(String data, PrivateKey privateKey) throws Exception {
        byte[] keyBytes = privateKey.getEncoded();
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        PrivateKey key = keyFactory.generatePrivate(keySpec);
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initSign(key);
        signature.update(data.getBytes());
        return new String(Base64.encodeBase64(signature.sign()));
    }

    /**
     * @param srcData   原始字符串
     * @param publicKey 公钥
     * @param sign      签名
     * @return boolean 是否验签通过
     * @throws Exception
     * @description 验签
     */
    public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception {
        byte[] keyBytes = publicKey.getEncoded();
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        PublicKey key = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initVerify(key);
        signature.update(srcData.getBytes());
        return signature.verify(Base64.decodeBase64(sign.getBytes()));
    }

    private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
        int maxBlock = 0;
        if (opmode == Cipher.DECRYPT_MODE) {
            maxBlock = keySize / 8;
        } else {
            maxBlock = keySize / 8 - 11;
        }

        int offSet = 0;
        byte[] buff;
        byte[] resultDatas;
        int i = 0;
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            while (datas.length > offSet) {
                if (datas.length - offSet > maxBlock) {
                    buff = cipher.doFinal(datas, offSet, maxBlock);
                } else {
                    buff = cipher.doFinal(datas, offSet, datas.length - offSet);
                }
                out.write(buff, 0, buff.length);
                i++;
                offSet = i * maxBlock;
            }
            resultDatas = out.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
        }
        return resultDatas;
    }
}

读取文件秘钥的工具类代码如下:

/**
 * @author liuyiyan
 * @Description java读取.properties文件
 * @date 2024/12/04
 */
public class PropertiesUtils {

    private final static Logger LOGGER = LoggerFactory.getLogger(PropertiesUtils.class);

    /**
     * @date 2024/12/04
     * @author liuyiyan
     * @func 读取资源属性文件(properties),无缓存方式
     */
    public static String readPropertyNoCache(String filePath, String param) {
        try {
            String url = PropertiesUtils.class.getResource("/").getPath() + filePath;
            Properties prop = new Properties();
            InputStream in = new BufferedInputStream(new FileInputStream(url));
            // 将属性文件流装载到Properties对象中
            // prop.load(in);
            prop.load(new InputStreamReader(in, "utf-8"));
            return prop.getProperty(param);
        } catch (IOException e) {
            LOGGER.error("读properties属性文件异常!", e);
        }
        return null;
    }

    /**
     * @date 2024/12/04
     * @author liuyiyan
     * @func 读取资源属性文件(properties),用IO流的方式
     */
    public static String readProperty(String filePath, String param) {
        // 属性集合对象
        Properties properties = new Properties();
        // 获取路径并转换成流
        InputStream path = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
        try {
            // 将属性文件流装载到Properties对象中
            properties.load(path);
            return properties.getProperty(param);
        } catch (IOException e) {
            LOGGER.error("读properties属性文件异常!", e);
        }
        return null;
    }

    /**
     * @date 2024/12/04
     * @author liuyiyan
     * @func 读取资源属性文件(properties),然后根据.properties文件的名称信息(本地化信息)
     */
    public static String getProperty(String filePath, String param) {
        ResourceBundle resourceBundle = ResourceBundle.getBundle(filePath);
        return resourceBundle.getString(param);
    }

    /**
     * @param propertiesFile
     * @param param
     * @return map
     * @description 读取.properties配置文件的内容至Map中
     */
    public static Map<String, String> read2Map(String propertiesFile, String param) {
        ResourceBundle rb = ResourceBundle.getBundle(propertiesFile);
        Map<String, String> map = new HashMap<String, String>(16);
        Enumeration<String> enu = rb.getKeys();
        while (enu.hasMoreElements()) {
            String obj = enu.nextElement();
            // 传了参数
            if (StringUtils.isNotEmpty(param)) {
                if (obj.indexOf(param) != -1) {
                    String objv = rb.getString(obj);
                    map.put(obj, objv);
                }
            }
            // 没传参数
            else {
                String objv = rb.getString(obj);
                map.put(obj, objv);
            }
        }
        return map;
    }

    /**
     * @param filePath
     * @param pKey
     * @param pValue
     * @func 写properties文件
     */
    public static boolean writeProperties(String filePath, String pKey, String pValue) {
        try {
            String url = PropertiesUtils.class.getResource("/").getPath() + filePath;
            Properties prop = new Properties();
            InputStream in = new BufferedInputStream(new FileInputStream(url));
            // 将属性文件流装载到Properties对象中
            prop.load(in);
            // 调用 Hashtable 的方法 put。使用 getProperty 方法提供并行性。
            // 强制要求为属性的键和值使用字符串。返回值是 Hashtable 调用 put 的结果。
            OutputStream out = new FileOutputStream(url);
            prop.setProperty(pKey, pValue);
            // 以适合使用 load 方法加载到 Properties 表中的格式,
            // 将此 Properties 表中的属性列表(键和元素对)写入输出流
            prop.store(out, "Update " + pKey + " name");
            return true;
        } catch (Exception e) {
            LOGGER.error("写properties属性文件异常!", e);
            return false;
        }
    }
}

2.web前端

加密的公钥存在web前端。然后使用jsencrypt.min.js进行rsa加密。

资源链接:https://download.csdn.net/download/milk_yan/90085095

加密代码如下:

        /**
         * 对字符串进行RSA加密
         * @param str 待加密字符串
         * @returns {null|string|undefined|PromiseLike<ArrayBuffer>|*}
         */
        rsaEncrypt: function (str) {
            try {
                let publicKey = '-----BEGIN PUBLIC KEY-----' + obj.webConst.RSA_DATA_ENCRYPT_PUBLIC_KEY + '-----END PUBLIC KEY-----';
                let encrypt = new JSEncrypt();
                encrypt.setPublicKey(publicKey);
                return encrypt.encrypt(str);
            } catch (e) {
                console.error(e.name + ',' + e.message);
                alert(e.name + ',' + e.message);
                throw e;
            }
        },

四、安全认证及原理

       因为springsecurity的加密是单向的无法进行解密,所以在认证的时候需要将前端传来的密码进行同步加密然后和库里加过密的密码交给springsecurity进行匹配对比(我这里的代码是自定义的认证方式)。

代码如下:

/**
 * <p>
 * 自定义springSecurity认证。
 * </p>
 *
 * @author 刘易彦
 * @custom.date 2024/6/21 17:23
 */
@Component
public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Resource
    private IUserService userService;

    /**
     * 密码加密器
     */
    @Resource
    private PasswordEncoder passwordEncoder;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @SneakyThrows
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object principal = authentication.getPrincipal();
        Object credentials = authentication.getCredentials();
        if (principal == null || credentials == null) {
            throw new BadCredentialsException("账号或密码不能为空!");
        }
        String account;
        String password;
        try {
            // 解密账号
            account = RSAUtils.privateDecrypt(String.valueOf(principal), RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
            // 解密密码
            password = RSAUtils.privateDecrypt(String.valueOf(credentials), RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
        } catch (Exception e) {
            throw new BadCredentialsException("账号或密码解密错误!");
        }
        // 拿到后端查询到的账号密码信息进行比较
        if (!StringUtils.equals(account, userDetails.getUsername())) {
            throw new BadCredentialsException("账号或密码错误!");
        }
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("账号或密码错误!");
        }
    }


    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        try {
            username = RSAUtils.privateDecrypt(username, RSAUtils.getPrivateKey(SecretConstants.DATA_RSA_PRIVATE_KEY));
            //查询用户角色权限
            UserDetails userDetails = userService.loadUserByUsername(username);
            if (userDetails == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return userDetails;
        } catch (UsernameNotFoundException | InternalAuthenticationServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }
}

五、总结

        经过以上安全加密保证系统的安全性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值