标签:iter data- 测试 控制 一个 Nid 特殊 == hash
在前面,我们把图上常见的安全机制都做了一个简单的实现,但是登陆并没有在图中体现,因为并不是每次调用API的时候都需要登陆;登陆只是一个偶尔发生的事情,并不像图中的机制,每一次API的调用都贯穿在其中。但登陆也是整个安全机制中,重要的一环。
在前面我们实现的HttpBasic认证逻辑中,每次客户端发请求的时候都要把用户的用户名密码通过base64加密传上来,这样有以下缺点:
2.1、不安全,每次请求都要带用户名和密码,增加了用户名密码泄漏的风险
2.2、每一次传上来用户名和密码以后都要去做check,加密算法校验比较消耗系统资源
对于上面的问题,我们可以采用基于token的身份认证,流程如下图;
这样做的好处是:token跟用户名密码是有关联的,但不是直接的关联,从token中没有办法解析出用户名和密码的。不用每次都传用户名和密码,token在服务器端有一个存储,服务器端从客户端拿到token以后,查一下存储中是否存在,就知道用户是否登陆了,不用像之前那样每次请求都要做密码比对。
对于基于token的身份认证实现有很多,对于java来说最常见的就是基于cookie和session的实现,流程如下图;Web浏览器作为客户端,Servlet容器作为服务器端(tomcat等),服务器端的内存作为token存储。
上面的这套逻辑Servlet规范里面都替我们实现好了,我们只需要在代码中执行request.getSession(),就会为我们做上面的生成sessionId,返回set-Cookie这些事情。
优点:提升了客户体验,比客户端保存用户名密码安全;使用起来很方便,Servlet容器都替我们实现好了。
缺点:只针对浏览器可以使用,APP和第三方应用不支持;服务器向浏览器传递cookie容易被劫持;多台服务器要保证session的一致性。
request.getSession()这句代码会根据请求里面cookie的sessionId,在服务器上去找对应的session,如果能找到直接用,如果没找到就会创建一个新的session然后返回回去。针对这样一个逻辑,黑客发明了session固定攻击,如下图
为了防止session固定攻击,我们要保证登陆前和登陆后的session不是同一个。
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
标签:iter data- 测试 控制 一个 Nid 特殊 == hash
原文地址:https://www.cnblogs.com/caofanqi/p/12241982.html