项目之用户认证与服务网关整合

1. 实现思路

1.所有请求都会经过服务网关,服务网关对外暴露服务,不管是api异步请求还是web同步请求都走网关,在网关进行统一用户认证
2.既然要在网关进行用户认证,网关得知道对哪些url进行认证,所以我们得对url制定规则
3.Web页面同请求(如:*.html),我采取配置白名单的形式,凡是配置在白名单里面的请求都是需要用户认证的(注:也可以采取域名的形式,方式多多)
4.Api接口异步请求的,我们采取url规则匹配,如:/api/**/auth/**,如凡是满足该规则的都必须用户认证

2. 在服务网关添加fillter

可先看sso单点登录模块了解存储结构
server-gateway 项目中添加一个过滤器
配置:

spring:
  redis:
    host: 192.168.200.128
    port: 6379
    database: 0
    timeout: 1800000
    password:
    lettuce:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1    #最大阻塞等待时间(负数表示没限制)
        max-idle: 5    #最大空闲
        min-idle: 0     #最小空闲
#配置权限的页面业务
authUrls:
  url: trade.html,myOrder.html,list.html

获取IP地址工具类:


/**
 * 获取ip地址
 */
public class IpUtil {

    public static String getIpAddress(HttpServletRequest request) {
        String ipAddress = null;
        try {
            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();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress="";
        }
        // ipAddress = this.getRequest().getRemoteAddr();

        return ipAddress;
    }
    // 网关中获取Ip地址
    public static String getGatwayIpAddress(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().getHostAddress();
        }
        return ip;
    }
}

常量类:

/**
 * 统一返回结果状态信息类
 *
 */
@Getter
public enum ResultCodeEnum {

    SUCCESS(200,"成功"),
    FAIL(201, "失败"),
    SERVICE_ERROR(2012, "服务异常"),

    PAY_RUN(205, "支付中"),

    LOGIN_AUTH(208, "未登陆"),
    PERMISSION(209, "没有权限"),
    SECKILL_NO_START(210, "秒杀还没开始"),
    SECKILL_RUN(211, "正在排队中"),
    SECKILL_NO_PAY_ORDER(212, "您有未支付的订单"),
    SECKILL_FINISH(213, "已售罄"),
    SECKILL_END(214, "秒杀已结束"),
    SECKILL_SUCCESS(215, "抢单成功"),
    SECKILL_FAIL(216, "抢单失败"),
    SECKILL_ILLEGAL(217, "请求不合法"),
    SECKILL_ORDER_SUCCESS(218, "下单成功"),
    COUPON_GET(220, "优惠券已经领取"),
    COUPON_LIMIT_GET(221, "优惠券已发放完毕"),
    ;

    private Integer code;

    private String message;

    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

/**
 * Redis常量配置类
 * set name admin
 */
public class RedisConst {

    public static final String SKUKEY_PREFIX = "sku:";
    public static final String SKUKEY_SUFFIX = ":info";
    //单位:秒
    public static final long SKUKEY_TIMEOUT = 24 * 60 * 60;
    // 定义变量,记录空对象的缓存过期时间
    // 商品如果在数据库中不存在那么会缓存一个空对象进去,但是这个对象是没有用的,所以这个对象的过期时间应该不能太长,
// 如果太长会占用内存。
// 定义变量,记录空对象的缓存过期时间
    public static final long SKUKEY_TEMPORARY_TIMEOUT = 10 * 60;

    //单位:秒 尝试获取锁的最大等待时间
    public static final long SKULOCK_EXPIRE_PX1 = 1;
    //单位:秒 锁的持有时间
    public static final long SKULOCK_EXPIRE_PX2 = 1;
    public static final String SKULOCK_SUFFIX = ":lock";

    public static final String USER_KEY_PREFIX = "user:";
    public static final String USER_CART_KEY_SUFFIX = ":cart";
    public static final long USER_CART_EXPIRE = 60 * 60 * 24 * 7;

    //用户登录
    public static final String USER_LOGIN_KEY_PREFIX = "user:login:";
    //    public static final String userinfoKey_suffix = ":info";
    public static final int USERKEY_TIMEOUT = 60 * 60 * 24 * 7;

    //秒杀商品前缀
    public static final String SECKILL_GOODS = "seckill:goods";
    public static final String SECKILL_ORDERS = "seckill:orders";
    public static final String SECKILL_ORDERS_USERS = "seckill:orders:users";
    public static final String SECKILL_STOCK_PREFIX = "seckill:stock:";
    public static final String SECKILL_USER = "seckill:user:";
    //用户锁定时间 单位:秒
    public static final int SECKILL__TIMEOUT = 60 * 60 * 1;


}

过滤器:

@Component
public class AuthGlobalFilter implements GlobalFilter {
    //做网关验证配置 从redis中获取数据
    @Autowired
    private RedisTemplate redisTemplate;

    // 匹配路径的工具类
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Value("${authUrls.url}")
    private String authUrls;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取到请求对象
        ServerHttpRequest request = exchange.getRequest();
        // 获取Url
        String path = request.getURI().getPath();
        // 如果是内部接口,则网关拦截不允许外部访问!
        if (antPathMatcher.match("/**/inner/**",path)){
            ServerHttpResponse response = exchange.getResponse();
            return out(response,ResultCodeEnum.PERMISSION);
        }
        // 获取用户Id
        String userId = getUserId(request);
        //token被盗用
        if("-1".equals(userId)) {
            ServerHttpResponse response = exchange.getResponse();
            return out(response,ResultCodeEnum.PERMISSION);
        }
        // 用户登录认证
        //api接口,异步请求,校验用户必须登录
        if(antPathMatcher.match("/api/**/auth/**", path)) {
            if(StringUtils.isEmpty(userId)) {
                ServerHttpResponse response = exchange.getResponse();
                return out(response,ResultCodeEnum.LOGIN_AUTH);
            }
        }
        // 验证url
        for (String authUrl : authUrls.split(",")) {
            // 当前的url包含登录的控制器域名,但是用户Id 为空!
            if (path.indexOf(authUrl)!=-1 && StringUtils.isEmpty(userId)){
                ServerHttpResponse response = exchange.getResponse();
                //303状态码表示由于请求对应的资源存在着另一个URI,应使用重定向获取请求的资源
                response.setStatusCode(HttpStatus.SEE_OTHER);
                response.getHeaders().set(HttpHeaders.LOCATION,"http://www.gmall.com/login.html?originUrl="+request.getURI());
                // 重定向到登录
                return response.setComplete();
            }
        }

        // 将userId 传递给后端
        if (!StringUtils.isEmpty(userId)){
            request.mutate().header("userId",userId).build();
            // 将现在的request 变成 exchange对象
            return chain.filter(exchange.mutate().request(request).build());
        }
        return chain.filter(exchange);
    }
    /**
     * 获取当前登录用户id
     * @param request
     * @return
     */
    private String getUserId(ServerHttpRequest request) {
        String token = "";
        //从请求头或cookies中获取
        List<String> tokenList = request.getHeaders().get("token");
        if(null  != tokenList) {
            token = tokenList.get(0);
        } else {
            MultiValueMap<String, HttpCookie> cookieMultiValueMap =  request.getCookies();
            HttpCookie cookie = cookieMultiValueMap.getFirst("token");
            if(cookie != null){
                token = URLDecoder.decode(cookie.getValue());
            }
        }
        if(!StringUtils.isEmpty(token)) {
            //从缓存中验证
            String userStr = (String) redisTemplate.opsForValue().get(RedisConst.USER_LOGIN_KEY_PREFIX + token);
            if (userStr != null) {
                JSONObject userJson = JSONObject.parseObject(userStr);
                String ip = userJson.getString("ip");
                String curIp = IpUtil.getGatwayIpAddress(request);
                //校验token是否被盗用
                if (ip.equals(curIp)) {
                    return userJson.getString("userId");
                } else {
                    //ip不一致
                    return "-1";
                }
            }
        }
        return "";
    }

    // 接口鉴权失败返回数据
    private Mono<Void> out(ServerHttpResponse response,ResultCodeEnum resultCodeEnum) {
        // 返回用户没有权限登录
        Result<Object> result = Result.build(null, resultCodeEnum);
        byte[] bits = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);
        DataBuffer wrap = response.bufferFactory().wrap(bits);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        // 输入到页面
        return response.writeWith(Mono.just(wrap));
    }
}

3. 在服务网关中判断用户登录状态

在网关中如何获取用户信息:
1,从cookie中获取(如:web同步请求)
2,从header头信息中获取(如:异步请求)
如何判断用户信息合法:
登录时我们返回用户token,在服务网关中获取到token后,我在到redis中去查看用户id,如果用户id存在,则token合法,否则不合法,同时校验ip,防止token被盗用。

3.1 取用户信息

/**
 * 获取当前登录用户id
 * @param request
 * @return
 */
private String getUserId(ServerHttpRequest request) {
    String token = "";
    List<String> tokenList = request.getHeaders().get("token");
    if(null  != tokenList) {
        token = tokenList.get(0);
    } else {
        MultiValueMap<String, HttpCookie> cookieMultiValueMap =  request.getCookies();
        HttpCookie cookie = cookieMultiValueMap.getFirst("token");
        if(cookie != null){
            token = URLDecoder.decode(cookie.getValue());
        }
    }
    if(!StringUtils.isEmpty(token)) {
        String userStr = (String)redisTemplate.opsForValue().get("user:login:" + token);
        JSONObject userJson = JSONObject.parseObject(userStr);
        String ip = userJson.getString("ip");
        String curIp = IpUtil.getGatwayIpAddress(request);
        //校验token是否被盗用
        if(ip.equals(curIp)) {
            return userJson.getString("userId");
        } else {
            //ip不一致
            return "-1";
        }
    }
    return "";
}

3.2 输入信息out 方法

// 接口鉴权失败返回数据
private Mono<Void> out(ServerHttpResponse response,ResultCodeEnum resultCodeEnum) {
    // 返回用户没有权限登录
    Result<Object> result = Result.build(null, resultCodeEnum);
    byte[] bits = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);
    DataBuffer wrap = response.bufferFactory().wrap(bits);
    response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
    // 输入到页面
    return response.writeWith(Mono.just(wrap));
}

3.3 测试

1.通过网关访问内部接口,则不能访问!
http://localhost/api/product/inner/getSkuInfo/17
在这里插入图片描述
2.测试登录权限
测试一:
未登录 :http://localhost/api/product/auth/hello
在这里插入图片描述测试二:
用户在未登录情况下测试:
http://item.gmall.com/api/product/auth/hello
在这里插入图片描述

在上面的访问链接的时候,如果用户登录了,那么还会继续提示未登录!
在这里插入图片描述

404 表示资源没有!没有提示未登录!
原因:
测试一:访问资源的时候,没有获取到userId
测试二:访问资源的时候,获取到了userId

因为:我们登录成功的时候,将token放入了cookie中。在放入cookie的时候,我们给cookie 设置了一个作用域。 return
$.cookie(‘token’, token, {domain: ‘gmall.com’, expires: 7, path: ‘/’})
测试一:使用的域名是localhost,测试二:使用item.gmall.com 包含gmall.com
所以测试二是正确的!以后我们访问的时候,不会通过localhost访问,都是通过域名访问的!

3.验证Url 访问的是控制器
未登录直接访问:会弹出登录页面
http://list.gmall.com/list.html
4.登录之后,然后在访问
会显示查询结果!
http://list.gmall.com/list.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值