zoukankan      html  css  js  c++  java
  • Spring Cloud微服务安全实战_4-9_用zuul网关解耦安全逻辑和业务逻辑

    上一篇通过网关,

    解决了 问题1:微服务场景下,客户端访问服务的复杂性
    未解决 问题2:安全逻辑和业务逻辑的耦合;问题3:微服务过多对认证服务器的压力增大

    本篇将微服务里的安全相关的逻辑挪到网关上来,这样就能解决这两个问题。

     在之前的订单服务里(资源服务器),主要做了两件事:

    1,认证,拿token去认证服务器验令牌

    2,授权,post请求的token必须要有write权限,get请求的token必需要有read权限

     有了网关之后,所有的请求都要走网关来转发到微服务上,所以网关上处理认证和授权,之前篇章说的所有的认证机制都要加到网关上:认证、授权、审计、限流,

    下面开始在网关上实现 认证、授权、审计、限流 

     1,认证Filter 

    新建类过滤器 OAuthFilter,继承 ZuulFilter,重写其方法

    package com.nb.security.filter;
    
    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.constants.ZuulConstants;
    import com.netflix.zuul.context.RequestContext;
    import com.netflix.zuul.exception.ZuulException;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.context.annotation.FilterType;
    import org.springframework.http.*;
    import org.springframework.stereotype.Component;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.client.RestTemplate;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * OAuth认证过滤器
     * Created by: 李浩洋 on 2019-12-28
     **/
    @Slf4j
    @Component
    public class OAuthFilter extends ZuulFilter {
    
        private RestTemplate restTemplate = new RestTemplate();
    
        /**
         * 过滤器类型:
         *  "pre":在业务逻辑执行之前执行run()的逻辑
         *  "post":在业务逻辑执行之后执行run()的逻辑
         *  "error":在业务逻辑抛出异常执行run()的逻辑
         *  "route":控制路由,一般不用这个,zuul已实现
         * @return
         */
        @Override
        public String filterType() {
            return "pre";
        }
    
        //执行顺序
        @Override
        public int filterOrder() {
            return 1;
        }
    
        //是否过滤
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        /**
         * 具体的业务逻辑
         * 这里是认证逻辑,
         */
        @Override
        public Object run() throws ZuulException {
            log.info("oauth start ");
            //获取请求和响应
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
            if(StringUtils.startsWith(request.getRequestURI(),"/token")){
                // /token开头的请求,是发往认证服务器的请求,获取token的,直接放行
                return null;
            }
            //获取请求头的token
            String authHeader = request.getHeader("Authorization");
    
            if(StringUtils.isBlank(authHeader)){
                //如果请求头没有带token,不管认证信息有没有,对不对,都往下走,(要做审计日志)
                return null;
            }
            if(!StringUtils.startsWithIgnoreCase(authHeader,"bearer ")){
                //这个过滤器只处理OAuth认证的请求,不是OAuth的token(如 HTTP basic),也往下走
                return null;
            }
            //走到这里,说明携带的OAuth认证的请求,验token
            try {
                TokenInfo info = getTokenInfo(authHeader);
                request.setAttribute("tokenInfo",info);
            }catch (Exception e){
                log.info("获取tokenInfo 失败!",e);
            }
            return null;
        }
    
        /**
         * 去认证服务器校验token
         * @param authHeader
         * @return
         */
        private TokenInfo getTokenInfo(String authHeader) {
    
            //截取请求头里的bearer token
            String token = StringUtils.substringAfter(authHeader,"bearer ");
            //认证服务器验token地址 /oauth/check_token 是  spring .security.oauth2的验token端点
            String oauthServiceUrl = "http://localhost:9090/oauth/check_token";
    
            HttpHeaders headers = new HttpHeaders();//org.springframework.http.HttpHeaders
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//不是json请求
            //网关的appId,appSecret,需要在数据库oauth_client_details注册
            headers.setBasicAuth("gateway","123456");
    
            MultiValueMap<String,String> params = new LinkedMultiValueMap<>();
            params.add("token",token);
    
            HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers);
            ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);
    
            log.info("token info : {}",response.getBody().toString());
    
            return response.getBody();//返回tokenInfo
        }
    }

    TokenInfo封装token信息:

    package com.nb.security.filter;
    
    import lombok.Data;
    
    import java.util.Date;
    
    /**
     * 包装从认证服务器获取token信息响应对象
     */
    @Data
    public class TokenInfo {
    
        //token是否可用
        private boolean active;
    
        //令牌发给那个客户端应用的 客户端id
        private String client_id;
    
        //令牌scope
        private String[] scope;
    
        //用户名
        private String user_name;
    
        //令牌能访问哪些资源服务器,资源服务器的id
        private String[] aud;
        //令牌过期时间
        private Date exp;
        //令牌对应的user的 权限集合 UserDetailsService里loadUserByUsername()返回的User的权限集合
        private String[] authorities;
    }

     2,审计日志Filter

     审计日志过滤器,请求过来的时候,记录一条日志,请求出去的时候更新日志
    package com.nb.security.filter;
    
    import com.nb.security.entity.AuditLog;
    import com.nb.security.service.IAuditLogService;
    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.context.RequestContext;
    import com.netflix.zuul.exception.ZuulException;
    import jdk.nashorn.internal.parser.Token;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Date;
    
    /**
     * 审计过滤器
     * 1流控--2认证--3审计--4授权
     */
    @Slf4j
    @Component
    public class AuditLogFilter extends ZuulFilter {
    
        @Autowired
        private IAuditLogService auditLogService;
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 2; //在OAuthFilter后
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
    
            log.info(" audit log insert ....");
    
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
            AuditLog log = new AuditLog();
            log.setCreateTime(new Date());
            log.setPath(request.getRequestURI());
            log.setMethod(request.getMethod());
            TokenInfo info = (TokenInfo) request.getAttribute("tokenInfo");
            if(info != null){
                log.setUsername(info.getUser_name());
            }
            auditLogService.save(log);
            request.setAttribute("auditLogId",log.getId());
            return null;
        }
    }

     3,授权过滤器

    在授权过滤器里,需要自己去查数据库,判断当前用户是否有权限。

    package com.nb.security.filter;
    
    import com.nb.security.entity.AuditLog;
    import com.nb.security.service.IAuditLogService;
    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.context.RequestContext;
    import com.netflix.zuul.exception.ZuulException;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.RandomUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Date;
    
    /**
     * 授权过滤器
     */
    @Slf4j
    @Component
    public class AuthorizationFilter extends ZuulFilter {
    
        @Autowired
        private IAuditLogService auditLogService;
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 3; //在审计过滤器后
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            log.info("authorization start");
    
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
    
            //判断是否需要认证
            if(isNeedAuth(request)){
                //需要认证,从request取出AuthFilter放入的tokenInfo
                TokenInfo tokenInfo = (TokenInfo)request.getAttribute("tokenInfo");
                if(tokenInfo != null && tokenInfo.isActive()){//不为空且为激活状态
                    //认证成功,看是否有权限
                    if(!hasPermission(tokenInfo,request)){
                        //没有权限
                        log.info("audit log update fail 403 ");
                        //更新审计日志 ,403
                        Long auditLogId = (Long)request.getAttribute("auditLogId");
                        AuditLog log = auditLogService.getById(auditLogId);
                        log.setUpdateTime(new Date());
                        log.setStatus(403);
                        auditLogService.updateById(log);
    
                        handleError(403,requestContext);
                    }
                    //走到这里说明权限也通过了,将用户信息放到请求头,供其他微服务获取
                    requestContext.addZuulRequestHeader("username",tokenInfo.getUser_name());
    
    
                }else{
                    //不是以 /token开头的,才拦截,否则登录请求也就被拦截了。这里放过
                    if(!StringUtils.startsWith(request.getRequestURI(),"/token")){
                        //////////更新审计日志////////////////
                        log.info("audit log update fail 401 ");
                        Long auditLogId = (Long)request.getAttribute("auditLogId");
                        AuditLog log = auditLogService.getById(auditLogId);
                        log.setUpdateTime(new Date());
                        log.setStatus(401);
                        auditLogService.updateById(log);
    
                        //认证失败,没有tokenInfo,报错,修改审计日志状态
                        handleError(401,requestContext);
                    }
                }
            }
            return null;
        }
    
        /**
         * 认证成功,看是否有权限
         * TODO:从数据库查询权限,这里直接返回
         * @param tokenInfo
         * @param request
         * @return
         */
        private boolean hasPermission(TokenInfo tokenInfo, HttpServletRequest request) {
            return true;//RandomUtils.nextInt() % 2 == 0;
        }
    
    
        /**
         * 处理认证失败或者没有权限
         * @param status http状态码
         * @param requestContext
         */
        private void handleError(int status, RequestContext requestContext) {
            requestContext.getResponse().setContentType("application/json");//响应json
            requestContext.setResponseStatusCode(status);//响应状态码
            requestContext.setResponseBody("{"message":"auth fail"}");
            requestContext.setSendZuulResponse(false);//这一句是说,当前过滤器到此返回,不会再往下走了、
        }
    
        /**
         * 判断当前请求是否需要认证
         * TODO:查数据库判断权限
         * @param request
         * @return
         */
        private boolean isNeedAuth(HttpServletRequest request) {
            return true;
        }
    }

    实验 

    依次启动订单,认证,网关 三个微服务

    在OAuth客户端配置的表里,配上网关的appId,appSecret,使其成为一个OAuth客户端。注意,一定要把client_secret配置正确,配置错误会一直报 HttpClientErrorException$Unauthorized: 401 null异常。

     访问网关获取token:

     访问网关,创建订单:

     一切还算顺利。下面开始删掉订单服务里,关于安全的一些个代码:

    订单服务里,删除oauth2的maven依赖,删除跟资源服务器相关的一切代码,只剩下如下干净的代码:

     目前在其他微服务中获取用户信息的办法是,在网关的授权过滤器中,当一切条件都通过后,将用户信息,添加到Zuul的请求头里,在其他微服务,就可以从请求头中获取用户信息了,甚至可以穿进去一个json字符串,然后取的时候将json字符串转换为对象。(这种做法不好,后续文章介绍其他方法)

     重复上边的实验步骤,依然可以从网关获取token,创建订单!

    代码github:https://github.com/lhy1234/springcloud-security/tree/chapt-4-9-gateway02

  • 相关阅读:
    socket communication between a Java and/or C++ programs (现成的程序)
    java和asp.net之间web Service的创建和调用(2)
    linux下自动启动tomcat
    什么是CPU架构
    Eclipse for Linux on POWER 的安装和使用
    linux开机启动tomcat6
    揭开Socket编程的面纱
    认识错误 武胜
    MySqlCommand, MySqlParameter and "LIKE" with percent symbol zt 武胜
    设置zedgraph鼠标拖拽和局部放大属性 转 武胜
  • 原文地址:https://www.cnblogs.com/lihaoyang/p/12110633.html
Copyright © 2011-2022 走看看