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

posted @ 2020-01-30 01:44  caofanqi  阅读(559)  评论(0编辑  收藏  举报