前言
在我的软件系统中,如果希望安全系数高一些的话,往往会有用户登陆行为表来记录用户登陆行为,保障用户账号安全,比如记录登陆地址,每次登陆时候读取数据最近几次登陆地点,进行账号安全验证
假如以下是我的用户登陆行为表
实现获取用户登陆地址的方法有很多种,比如通过前端整合第三方gps库发送地址,也可以通过整合ip地址来查询,我这里采取第二种,既然已经获取到了ip,那么就直接使用ip进行解析
前端操作
首先在前后端项目中,前端文件时是部署给nginx来完成负载均衡的,这里我们看nginx的配置文件
nginx主配置
nginx.conf
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
##nginx.conf 把里面注释的内容和静态资源配置相关删除,文件加载 http模块中有许多的server 一个server表示一个虚拟主机
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 引入自定义配置文件
include web.conf/*.conf;
}
这里我是将我前端的配置文件全部放在web.conf目录下的,所以主配置文件只需要负载导入
前端项目的具体
gate-way.conf (我的配置名)
upstream admin-gateway{
server localhost:51603;
}
server {
listen 8803;
location / {
root html/admin-web/;
index index.html;
}
location ~/service_6001/(.*) {
proxy_pass http://admin-gateway/$1;
proxy_set_header HOST $host; # 不改变源请求头的值
proxy_pass_request_body on; #开启获取请求体
proxy_pass_request_headers on; #开启获取请求头
proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP 用于记录ip
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息
}
}
解析:
- localtion模块配置前端读取的根目录
- listen该前端的端口
- upstream admin-gateway反向代理的具体地址
- proxy_set_header X-Real-IP $remote_addr; 重点是这个
- ~/service_6001/(.*) 是一个正则表达式,用于匹配以 “/service_6001/” 开头的请求路径,并捕获括号内的内容。
- proxy_pass http://admin-gateway/$1;:
proxy_pass 用于指定被代理的服务器地址。
在这里,http://admin-gateway 是代理目标的基础地址。
$1 是正则表达式中捕获的第一个分组的变量,即括号内的内容。这是用于在代理目标中构建新的路径
比如
/service_6001/login 代理到admin-gateway就是localhost:51603/login
比如
此时真实请求地址是 localhost:51603/admin/login/in
后端操作
这里使用ip2region , Ip2region开源项目,github地址:https://github.com/lionsoul2014/ip2region。一个离线地址库可以根据ip地址来查询文件中的地址,准确率高达99.9%,吹的话就不说了,这里可以看github查看所有优点
这里直接将整合
导入依赖
<!-- ip2region -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.3</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
ip2region 用于 IP 地址解析,提供了高性能的 IP 地址定位功能。
commons-io 提供了一些常见的、与 I/O 操作相关的工具类,用于简化和增强 Java 的 I/O 操作。
下载离线数据集
去官网下载整个项目,需要的是解压后的data/xdb文件
然后新建ip2region目录把数据文件导入
目录结构
工具类
import org.apache.commons.io.FileUtils;
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.File;
import java.text.MessageFormat;
import java.util.Objects;
public class AddressUtil {
/**
* 当前记录地址的本地DB
*/
private static final String TEMP_FILE_DIR = "/home/admin/app/";
/**
* 根据IP地址查询登录来源
*
* @param ip
* @return
*/
public static String getCityInfo(String ip) {
try {
// 获取当前记录地址位置的文件
String dbPath = Objects.requireNonNull(AddressUtil.class.getResource("/ip2region/ip2region.xdb")).getPath();
File file = new File(dbPath);
//如果当前文件不存在,则从缓存中复制一份
if (!file.exists()) {
dbPath = TEMP_FILE_DIR + "ip.db";
System.out.println(MessageFormat.format("当前目录为:[{0}]", dbPath));
file = new File(dbPath);
FileUtils.copyInputStreamToFile(Objects.requireNonNull(AddressUtil.class.getClassLoader().getResourceAsStream("classpath:ip2region/ip2region.xdb")), file);
}
//创建查询对象
Searcher searcher = Searcher.newWithFileOnly(dbPath);
//开始查询
return searcher.searchByStr(ip);
} catch (Exception e) {
e.printStackTrace();
}
//默认返回空字符串
return "";
}
//todo 测试
public static void main(String[] args) {
System.out.println(getCityInfo(""));
}
}
测试这里ip是我的服务器
库有了,需要ip地址才能解析地方
获取请求ip
之前配置前端是携带真实请求地址的,那么只需要解析即可
这里我直接在controller进行解析
controller
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
AdUserLoginService userService;
@PostMapping("/in")
public ResponseResult login(@RequestBody AdUserDto dto, HttpServletRequest request){
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
System.out.println("登陆用户的ip地址为"+ ipAddress);
return userService.login(dto,ipAddress);
}
}
获取 x-forwarded-for 头部信息:
x-forwarded-for 是一个常用的 HTTP 头部字段,用于表示客户端的原始 IP 地址,如果请求经过了代理服务器,这个字段可能包含了多个 IP 地址,通过逗号分隔。所以在这里,首先尝试从这个字段中获取用户的 IP 地址。
-
如果 x-forwarded-for 为空或者为 “unknown”,尝试获取其他头部信息:
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
如果 x-forwarded-for 为空或者为 “unknown”,则尝试从其他可能包含用户 IP 地址的头部信息中获取。这包括 Proxy-Client-IP 和 WL-Proxy-Client-IP 头部。
3.
如果上述头部信息都未获取到有效的 IP 地址,使用 request.getRemoteAddr():
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
如果前面的头部信息都未获取到有效的 IP 地址,最后使用 request.getRemoteAddr() 获取客户端的 IP 地址。
这里的controller路径是login开头而不是admin/开头是因为我这里使用了网关转发时候省略了前缀
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
# 将全局CORS配置添加到SimpleUrlHandlerMapping
corsConfigurations:
'[/**]':
allowedHeaders: "*"
# 允许所有请求头
allowedOrigins: "*"
# 允许所有来源
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
# 允许的HTTP请求方法,包括OPTIONS(通常应该是OPTIONS而不是OPTION)
routes:
- id: admin
uri: lb://admin
predicates:
- Path=/admin/**
filters:
- StripPrefix= 1
#去掉请求路径中的一个前缀(通常是去掉服务名的前缀) filters 下的 - StripPrefix=1: 当请求匹配到这个路由规则时,会执行此处定义的过滤器。StripPrefix=1 表示去掉请求路径中的一个前缀。在这个场景下,如果请求路径是 /admin/example,经过这个过滤器后,会将 /admin 去掉,转发到后端服务的路径就变成了 /example。
好了这个不是重点,继续讲业务
测试controller是否能获取ip,测试成功
具体实现业务的
业务代码
Service
@Service
public class AdUserLoginServiceImpl extends ServiceImpl<AdUserLoginMapper, AdUserLogin>
implements AdUserLoginService {
@Autowired
AdUserMapper adUserMapper;
@Override
public ResponseResult login(AdUserDto dto, String ipAddress) {
//1.检查参数
if(StringUtils.isBlank(dto.getName()) || StringUtils.isBlank(dto.getPassword())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID,"用户名或密码为空");
}
//2.查询用户
AdUser user = adUserMapper.selectOne(Wrappers.<AdUser>lambdaQuery().eq(AdUser::getName, dto.getName()));
if(user == null){
// 说明没有该用户
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
}
//2.1.检查密码
//3.比对密码
String salt = user.getSalt();
String pwd = dto.getPassword();
// 获取盐加密和数据库密码比较
String codeepwd = DigestUtils.md5DigestAsHex((pwd + salt).getBytes());
if(codeepwd.equals(user.getPassword())){
//4.封装返回数据 jwt
Map<String,Object> map = new HashMap<>();
map.put("token", AppJwtUtil.getToken(user.getId().longValue()));
user.setSalt("");
user.setPassword("");
map.put("user",user);
// 写入登陆行为
AdUserLogin adUserLogin = new AdUserLogin();
adUserLogin.setUserId(user.getId());
// 字符在字符串中的位置索引,如果字符不存在,则返回 -1。通过判断索引是否为 -1
if (ipAddress.indexOf(',')!=-1){
// 一样的效果
// ipAddress=ipAddress.split(",")[0];
ipAddress = ipAddress.substring(0,ipAddress.indexOf(','));
}
adUserLogin.setIp(ipAddress);
// 保存用户登陆地址
adUserLogin.setAddress(AddressUtil.getCityInfo(ipAddress));
adUserLogin.setCreatedTime(new Date());
save(adUserLogin);
return ResponseResult.okResult(map);
}else {
return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
}
}