• Spring Cloud微服务安全实战_6-5_jwt改造之日志以及错误处理(403/401)


    到现在为止基于Jwt的认证和授权的改造已经完成了。在网关上,刚开始都是自己定义一系列的Filter实现认证和授权。现在已经没有了这些过滤器,完全由SpringSecurity的过滤器接管了。

    一、审计日志过滤器

    现在来实现在SpringSecurity过滤器链上加入自己的逻辑,现在的过滤器链上只处理了认证和授权。其他的安全机制比如限流、日志,也需要加入SpringSecurity的实现。

     1,新建审计日志过滤器GatewayAuditLogFilter

    安全处理的几个步骤是   : 流控 - 认证 - 审计 - 授权  。

    注意审计日志过滤器的位置,要添加在认证过滤器之后,所以GatewayAuditLogFilter  上不要直接加@Component 注解,加上该注解,springboot会自动把这个过滤器加在web过滤器链里,如果再自己配置其位置,就会加两次。

    package com.nb.security.filter;
    
    import com.nb.security.entity.AuditLog;
    import com.nb.security.service.IAuditLogService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.actuate.endpoint.SecurityContext;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Date;
    
    /**
     * 审计日志过滤器:
     * 流控 - 认证 - 审计 - 授权
     *  这里不要声名为spring的Component
     *  如果声名了,springboot会自动把这个过滤器加在web过滤器链里,再自己配置其位置就会加两次。
     */
    public class GatewayAuditLogFilter extends OncePerRequestFilter {
    
        //@Autowired
        private IAuditLogService auditLogService;
    
        public GatewayAuditLogFilter(IAuditLogService auditLogService){
            this.auditLogService = auditLogService;
        }
    
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            //认证过滤器会把jwt令牌转换为Authentication放在SecurityContext安全上下文里,Principal就是申请令牌的用户名
            String username = (String)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            //1,记录日志
            AuditLog log = new AuditLog();
            log.setUsername(username);
            log.setMethod(request.getMethod());
            log.setPath(request.getRequestURI());
            log.setCreateTime(new Date());
            auditLogService.save(log);
            System.err.println("1 记录日志 :" + log.toString());
            //2,调用其他过滤器链
            filterChain.doFilter(request,response);
            //3,更新日志
            log.setUpdateTime(new Date());
            log.setStatus(response.getStatus());
            auditLogService.updateById(log);
            System.err.println("3 更新日志 :" + log.toString());
        }
    }

      

      

    2,配置审计日志过滤器位置

     审计日志过滤器需要加在授权过滤器前面,因为授权过滤器会抛出401或403异常,都是由ExceptionTranslationFilter 过滤器处理的,所以把审计日志过滤器加在这个过滤器前面。

    package com.nb.security.config;
    
    import com.nb.security.GatewayWebSecurityExpressionHandler;
    import com.nb.security.filter.GatewayAuditLogFilter;
    import com.nb.security.service.IAuditLogService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
    import org.springframework.security.web.access.ExceptionTranslationFilter;
    
    /**
     * 作为一个资源服务器存在
     */
    @Configuration
    @EnableResourceServer
    public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {
    
        @Autowired
        private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;
    
        @Autowired
        private IAuditLogService auditLogService;
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            //资源服务器id
            //resources.resourceId("gateway");
            //注入自己的 表达式处理器
            resources.expressionHandler(gatewayWebSecurityExpressionHandler);
        }
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                //可以指定过滤器位置,加载授权过滤器前面
                //授权过滤器里,会抛出异常 401或403,这两个异常抛出来后都会由ExceptionTranslationFilter来处理,所以加在这里
                .addFilterBefore(new GatewayAuditLogFilter(auditLogService), ExceptionTranslationFilter.class)
                .authorizeRequests()
                .antMatchers("/token/**").permitAll() //放过/token开头的请求,是在申请令牌
                .anyRequest()
                    //指定权限访问规则,permissionService需要自己实现,返回布尔值,true-能访问;false-无权限
                    // 传进去2个参数,1-当前请求 ,2-当前用户
                .access("#permissionService.hasPermission(request,authentication)");
    
        }
    }

    3,实验

      通过网关获得一个token,再通过网关访问创建订单服务 http://localhost:9070/order/orders

      

       查看打印的日志信息,跟预期结果一致:

      

       审计日志表

      

     二、错误处理

      2.1 403 无权限处理

        默认的403无权限,是由  AccessDeniedHandler 接口的实现类  OAuth2AccessDeniedHandler 来处理的,返回的信息默认是这样的

      

       可以自己指定403的响应信息,新建一个403处理器,继承OAuth2AccessDeniedHandler  ,重写其 handler方法即可

        

    package com.nb.security;
    
    import com.alibaba.druid.support.json.JSONUtils;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
    import com.nb.security.entity.AuditLog;
    import com.nb.security.service.IAuditLogService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 自定义403异常处理器,可以自定义响应信息
     */
    @Component
    public class GatewayAccessDeniedHandler extends OAuth2AccessDeniedHandler {
    
        @Autowired
        private IAuditLogService auditLogService;
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException {
    
            //更新日志信息
            Long logId = (Long)request.getAttribute("logId");
            if(logId != null){
                AuditLog log = new AuditLog();
                log.setUpdateTime(new Date());
                log.setStatus(response.getStatus());
                auditLogService.update(log,new UpdateWrapper<AuditLog>().eq("id",logId));
            }
            //super.handle(request, response, authException); //默认处理
            Map<String,Object> resultMap = new HashMap<>();
            resultMap.put("status",403);
            resultMap.put("msg","sorry! 403");
            response.getWriter().write(JSONUtils.toJSONString(resultMap));
    
            //通知审计日志过滤器,403已经被处理过的标识,那里加个判断,否则就会更新两次
            request.setAttribute("logUpdated","yes");
        }
    }

      审计日志过滤器修改

      

      网关安全配置,配置自定义403 handler

      

       至此完成 自定义403 无权限的处理。

      2.2  401的处理

           

        401有两种情况:

          1,令牌是错的,就不会经过审计日志的过滤器(日志过滤器在认证之后)

             2,没传令牌,都会经过logFilter,又分两种情况:1-通过权限认证(返回的401是微服务返回的),2-未通过权限认证

        网关安全处理,401默认的处理是 OAuth2AuthenticationEntryPoint

        

         GatewayAuthenticationEntryPoint代码:

        

    package com.nb.security;
    
    import com.alibaba.druid.support.json.JSONUtils;
    import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
    import com.nb.security.entity.AuditLog;
    import com.nb.security.service.IAuditLogService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
    import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 自定义401处理
     */
    @Component
    public class GatewayAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
    
        @Autowired
        private IAuditLogService auditLogService;
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
    
            //这里分情况:1,令牌是错的,就不会经过审计日志的过滤器(日志过滤器在认证之后)
            //          2,没传令牌,都会经过logFilter,又分两种情况:1-通过权限认证(返回的401是微服务返回的),2-未通过权限认证
            if(authException instanceof AccessTokenRequiredException){
                //说明没传令牌,但是已记录401日志,这里更新日志
                Long logId = (Long)request.getAttribute("logId");
                if(logId != null){
                    AuditLog log = new AuditLog();
                    log.setUpdateTime(new Date());
                    log.setStatus(response.getStatus());
                    auditLogService.update(log,new UpdateWrapper<AuditLog>().eq("id",logId));
                    System.err.println("自定义处理401,更新日志 logId=" + logId);
                }
            }else {
                //到这里说明令牌错误,没有经过身份认证过滤器,没记录日志,就insert日志
                AuditLog log = new AuditLog();
                log.setUsername("");
                log.setMethod(request.getMethod());
                log.setPath(request.getRequestURI());
                log.setCreateTime(new Date());
                auditLogService.save(log);
                System.err.println("自定义处理401,新增日志");
            }
            super.commence(request,response,authException);
            //通知审计日志过滤器,401已经被处理过的标识,那里加个判断,否则就会更新两次
            request.setAttribute("logUpdated","yes");
        }
    }

      因为现在网关安全配置里, 没有经过认证的请求,也是可以进入权限处理逻辑 permissionService 的(之前都是http.anyRequest().authenticated() 所有的请求必须经过身份认证)

      修改自定义的权限处理逻辑,判断Authentication 如果是 AnonymousAuthenticationToken 说明是没经过认证的,

      没有经过身份认证的请求,SpringSecurity会给一个匿名的 Authentication ----- AnonymousAuthenticationToken ,所以在自定义的权限处理逻辑里,判断如果入参 Authentication 是AnonymousAuthenticationToken  类型,就抛出需要认证token异常。

      

      至此,401自定义处理也完成了。可以下载下代码,跑一下看下打印日志。

      代码 :https://github.com/lhy1234/springcloud-security/tree/chapt-6-4-log

  • 相关阅读:
    数组协变性
    tomcat源码阅读23
    用枚举来实现单例模式
    CSS 的 zindex 属性
    屏幕大小与视区大小
    CSS 生成的模态窗口
    事件处理程序的绑定
    事件对象的属性和使用
    android打电话,接电话,挂电话过程
    ubuntu 12.04编译ics
  • 原文地址:https://www.cnblogs.com/lihaoyang/p/12500103.html
Copyright © 2020-2023  润新知