最近开发的应用需要外协人员实现登录认证,外协人员的密码等信息已经录入到ldap, 需要连接ldap进行登录认证。下面先介绍一下登录的网络旅程图。
一.nginx实现AES加密
nginx请求处理入口(前端请求为json格式)
location /aes {
default_type text/html;
content_by_lua_block{
local access_filter = require 'resty.aes_auth'
local r = access_filter.aes_auth()
ngx.header.content_type = "application/json; charset=UTF-8"
if r == true then
ngx.say([[{"code":200,"message":"Certification successful!","data":true,"logCode":null}]])
else
ngx.say([[{"code":401,"message":"Authentication failed!","data":false,"logCode":null }]])
end
ngx.exit(200)
}
}
openresty请求认证接口脚本
local aes = require "resty.aes"
local cjson = require("cjson.safe")
local http = require("resty.http")
local key = "abcdefmJTNn}8Z#2`"
local iv = "1234567890123456"
local _M = {}
function _M.aes_auth()
ngx.req.read_body()
local args,err = ngx.req.get_body_data()
if (not args) or (err) then
return false
end
local arg_json = cjson.decode(args)
local username = arg_json.username
local password = arg_json.password
if (not username) or (not password) then
return false
end
local cript = aes:new(key, nil, aes.cipher(128, "cbc"), {iv=iv, method=nil})
local pwd = cript:encrypt(password)
if pwd then
pwd = ngx.encode_base64(pwd)
else
return false
end
local httpc = http.new()
local requestBody = {
username = username,
password = pwd
}
local json_body = cjson.encode(requestBody)
local resp,err = httpc:request_uri("http://10.1.1.1:8080", {
method = "POST",
path = "/ldap/authUser",
body = json_body,
headers = { ---header参数
["Content-Type"] = "application/json;charset=UTF-8"
}
})
if err then
return false
end
local result = false
if resp then
local data = cjson.decode(resp.body).data
if data then
result = data
end
end
return result
end
return _M
二.应用服务调用ldap服务
引入依赖
<!--ldap-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<!--aes对称加密-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.56</version>
</dependency>
AES加密解密工具类,需要注意的是nginx不支持PKCS5Padding填充方式。
package com.xxx.xxx.xxx.util;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import lombok.extern.slf4j.Slf4j;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.util.Base64Utils;
/**
* description:AES对称加密工具类
*
* @author: lgq
* @create: 2024-01-26 10:03
*/
@Slf4j
public class AESUtil {
/**
* 日志相关
*/
/**
* 编码
*/
private static final String ENCODING = "UTF-8";
/**
* 算法定义
*/
private static final String AES_ALGORITHM = "AES";
/**
* 指定填充方式
*/
private static final String CIPHER_PADDING = "AES/ECB/PKCS5Padding";
//必须使用PKCS7Padding,因为nginx不支持PKCS5Padding填充方式
private static final String CIPHER_CBC_PADDING = "AES/CBC/PKCS7Padding";
/**
* 偏移量(CBC中使用,增强加密算法强度)
*/
private static final String IV_SEED = "1234567890123456";
private static final String RANDOM_SECRET = "abcefmJTNn}8Z#2`";
static {
// 指定使用bouncycastle包来加密, 引入目的就是为了支持AES/CBC/PKCS7Padding
Security.addProvider(new BouncyCastleProvider());
}
public static String getRandomSecret() {
return RANDOM_SECRET;
}
/**
* AES加密
*
* @param content 待加密内容
* @param aesKey 密码
* @return
*/
public static String encrypt(String content, String aesKey) {
if (StringUtils.isBlank(content)) {
log.info("AES encrypt: the content is null!");
return null;
}
//判断秘钥是否为16位
if (StringUtils.isNotBlank(aesKey) && aesKey.length() == 16) {
try {
//对密码进行编码
byte[] bytes = aesKey.getBytes(ENCODING);
//设置加密算法,生成秘钥
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(CIPHER_PADDING);
//选择加密
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
//根据待加密内容生成字节数组
byte[] encrypted = cipher.doFinal(content.getBytes(ENCODING));
//返回base64字符串
return Base64Utils.encodeToString(encrypted);
} catch (Exception e) {
log.info("AES encrypt exception:" + e.getMessage());
throw new RuntimeException(e);
}
} else {
log.info("AES encrypt: the aesKey is null or error!");
return null;
}
}
/**
* 解密
*
* @param content 待解密内容
* @param aesKey 密码
* @return
*/
public static String decrypt(String content, String aesKey) {
if (StringUtils.isBlank(content)) {
log.info("AES decrypt: the content is null!");
return null;
}
//判断秘钥是否为16位
if (StringUtils.isNotBlank(aesKey) && aesKey.length() == 16) {
try {
//对密码进行编码
byte[] bytes = aesKey.getBytes(ENCODING);
//设置解密算法,生成秘钥
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(CIPHER_PADDING);
//选择解密
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
//先进行Base64解码
byte[] decodeBase64 = Base64Utils.decodeFromString(content);
//根据待解密内容进行解密
byte[] decrypted = cipher.doFinal(decodeBase64);
//将字节数组转成字符串
return new String(decrypted, ENCODING);
} catch (Exception e) {
log.info("AES decrypt exception:" + e.getMessage());
throw new RuntimeException(e);
}
} else {
log.info("AES decrypt: the aesKey is null or error!");
return null;
}
}
/**
* AES_CBC加密
*
* @param content 待加密内容
* @param aesKey 密码
* @return
*/
public static String encryptCBC(String content, String aesKey) {
if (StringUtils.isBlank(content)) {
log.info("AES_CBC encrypt: the content is null!");
return null;
}
//判断秘钥是否为16位
if (StringUtils.isNotBlank(aesKey) && aesKey.length() == 16) {
try {
//对密码进行编码
byte[] bytes = aesKey.getBytes(ENCODING);
//设置加密算法,生成秘钥
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(CIPHER_CBC_PADDING);
//偏移
IvParameterSpec iv = new IvParameterSpec(IV_SEED.getBytes(ENCODING));
//选择加密
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);//, iv
//根据待加密内容生成字节数组
byte[] encrypted = cipher.doFinal(content.getBytes(ENCODING));
//返回base64字符串
return Base64Utils.encodeToString(encrypted);
} catch (Exception e) {
log.info("AES_CBC encrypt exception:" + e.getMessage());
throw new RuntimeException(e);
}
} else {
log.info("AES_CBC encrypt: the aesKey is null or error!");
return null;
}
}
/**
* AES_CBC解密
*
* @param content 待解密内容
* @param aesKey 密码
* @return
*/
public static String decryptCBC(String content, String aesKey) {
if (StringUtils.isBlank(content)) {
log.info("AES_CBC decrypt: the content is null!");
return null;
}
//判断秘钥是否为16位
if (StringUtils.isNotBlank(aesKey) && aesKey.length() == 16) {
try {
//对密码进行编码
byte[] bytes = aesKey.getBytes(ENCODING);
//设置解密算法,生成秘钥
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
//偏移
IvParameterSpec iv = new IvParameterSpec(IV_SEED.getBytes(ENCODING));
// "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(CIPHER_CBC_PADDING);
//选择解密
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
//先进行Base64解码
byte[] decodeBase64 = Base64Utils.decodeFromString(content);
//根据待解密内容进行解密
byte[] decrypted = cipher.doFinal(decodeBase64);
//将字节数组转成字符串
return new String(decrypted, ENCODING);
} catch (Exception e) {
log.info("AES_CBC decrypt exception:" + e.getMessage());
throw new RuntimeException(e);
}
} else {
log.info("AES_CBC decrypt: the aesKey is null or error!");
return null;
}
}
}
ladp配置类
package com.xxx.xxx.xxx.config;
import javax.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
/**
* description:LdapConfig
*
* @author: lgq
* @create: 2024-01-25 10:34
*/
@Configuration
public class LdapConfig {
@Resource
private LdapProperties ldapProperties;
@Bean
public LdapTemplate ldapTemplate() {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(ldapProperties.getUrls());
contextSource.setBase(ldapProperties.getBase());
contextSource.setUserDn(ldapProperties.getUsername());
contextSource.setPassword(ldapProperties.getPassword());
contextSource.afterPropertiesSet();
LdapTemplate ldapTemplate = new LdapTemplate(contextSource);
ldapTemplate.setIgnorePartialResultException(true);
ldapTemplate.setDefaultTimeLimit(1000);
ldapTemplate.setDefaultCountLimit(100);
return ldapTemplate;
}
}
package com.xxx.xxx.xxx.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* description:ldapProperties
*
* @author: lgq
* @create: 2024-01-25 18:13
*/
@Data
@ConfigurationProperties(prefix = "spring.ldap")
public class LdapProperties {
/**
* ldap服务地址
*/
private String urls;
/**
* 用户账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* base路径
*/
private String base;
}
yml文件配置:
spring:
profiles: prod
application:
name: service-xxx
ldap:
urls: "ldap://10.1.1.1:389"
password: "xxxxxxxx"
username: "cn=xxx.LDAP,ou=xxx,ou=xxx,dc=xxx,dc=xxx"
base: "dc=xxx,dc=xxx"
认证服务类
package com.xxx.xxx.xxx.service.impl;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.annotation.Resource;
import com.xxx.xxx.xxx.service.LdapService;
import com.xxx.xxx.xxx.util.AESUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.stereotype.Service;
/**
* description:LdapServiceImpl
*
* @author: lgq
* @create: 2024-01-26 09:26
*/
@Service
@Slf4j
public class LdapServiceImpl implements LdapService {
@Resource
private LdapTemplate ldapTemplate;
/**
* 验证登录用户的账号密码是否正确
* @param username
* @param password
* @return
*/
@Override
public boolean authLoginUser(String username, String password) {
if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) {
return false;
}
/**
* aes对password进行解密
*/
String content = AESUtil.decryptCBC(password, AESUtil.getRandomSecret());
if (ObjectUtils.isEmpty(content)) {
return false;
}
String baseDn = "";
String filter = "sAMAccountName=" + username;
boolean result = false;
try {
result = ldapTemplate.authenticate(baseDn, filter, content);
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
} catch (Error er) {
log.error(er.getMessage(), er);
}
return result;
}
}