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

原文地址:https://www.cnblogs.com/lihaoyang/p/12110633.html