SpringSecurity OAuth2 builds a microservice security authentication gateway

Before, we built an introductory case of SpringSecurity OAuth2

Spring Security OAuth2 password mode entry case

In this case, we set up the authentication server and the resource server separately. When requesting the resources of the resource server, we need to bring a token in the request header. The resource server requests the authentication server to verify that the token is correct after receiving the token. Can allow access to the resources of the resource server. But under the microservice architecture, this model will cause three problems:

1. The security authentication logic and business logic are highly coupled. We can see that the security logic and business logic of our previous case are under the same application for the resource server. Even if we put the security logic into a public jar package for other microservices to use, the essence remains the same. Yes, it is still coupled with our business code.

2. The resource server directly interacts with the authentication server, resulting in an increase in the number of authentication server connections. In a system with a microservice architecture, there may be many microservices. At this time, each microservice sends a request to verify the token to the authentication server, which undoubtedly puts huge pressure on the connection pool of the authentication server.

3. Exposure of microservices, there is no unified entrance for access. Obviously, this refers to the role of the gateway.

So we are here to improve the previous security authentication mode architecture, as shown in the figure:

Add a gateway to manage the entrance of external access to microservices in a unified way, and put the logic of security authentication on the gateway layer to solve, thus stripping off the coupling with the business logic of microservices, and the gateway service is the gateway service that deals with the authentication server from beginning to end , And gateway services are usually relatively small, so the connection pool of the authentication server will not be full.

Add gateway service

Add a gateway service to the previous case project

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

application.yml, the registration center eureka is not added here, so it cannot be forwarded by the service name.

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

 The processing of business logic in our gateway mainly relies on filters one by one, and for the logic of security authentication, it is usually executed in the following order:

And we can use three filters to process authentication, audit, and authorization respectively.

Authentication 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();
    }
}

When the request comes in, the first pass is the authentication filter, which is used to verify whether the token in the request header is valid by remotely requesting to the authentication server. If it is valid, the authentication server will respond with a TokenInfo object back:

/**
 * @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;
 	
}

After the authentication is successful, it will be stored in the request field for the following logic to use after the request. If the authentication fails, it will be caught by an exception and passed to the authorization filter below to determine whether there is a tokenInfo object in the request field, or token If it expires, it will prompt authentication failure.

Of course we also need to add gateway information in the database:

Audit filter

The audit filter is after the authentication filter, and is used to record the information accessed by the user after authentication

//审计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;
    }
}

Here is a simple log, the specific log records are determined according to the business.

Authorization filter

In this filter, it is judged whether the result of the previous authentication is passed, and whether the user has permission to access the current URL after passing, so this filter is the filter that makes the final response

@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;
    }
}

Judge whether the requested url requires permission. If permission is required, then judge whether the authentication is passed. If it fails, return 401. If it passes, then judge whether the user has permission request. If there is no permission, return 403.

Since then, we have completed the use of the gateway to separate the logic of security authentication and the business logic of microservices, and there is another step that we did not do, which is the first step of current limiting. Here we can use spring-cloud- zuul-ratelimit is an open source component to process the gateway, so I won’t go into it here. Note that for the gateway's current limiting, don't pay too much attention to more fine-grained current limiting. For example, the current limiting is classified according to user roles. This is meaningless, because remote calls will also be made within the microservices. In this case, the calls between microservices do not go through the gateway at all. No matter how the gateway limits the current, it is useless, so the gateway is still suitable for the kind of current limit with less narrow conditions.

Guess you like

Origin blog.csdn.net/weixin_37689658/article/details/104195224