zoukankan      html  css  js  c++  java
  • API安全(十)-登陆

    1、登陆

      在前面,我们把图上常见的安全机制都做了一个简单的实现,但是登陆并没有在图中体现,因为并不是每次调用API的时候都需要登陆;登陆只是一个偶尔发生的事情,并不像图中的机制,每一次API的调用都贯穿在其中。但登陆也是整个安全机制中,重要的一环。

    2、之前认证中(HttpBasic)存在的缺陷

      在前面我们实现的HttpBasic认证逻辑中,每次客户端发请求的时候都要把用户的用户名密码通过base64加密传上来,这样有以下缺点:

        2.1、不安全,每次请求都要带用户名和密码,增加了用户名密码泄漏的风险

        2.2、每一次传上来用户名和密码以后都要去做check,加密算法校验比较消耗系统资源

    3、基于token的身份认证

      对于上面的问题,我们可以采用基于token的身份认证,流程如下图;

      这样做的好处是:token跟用户名密码是有关联的,但不是直接的关联,从token中没有办法解析出用户名和密码的。不用每次都传用户名和密码,token在服务器端有一个存储,服务器端从客户端拿到token以后,查一下存储中是否存在,就知道用户是否登陆了,不用像之前那样每次请求都要做密码比对。

    4、基于cookie和session的实现

      对于基于token的身份认证实现有很多,对于java来说最常见的就是基于cookie和session的实现,流程如下图;Web浏览器作为客户端,Servlet容器作为服务器端(tomcat等),服务器端的内存作为token存储。

      上面的这套逻辑Servlet规范里面都替我们实现好了,我们只需要在代码中执行request.getSession(),就会为我们做上面的生成sessionId,返回set-Cookie这些事情。

      优点:提升了客户体验,比客户端保存用户名密码安全;使用起来很方便,Servlet容器都替我们实现好了。

      缺点:只针对浏览器可以使用,APP和第三方应用不支持;服务器向浏览器传递cookie容易被劫持;多台服务器要保证session的一致性。

    5、session固定攻击

      request.getSession()这句代码会根据请求里面cookie的sessionId,在服务器上去找对应的session,如果能找到直接用,如果没找到就会创建一个新的session然后返回回去。针对这样一个逻辑,黑客发明了session固定攻击,如下图

      为了防止session固定攻击,我们要保证登陆前和登陆后的session不是同一个。

    6、代码实现

      6.1、登陆方法实现

        @PostMapping("/login")
        public Map<String, String> login(@RequestBody @Validated(Login.class) UserDTO userDTO,HttpServletRequest request) {
            return userService.login(userDTO,request);
        }
        @Override
        public Map<String, String> login(UserDTO userDTO, HttpServletRequest request) {
    
            Map<String,String> result = Maps.newHashMap();
    
            UserDO userDO = userRepository.findByUsername(userDTO.getUsername());
            if (userDO == null){
                result.put("message","用户名错误");
            }else if (!BCrypt.checkpw(userDTO.getPassword(),userDO.getPassword())){
                result.put("message","密码错误");
            }else {
                HttpSession session = request.getSession(false);
                //将之前的session失效掉
                if (session != null){
                    session.invalidate();
                }
                //将用户信息放到新的session中
                request.getSession(true).setAttribute("user",userDO.buildUserDTO());
    
                result.put("message","登陆成功");
            }
    
            return result;
        }

      6.2、Acl权限控制对登陆请求不需要认证

    /**
     * ACL过滤器,这需要审计也是基于Filter实现的
     *
     * @author caofanqi
     * @date 2020/1/29 15:04
     */
    @Slf4j
    @Order(4)
    @Component
    @SuppressWarnings("ALL")
    public class AclFilter extends OncePerRequestFilter  implements InitializingBean {
    
        @Value("${permit.urls}")
        private String permitUrls;
    
        private Set<String> permitUrlSet = new HashSet<>();
    
        private AntPathMatcher pathMatcher = new AntPathMatcher();
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            log.info("++++++4、授权++++++");
    
            if (isPermitUrl(request)){
                //对于不需要认证和鉴权的请求直接放过
                filterChain.doFilter(request, response);
            }else {
                /*
                 * 要求请求都必须经过认证才能访问
                 */
                UserDTO user = (UserDTO) request.getSession().getAttribute("user");
                if (user == null) {
                    //说明没有进行认证,返回401和WWW-Authenticate,让浏览器弹出输入框
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.setHeader("WWW-Authenticate", "Basic realm=<authentication required>");
                    return;
                }
    
                /*
                 * 要求有对应的权限才可以进行访问
                 */
                if (!hasPermission(user.getPermissions(), request.getMethod())) {
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.getWriter().write("Forbidden");
                    response.getWriter().flush();
                    return;
                }
    
                filterChain.doFilter(request, response);
            }
    
        }
    
        /**
         * 判断是否是直接放过的请求
         */
        private boolean isPermitUrl(HttpServletRequest request) {
            String uri = request.getRequestURI();
            for (String url : permitUrlSet){
                if (pathMatcher.match(url,uri)){
                    // 不需要认证和权限,直接访问
                    return true;
                }
            }
    
            return false;
        }
    
        /**
         *  判断是否有权限
         */
        private boolean hasPermission(String permissions, String method) {
    
            if (StringUtils.equalsIgnoreCase(method, HttpMethod.GET.name())) {
                //要有读权限
                return StringUtils.containsIgnoreCase(permissions, "read");
            } else {
                //要有写权限
                return StringUtils.containsIgnoreCase(permissions, "write");
            }
    
        }
    
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
            Collections.addAll(permitUrlSet,StringUtils.splitByWholeSeparatorPreserveAllTokens(permitUrls,","));
        }
    
    }

      6.3、修改审计功能,从session中获取用户信息

        /**
         * 获取当前登陆用户
         */
        @Bean
        public AuditorAware<String> auditorAware() {
            return () -> {
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                HttpSession session = request.getSession(false);
                String username = "anonymous";
                if (session != null) {
                    UserDTO user = (UserDTO) session.getAttribute("user");
                    if (user != null) {
                        username = user.getUsername();
                    }
                }
    
                return Optional.of(username);
            };
        }

      6.4、修改认证功能,同时支持HttpBasic和cookie、session认证

    /**
     * HttpBasic 认证
     *
     * @author caofanqi
     * @date 2020/1/21 15:10
     */
    @Slf4j
    @Order(2)
    @Component
    @SuppressWarnings("ALL")
    public class BasicAuthorizationFilter extends OncePerRequestFilter {
    
        @Resource
        private UserRepository userRepository;
    
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
            log.info("++++++2、认证++++++");
    
            String authorizationHeader = request.getHeader("Authorization");
    
            if (StringUtils.isNotBlank(authorizationHeader)) {
    
                String token64 = StringUtils.substringAfter(authorizationHeader, "Basic ");
    
                if (StringUtils.isNotBlank(token64)) {
                    try {
                        String token = new String(Base64Utils.decodeFromString(token64));
                        String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(token, ":");
                        String username = items[0];
                        String password = items[1];
    
                        UserDO user = userRepository.findByUsername(username);
    
                        if (user != null && BCrypt.checkpw(password, user.getPassword())) {
    //                    if (user != null && SCryptUtil.check(password,user.getPassword())) {
                            //认证通过,存放用户信息,对于使用httpBasic认证的,添加特殊标记
                            request.getSession().setAttribute("user", user.buildUserDTO());
                            request.getSession().setAttribute("httpBasic", Boolean.TRUE);
                        }
    
                    } catch (Exception e) {
                        log.info("Basic Authorization Fail!");
                    }
                }
    
            }
    
            //不管认证是否正确,继续往下走,是否可以访问,交给授权处理
            filterChain.doFilter(request, response);
    
            //执行完之后,如果是httpBasic方式认证,将session失效
            HttpSession session = request.getSession(false);
            if (session != null && session.getAttribute("httpBasic") != null){
                session.invalidate();
            }
    
        }
    
    }

      6.5、退出功能

        @RequestMapping("/logout")
        public void logout(HttpServletRequest request){
            request.getSession().invalidate();
        }

      6.6、启动项目进行测试

        6.6.1、输入错误的密码进行登陆,在响应头中没有看到Set-Cookie

         6.6.2、输入正确的密码进行登陆,响应头中有Set-Cookie,JSESSIONID=79C8ECFDC0AF3EFD82CAE65FAF226E4C

         6.6.3、访问获取用户的请求,因为登陆了,请求头Cookie中的JSESSIONID=79C8ECFDC0AF3EFD82CAE65FAF226E4C 所以可以访问

        6.6.4、调用退出功能,将原有session失效

         6.6.5、访问获取用户的请求,因为没有关闭浏览器,之前的cookie还在,但是调用了退出使对应的session失效了,所以需要使用httpbasic认证

         6.6.6、通过httpbasic认证后,可以正常访问,说明两种认证方式都支持。

     项目源码:https://github.com/caofanqi/study-security/tree/dev-login

  • 相关阅读:
    tcpdump命令
    浅谈  curl命令
    MongoDB下rs.status()命令
    Device mapper存储方式
    top命令
    cat命令汇总整理
    centos7搭建nginx日志
    CentOS7 防火墙(firewall)的操作命令(转)
    服务器的硬件组成
    shell随机生成10个文件
  • 原文地址:https://www.cnblogs.com/caofanqi/p/12241982.html
Copyright © 2011-2022 走看看