ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

基于Vue+SpringCloudAlibaba微服务电商项目实战-构建会员服务-006:网关获取真实ip&唯一登陆&异步处理线程存在的问题

2021-05-21 22:57:38  阅读:278  来源: 互联网

标签:Vue String 登录 ip token login 电商 channel


006:网关获取真实ip&唯一登陆&异步处理线程存在的问题

1 上课内容基本的介绍

今日课程任务

  1. 微服务中如何获取真实的客户端ip
  2. 会员登录如何实现唯一登录
  3. 扫码关注登录的实现
  4. 构建微信服务消息模板推送接口

2 微服务如何获取客户端真实ip地址

在会员服务中直接从Request获取请求来源,获取的是网关服务的ip,并不是最终真实的客户端ip。
解决办法:Nginx获取客户端真实ip之后放入请求头中,直接从请求头获取即可。
在这里插入图片描述
在微服务的项目中不能够直接获取到真实的客户端ip地址,原因:中间有多层代理。

客户端vue 发送ajax的请求
Nginx nginx可以获取真实的客户端ip地址,存放到请求头中,请求头中设置该参数 X-Real-IP,value为vue客户端真实的ip地址
网关 从请求头中获取到X-Real-IP(vue客户端真实ip地址)
会员 从请求头中获取到X-Real-IP(vue客户端真实ip地址)

3 基于nginx转发网关转发会员服务访问

本地hosts文件配置127.0.0.1 gw.mayikt.com

会员服务真实地址:
http://127.0.0.1:7070/getTokenUser?token=memberlogin_cd77fb990203405796563178d2e11f3b
网关转发到会员服务:
http://127.0.0.1:81/mayikt-member/getTokenUser?token=memberlogin_cd77fb990203405796563178d2e11f3b
nginx访问网关:
gw.mayikt.com/mayikt-member/getTokenUser?token=memberlogin_cd77fb990203405796563178d2e11f3b

Nginx相关配置

http {
    include       mime.types;
    default_type  application/octet-stream;

  upstream mayiktgwadds {
  server 127.0.0.1:81;
}

server {
  listen 80;
  server_name  gw.mayikt.com;
  location / {
    proxy_pass http://mayiktgwadds/;
	proxy_set_header   Host             $host;
    proxy_set_header   X-Real-IP        $remote_addr;						
    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
  }
}

测试效果:
在这里插入图片描述

4 微服务网关、会员获取nginx设置的真实ip

网关服务新增过滤器

@Component
public class MayiktGatewayFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //  nginx 会从请求头中设置 客户端的真实ip放入网关
        String sourceIp = exchange.getRequest().getHeaders().getFirst("X-Real-IP");
        if (StringUtils.isEmpty(sourceIp)) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("code", "500");
            jsonObject.put("msg", "sourceIp is null");
            DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
            return response.writeWith(Mono.just(buffer));
        }
        // 网关放行 转发到真实服务
        return chain.filter(exchange);
    }
}

改造登录接口,方法增加参数@RequestHeader(“X-Real-IP”) String sourceIp
测试效果:
在这里插入图片描述

5 唯一登录的渠道配置设计

唯一令牌登录设计
PC、Android、Ios等 一个渠道对应只能生成一个token

第一次登录生成令牌
第二次登录之后 之前的token令牌从redis中删除

唯一登录设计的原理思想
代码流程:

  1. 查询登录记录表 条件:渠道+userId+is_availability(当前token是否可用)
    @Select("\n" +
            "SELECT ID AS ID,USER_ID AS USERID,\n" +
            "login_time AS logintime,login_token AS logintoken\n" +
            ",channel AS channel,equipment AS equipment\n" +
            ",is_availability AS is_availability\n" +
            "FROM user_login_log WHERE channel =#{channel} and user_id=#{userId}\n   and is_availability=1;")
UserLoginLogDo selectByUserIdAndLoginType(@Param("userId") Long userId, @Param("channel") String channel);
  1. 没有查询到登录记录情况下说明 该渠道对应的该用户没有登录过,直接向该表插入一条日志记录
  2. 如果查询到有登录记录的情况下,说明该账户在登录状态,将当前状态变为不可用。从redis中移除原来令牌,插入最新的登录日志信息

6 实现唯一登录渠道的验证

后续可能新增或者删除渠道,考虑可用性代码不能写死判断逻辑
—>存放在nacos中动态维护

ChannelUtils 不是公用工具类,建立在会员服务中

@Component
public class ChannelUtils {
    @Value("${mayikt.login.token.channel}")
    private String[] loginTokenChannel;

    public Boolean existChannel(String channel) {
        for (int i = 0; i < loginTokenChannel.length; i++) {
            if (channel.toLowerCase().equals(loginTokenChannel[i])) {
                return true;
            }
        }
        return false;
    }
}

bootstrap.yml新增配置,后期移入nacos中,动态配置

mayikt:
  login:
    token:
      prefix: memberlogin
      channel: android,ios,pc

登录接口改造

@Api(tags = "会员登录接口")
public interface MemberLoginService {

    @PostMapping("/login")
    BaseResponse<JSONObject> login(@RequestBody UserLoginDTO userLoginDto, @RequestHeader("X-Real-IP")
            String sourceIp, @RequestHeader("channel") String channel, @RequestHeader("deviceInfor") String deviceInfor);
}
@RestController
@Slf4j
public class MemberLoginServiceImpl extends BaseApiService implements MemberLoginService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private TokenUtil tokenUtil;

    @Value("${mayikt.login.token.prefix}")
    private String loginTokenPrefix;

    @Autowired
    private AsyncLoginLogManage asyncLoginLogManage;

    @Autowired
    private ChannelUtils channelUtils;

    @Override
    public BaseResponse<JSONObject> login(UserLoginDTO userLoginDTO, String sourceIp, String channel, String deviceInfor) {
        // 参数验证
        String mobile = userLoginDTO.getMobile();
        if (StringUtils.isEmpty(mobile)) {
            return setResultError("mobile参数不能为空");
        }
        String password = userLoginDTO.getPassword();
        if (StringUtils.isEmpty(password)) {
            return setResultError("password参数不能为空");
        }
        // 查询数据库
        String newPassword = MD5Util.MD5(password);
        UserDO loginUserDO = userMapper.login(mobile, newPassword);
        if (loginUserDO == null) {
            return setResultError("手机号码或者密码不正确");
        }
        // 设备信息
        if (StringUtils.isEmpty(deviceInfor)) {
            return setResultError("设备信息不能为空");
        }
        if (!channelUtils.existChannel(channel)) {
            return setResultError("渠道来源错误");
        }
        Long userId = loginUserDO.getUserId();
        String userToken = tokenUtil.createToken(loginTokenPrefix, userId + "");
        JSONObject resultJSON = new JSONObject();
        resultJSON.put("userToken", userToken);

        asyncLoginLogManage.loginLog(userId, sourceIp, new Date(), userToken
                , channel, deviceInfor);
        return setResultSuccess(resultJSON);
    }
}

7 唯一登录业务逻辑的实现

数据库中建表user_login_log

CREATE TABLE `user_login_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `login_ip` varchar(255) DEFAULT NULL,
  `login_time` datetime DEFAULT NULL,
  `login_token` varchar(255) DEFAULT NULL,
  `channel` varchar(255) DEFAULT NULL,
  `equipment` varchar(255) DEFAULT NULL,
  `is_availability` int(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

UserLoginLogMapper

public interface UserLoginLogMapper {

    @Insert("\n" +
            "insert into  user_login_log values(null,#{userId},#{loginIp},now(),#{loginToken},#{channel},#{equipment},1);\n")
    int insertUserLoginLog(UserLoginLogDo userLoginLogDo);

    @Select("\n" +
            "SELECT ID AS ID,USER_ID AS USERID,\n" +
            "login_time AS logintime,login_token AS logintoken\n" +
            ",channel AS channel,equipment AS equipment\n" +
            ",is_availability AS is_availability\n" +
            "FROM user_login_log WHERE channel =#{channel} and user_id=#{userId}\n " +
            "  and is_availability=1;")
    UserLoginLogDo selectByUserIdAndLoginType(@Param("userId") Long userId, @Param("channel") String channel);

    @Update("update user_login_log set is_availability=0 where login_token=#{loginToken};")
    int updateUserTokenNotQuipment(@Param("loginToken") String loginToken);
}

AsyncLoginLogManage

@Component
@Slf4j
public class AsyncLoginLogManage {

    @Autowired
    private UserLoginLogMapper userLoginLogMapper;

    @Autowired
    private TokenUtil tokenUtil;

    @Async
    public void loginLog(Long userId, String loginIp, Date loginTime, String loginToken, String channel,
                         String equipment) {
        // 1. 根据当前的渠道+userid+可用 查询 该用户是否已经登录过
        UserLoginLogDo userLoginLogDo =
                userLoginLogMapper.selectByUserIdAndLoginType(userId, channel);
        // 2. 如果没有查询到记录情况下
        if (userLoginLogDo != null) {
            String oldToken = userLoginLogDo.getLoginToken();
            // 更新数据库token状态
            userLoginLogMapper.updateUserTokenNotQuipment(oldToken);
            // 从redis删除该token
            tokenUtil.delToken(oldToken);
        }
        // 插入最新的token到数据库中
        UserLoginLogDo newUserLoginLogDo = new UserLoginLogDo(userId, loginIp, loginTime, loginToken, channel, equipment);
        userLoginLogMapper.insertUserLoginLog(newUserLoginLogDo);
    }
}

测试会员服务接口唯一登录效果
在这里插入图片描述
如何提示给客户端当前token被人挤下
Ajax开启定时任务,每2s时间检查当前自己的token是否有失效,如果失效给出提示

会员token如何设置合理的有效期
注意token一定要有超时时间,有效期时长根据项目而定,最长一般不超过90天,一般设置7天即可。

8 多线程异步处理操作队列满了如何处理

多线程如何处理异常情况
使用Try catch将异常数据记录在redis或者表中,后期人工补偿或者定时任务补偿。
小项目使用多线程,大型项目使用mq

标签:Vue,String,登录,ip,token,login,电商,channel
来源: https://blog.csdn.net/u012425860/article/details/117136254

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有