微服务(六)-SpringCloud Gateway集成登录校验

登录校验功能是每个项目都必须的功能,常见的登陆校验方式有JWT和session.

JWT的优点是无状态,缺点很多,明文传输,无法提前终止,字段过长。所以我们采用JWT加session的方式。JWT中只存放随机生成的token,再通过token去redis中找用户信息。当然也可以直接传token,就是安全性比放JWT中差一点。

第一步,登录完成随机生成token,把用户信息放入redis中

    public String saveUserToken(Integer userId, Integer platform, String ip,
                                Boolean autoLogin, Boolean checkUrl) {
        UserToken userToken = new UserToken();
        String token = UUIDUtils.getUUID();
        userToken.setToken(token);
        userToken.setUserId(userId);
        userToken.setLoginTime(LocalDateTime.now());
        userToken.setIp(ip);
        userToken.setPlatform(platform);
        userToken.setAutoLogin(autoLogin);
        userToken.setCheckUrl(checkUrl);
        //保存token与用户对应关系,便于后面自动登录
        save(userToken);
        User user = userService.getById(userId);
        //获取用户中不常变动的信息放入redis,节省内存
        UserCacheBean userBean = Convert.convert(UserCacheBean.class, user);
        userBean.setCheckUrl(checkUrl);
        userBean.setToken(token);
        //保存用户信息到redis
        saveUserInSession(userBean);
        //生成JWT
        JwtBean jwtBean = new JwtBean();
        jwtBean.setToken(token);
        token = JWTUtil.createToken(BeanUtil.beanToMap(jwtBean), systemConfig.getJwtPassword().getBytes());
        return token;
    }

第二步,网关校验

首先,新建一个过滤器来过滤所有请求

@Component
public class SecurityFilter implements GlobalFilter, Ordered {

接着,我们要排除不需要校验的url,比如登录就不需要校验,通常我们会把不需要校验的路径放入public路径下,比如/basic/public/login,就这样配置/basic/public/**,然后使用AntPathMatcher来校验

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();
        //对于排除的url放行
        String excludePath = systemConfig.getExcludePath();
        if (StringUtils.isNotEmpty(excludePath)) {
            String[] excludePaths = excludePath.split(",");
            for (String pattern : excludePaths) {
                if (pathMatcher.match(pattern, url)) {
                    return chain.filter(exchange);
                }
            }
        }

然后从头字段中取出jwt

        //从头字段或者请求参数中取出token
        String token = exchange.getRequest().getHeaders().getFirst(ConstantKey.TOKEN_HEADER);
        if (StringUtils.isEmpty(token)) {
            token = exchange.getRequest().getQueryParams().getFirst(ConstantKey.TOKEN_HEADER);
        }

接下来校验JWT的有效性

        ServerHttpResponse resp = exchange.getResponse();
        if (StringUtils.isEmpty(token)) {
            return printMsg(Result.UNAUTHORIZED, resp, "请登录");
        }
        //校验jwt签名有效性
        if (!JWTUtil.verify(token, systemConfig.getJwtPassword().getBytes())) {
            return printMsg(Result.UNAUTHORIZED, resp, "token签名错误");
        }

如果JWT没问题则从中取出token,然后去redis中找用户信息

        //解析jwt取出标识redis的token
        final JWT jwt = JWTUtil.parseToken(token);
        JwtBean jwtBean = JsonUtils.parse(jwt.getPayload().toString(), JwtBean.class);
        token = jwtBean.getToken();
        //从session中取user
        UserCacheBean simpleUser = redisSessionService.getUser(token);
        if (simpleUser == null) {
            //自动登录
            Result<UserCacheBean> result = basicPublicService.autoLogin(token);
            if (result.getData() == null) {
                return printMsg(Result.UNAUTHORIZED, resp, "token已过期");
            }
            simpleUser = result.getData();
        }

如果正常找到用户信息,则把用户信息放入token,这样微服务就能直接使用了

        String authUserVo = JsonUtils.serialize(simpleUser);
        ServerHttpRequest newHttpRequest = FilterRequestResponseUtil.getNewHttpRequest(exchange.getRequest()
                , FilterRequestResponseUtil.getNewHttpHeadersConsumer(ConstantKey.TOKEN_HEADER, authUserVo));
        return chain.filter(exchange.mutate()
                .request(newHttpRequest).build());

这是操作gateway输入输出的工具类

public final class FilterRequestResponseUtil {

    public static ServerHttpRequest getNewHttpRequest(ServerHttpRequest httpRequest
            , Consumer<HttpHeaders> httpHeadersConsumer, Flux<DataBuffer> dataBufferFlux) {
        ServerHttpRequest newHttpRequest = httpRequest.mutate()
                .headers(httpHeadersConsumer)
                .build();
        return new ServerHttpRequestDecorator(newHttpRequest) {
            @Override
            public Flux<DataBuffer> getBody() {
                return dataBufferFlux;
            }
        };
    }

    public static ServerHttpRequest getNewHttpRequest(ServerHttpRequest httpRequest
            , Consumer<HttpHeaders> httpHeadersConsumer) {
        return httpRequest.mutate()
                .headers(httpHeadersConsumer)
                .build();
    }

    public static Consumer<HttpHeaders> getNewHttpHeadersConsumer(String headerName, String headerVal) {
        Consumer<HttpHeaders> consumer = headers -> {
            headers.set(headerName, headerVal);
        };
        return consumer;
    }

    /**
     * 认证错误输出
     *
     * @param resp    响应对象
     * @param message 错误信息
     * @return
     */
    public static Mono<Void> printMsg(int status, ServerHttpResponse resp, String message) {
        resp.setStatusCode(HttpStatus.OK);
        resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        Result result = new Result();
        result.setStatus(status);
        result.setMessage(message);
        String returnStr = JsonUtils.serialize(result);
        DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }
}

第三步,微服务校验

微服务的校验就比较简单了,由于微服务是处于内网的,可以明文传输,拿到用户信息放入ThreadLocal直接用就行了

public class SecurityInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader(ConstantKey.TOKEN_HEADER);
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter(ConstantKey.TOKEN_HEADER);
        }
        if (StringUtils.isEmpty(token)) {
            AjaxUtils.printMsg(Result.UNAUTHORIZED, response, "请登录");
            return false;
        }
        //从头字段中取user
        UserCacheBean simpleUser = JsonUtils.parse(token, UserCacheBean.class);
        if (simpleUser == null || simpleUser.getId() == null) {
            AjaxUtils.printMsg(Result.UNAUTHORIZED, response, "无效的token");
            return false;
        }
        UserThreadLocal.set(simpleUser);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.remove();
    }
}


版权声明:本文为ting4937原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。