SpringSecurity OAuth2搭建微服务安全认证网关

之前我们搭建了一个SpringSecurity OAuth2的入门案例

SpringSecurity OAuth2密码模式入门案例

在这个案例中我们分别搭建了认证服务器与资源服务器,在请求资源服务器的资源的时候需要在请求头中带上一个token,资源服务器在拿到token之后请求认证服务器去验证token是否正确,验证正确就能允许访问资源服务器的资源。但是在微服务架构下这种模式会导致3个问题:

1.安全认证逻辑与业务逻辑高度耦合。我们可以看到我们之前的案例对于资源服务器来说安全逻辑和业务逻辑是在同一个应用下的,即使我们把安全逻辑打成一个公共的jar包供其他的微服务去使用但是本质还是没有变的,仍然是与我们的业务代码耦合在一起。

2.资源服务器直接与认证服务器打交道,造成认证服务器连接数量增大。在一个微服务架构的系统中,微服务可能是有很多个的,此时每一个微服务都发送请求向认证服务器验证token,这无疑给认证服务器的连接池造成巨大的压力。

3.微服务的暴露,没有一个统一的入口进行访问。显而易见,这里指的就是网关的作用了。

所以我们在这里对之前的安全认证模式架构进行一下改进,如图:

在里面添加网关进行统一管理外部访问微服务的入口,并且将安全认证的逻辑放在网关这一层去解决,从而剥离了与微服务业务逻辑的耦合,而且自始至终与认证服务器打交道的就是网关服务,而网关服务通常都是比较少的,所以认证服务器的连接池也不会被撑满。

添加网关服务

在之前的案例项目中添加一个网关服务

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

application.yml,这里没有添加注册中心eureka,所以不能通过服务名进行转发。

server:
  port: 8989
zuul:
  routes:
    auth-center:
      url: http://localhost:9000
    order-service:
      url: http://localhost:9001
  sensitive-headers:   #设置敏感头为空,表示向网关发起请求所带的headers都会向后转发

 在我们的网关中处理业务逻辑的主要就靠一个个的filter,而对于安全认证的逻辑来说,通常是按照下面的顺序去执行的:

而我们可以把认证,审计,授权分别使用3个filter来对其进行处理。

认证filter

//认证token的filter
@Component
@Slf4j
public class AuthFilter extends ZuulFilter {

    @Autowired
    private RestTemplate restTemplate;


    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        //如果是向认证服务器请求token的url不进行处理
        if (StringUtils.startsWith(request.getRequestURI(), "/auth-center")) {
            return null;
        }

        String oauthHeader = request.getHeader("Authorization");
        if (oauthHeader == null) {
            return null;
        }

        if (!StringUtils.startsWithIgnoreCase(oauthHeader, "bearer ")) {
            return null;
        }


        try {
            //向认证服务器发验证token的请求,拿到验证响应
            TokenInfo tokenInfo = this.getTokenInfo(oauthHeader);
            request.setAttribute("tokenInfo",tokenInfo);
        }catch (Exception ex){
            ex.printStackTrace();
            log.error("get tokenInfo error : {}",ex.getMessage());
        }
        return null;
    }

    private TokenInfo getTokenInfo(String oauthHeader){
        String token = StringUtils.substringAfter(oauthHeader, "Bearer ");
        String oauthServiceUrl = "http://localhost:9000/oauth/check_token";

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        httpHeaders.setBasicAuth("gateway","123456");
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("token", token);
        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, httpHeaders);
        ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);
        log.info("response = {}",response.toString());
        return response.getBody();
    }
}

请求过来的时候首先经过的是认证的filter,用于通过向认证服务器远程请求验证请求头的token是否有效,如果有效的话认证服务器会响应一个TokenInfo对象回来:

/**
 * @author jojo  //认证服务器认证token后返回的数据对象
 *
 */
@Data
public class TokenInfo {

	//token是否可用
	private boolean active;
	//哪个客户端申请的token
	private String client_id;
	//认证成功后返回的权限
	private String[] scope;
	//用户名
	private String user_name;
	//向哪些资源服务器访问
	private String[] aud;
	//token的过期时间
	private Date exp;
	//该用户的权限
	private String[] authorities;
 	
}

认证成功之后会保存在request域中给请求之后下面的逻辑使用,如果认证失败的话,就会被异常捕获到,并且继续传递到下面的授权filter判断request请求域中是否有tokenInfo对象,没有或者token过期的话就提示认证失败。

当然我们还需要在数据库中加上gateway的信息:

审计filter

审计filter是在认证filter之后的,用于记录认证之后的用户访问的信息

//审计filter,记录认证之后过来的信息
@Component
@Slf4j
public class AuditLogFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        TokenInfo tokenInfo = (TokenInfo) requestContext.getRequest().getAttribute("tokenInfo");
        log.info("audit log insert");
        return null;
    }
}

这里就简单打了下日志,具体的日志记录根据业务来定。

授权filter

在这个filter中判断之前认证的结果是否通过,通过之后用户是否权限访问当前的url,所以这个filter中是做出最后的响应的filter

@Component
@Slf4j
public class AuthorizationFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 3;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        HttpServletResponse response = requestContext.getResponse();

        //判断请求的url是否需要权限
        if (isNeedAuth(request)){
            //判断token是否存在且有效
            TokenInfo tokenInfo = (TokenInfo) request.getAttribute("tokenInfo");
            if (tokenInfo !=null && tokenInfo.isActive()){
                //判断token是否有足够的权限访问该url
                if (!hasPermission(request)){
                    //无权限访问
                    handleError(403,requestContext);
                }
                requestContext.addZuulRequestHeader("username", tokenInfo.getUser_name());
            }else{
                if (StringUtils.startsWith(request.getRequestURI(),"/auth-center")){
                    return null;
                }
                handleError(401,requestContext);
            }
        }
        return null;
    }

    private void handleError(int status,RequestContext requestContext) {
        requestContext.getResponse().setContentType("application/json");
        requestContext.getResponse().setStatus(status);
        requestContext.setResponseBody("{\"message\":\"auth fail\"}");
        requestContext.setSendZuulResponse(false);
    }

    //从数据库中判断该用户是否有权限访问该url
    private boolean hasPermission(HttpServletRequest request) {
        return RandomUtils.nextInt(0,20) % 2 == 0;
    }

    private boolean isNeedAuth(HttpServletRequest request) {
        return true;
    }
}

判断该请求的url是否需要权限,需要权限的话再判断认证是否通过,没通过的话返回401,通过的话再判断该用户是否有权限请求,没有权限的话返回403。

自此,我们就完成了利用网关来剥离出安全认证的逻辑与微服务业务逻辑了,而上面还有一步我们是没有做的,就是第一步的限流,这里我们可以使用spring-cloud-zuul-ratelimit这个开源组件去对网关进行处理,这里就不多述说了。注意的是对于网关的限流不要太注重于更加细粒度的限流,比如说按照用户角色来分类去进行限流,这是没什么意义的,因为在微服务内部也会进行互相的远程调用,这样的话微服务之间的调用根本就不走网关,网关再怎么限流也是没用的,所以网关还是适合于条件相对于没那么窄的那种限流。

猜你喜欢

转载自blog.csdn.net/weixin_37689658/article/details/104195224