Java Web基础知识之安全:人生苦短,注意安全

关于web程序中的安全方面,想必大多数人都不甚了解,或者说感觉没有必要了解,身边开发网站的人主要就是注重后台的功能和前台的界面,不要说程序的安全问题,甚至后台数据库访问的问题可能都没有下大力气解决。但是这又是和我们密切相关的一个问题,每天看到网站哪个系统或者网站又出现安全问题都感觉离自己很遥远,其实这只是一个错觉,还是那句话——人生苦短,注意安全(某些人不要理解错了,说的就是你。。)。写这篇文章的时候,恰好想起来本屌丝考大学报志愿的时候,那时候北邮新开了一门专业叫信息安全,那个年代还不是很火,但是凭借本屌丝敏锐的洞察力(其实是情怀啦)一眼就看出来了该专业的前景,但是遗憾的是刚刚开办,还不招生。。。遗憾啊!!

保护我们的web程序可以通过声明和编程两种方式来完成,但是不管是哪种方式,都要满足web安全性的这4个方面:

  • 验证:这是我们最熟悉的,每个一开始开发web程序的人都会做一个登录页面,这其实就是在验证web使用者的身份,这里的web使用者不一定是人,也可以是程序,比如某些爬虫程序想要爬取一些页面时,这就需要他们提供用户名和密码;
  • 授权:关于这个应该也比较熟悉,但是由于我们不太关注,导致忽略了这一点,它主要关注被验证使用者的级别,是在上一步验证成功之后进行的,它的作用就是用来限制某个用户是否有权限进入web程序的某一个部分,直观点说,一个网站有普通用户,也有管理员,还有什么内容编辑等等,虽然他们都能登录成功,但是普通用户和页面编辑是不能进入网站的管理界面的,这就是他们没有得到网站拥有者的授权,这里的实现方式是通过建立角色来完成的,给予每个人特定的角色,然后规定一种角色能够访问web程序的哪些部分,最近Facebook奖励给发现Instagram漏洞的10岁儿童一万美金,就是出现了授权的漏洞。
  • 加密:这就比较好理解了,因为数据自互联网上进行传输的时候是从一台计算机传到另一台计算机,等到了服务器时,可能已经经过了不止一台计算机,这就给别人拦截数据提供了极大地方便,因此我们需要对传输的数据进行加密,关于加密算法有很多,慕课网上有很多加密算法的讲解,可以去看一下;
  • 完整:关于数据的完整性,简单来说虽然你加密了传输的数据,但是人家还是可以拦截,可能只是读不懂是什么意思,但是可以随便更改,到接收方就无法确认该数据是否还是从客户端发出的数据,这样就无法保证数据的完整性了,像数字电路中还有奇偶校验位来保证数据传输的正确,在web程序中可以通过建立一个安全通道来传输数据。
关于web应用程序的安全管理是使用声明还是编程呢?其实这是取决于具体的业务要求,它们两个各有各的优点和缺点:
  • 编程:其实大多数web程序使用的都是这种方式,我们不用看这篇文章都知道要怎么做,将用户输入的用户名和密码与存储在服务器上或者数据库中的进行验证,如果验证成功,再看该客户具体的角色。
  • 声明:最大的好处是避免部分编程,因为验证和授权的部分是servlet容器完成的,而且在声明式安全中,浏览器可以在将用户名和密码发到服务器前对其进行加密,由于使用声明,所以所有的安全性约束都不用写到servlet类中,只要在部署描述符中进行声明即可,有很大的灵活性;但是声明这种方式也有缺点,支持数据加密的验证方法只能使用servlet容器(Tomcat)提供的默认登录框,不能定制,在这个看脸的社会无疑已经被淘汰出局,另外如果要使用定制的登录表单就不会对所传输的数据加密。

一、 声明式安全

其实使用声明式验证功能使用的是Http协议中的内容,而不是servlet中的内容(Http功能简直太强大了)。使用声明式安全,首先要做的就是定义用户和角色,不同的servlet容器,用户和角色的保存位置也不同,使用tomcat可以将这些信息保存在配置文件tomcat-users.xml中,然后可以对一些资源的进行安全限制。在tomcat-users.xml中配置如下:
<role rolename="manager-gui"/>
<role rolename="admin"/>
<role rolename="user"/>
<user password="tomcat" roles="manager-gui" username="tomcat"/>
<user password="lmy86263" roles="admin" username="lmy86263"/>
<user password="guest" roles="user" username="guest"/>
在配置tomcat的用户和角色时要注意,每次重启tomcat的时候,你在之前配置的用户和角色都会消失导致恢复到tomcat的默认状态,这是因为在eclipse中初次配置tomcat服务器时,eclipse会将tomcat的配置文件拷贝到自己的workspace下的server文件夹,每次启动读取的配置文件都是从这里读取的,而且还会用这里的配置覆盖tomcat目录下的配置文件,所以为了避免出现这种麻烦,我们将配置好的文件拷贝到workspace下的server文件夹一份即可,如下:

在web程序中将文件放到WEB-INF下或者其子目录下可以隐藏他们,但是在后台可以通过servlet和JSP页面跳转到那里,简单粗暴,不够好。我们可以通过将这些资源放到应用程序目录下,然后通过安全约束来对访问这些资源的请求进行验证,主要是通过在部署描述符中进行配置来实现对一些请求进行约束。
在部署描述符中使用security-constraint和login-config两个元素来完成访问的验证,下面三种类型只是变换其中的几个属性而已。使用如下:
<security-constraint>
	<web-resource-collection>
		<web-resource-name>HttpServlet</web-resource-name>
		<url-pattern>/myHttpServlet</url-pattern>
		<http-method>GET</http-method>
		<http-method>POST</http-method>
	</web-resource-collection>
	
	<auth-constraint>
		<role-name>admin</role-name>
	</auth-constraint>
</security-constraint>

<login-config></login-config>
关于上述几个元素,解释如下:
  • security-constraint:用来指定一个资源集合和可以访问这些资源的一个或者多个角色
  • web-resource-collection:用来指定一组资源集合,这里面有几个元素比较重要,url-pattern就不说了,和之前使用servlet和Filter时一样,就是为了映射一个servlet资源,此处的映射只适用于直接访问该资源,如果是后台forward该资源或者使用JSP标签来访问时不受该安全机制约束的,这个元素可以有多个;http-method元素用来定义http方法,例如上述中使用了GET和POST方法说明安全性约束只适用于这两种方法,也就是说如果使用了PUT或者DELETE方法访问这些资源是不受该安全机制约束的,默认是所有方法都保护,这个元素也可以有多个;还有一个元素这里没有写出来是http-method-omission,这个正好和http-method相反,它是说明除了该属性中的方法之外的所有方法访问该资源时都会被限制,它不能和http-method一起使用;
  • auth-constraint:用来指定可以访问该资源的角色名称,如果没有该元素则说明所有人都可以访问该元素,这样就没有意义;如果该元素存在但是是空的,说明没有人能够直接访问该资源(注意是直接)

1、 基本访问验证

基本访问验证是接受用户名和密码的HTTP验证,如果用户访问一个受保护的资源,则会被服务器拒绝,登录框会一直存在直到你输入正确的用户名和密码,如果你取消认证则会返回401错误,但是要注意的一点是如果你登录成功,但是你的role-name并不在<auth-constraint>中出现,则会返回403错误,所以一定要注意这两种错误是不同的。
<login-config>
	<auth-method>BASIC</auth-method>
	<realm-name>Admin only</realm-name>
</login-config>
使用这种Http验证方式,是将用户名和密码按照"用户名:密码"这种形式组合并且使用Base64算法进行编码传输到服务器,这种算法很弱,在网上随便找一个解码的网站都能知道你的用户名和密码。下面实现出现验证错误和没有授权时的截图:
验证错误:

注意状态码,还有注意Http协议中提供的首部WWW-Authenticate,可以看出这里面的值就是我们在上面配置的属性,这个首部就是用来验证程序使用者的身份的,在其中realm项中包含可以访问该资源的角色名,在登录时就会使用另一个首部Authorization包含使用Base64编码的用户名和密码信息,如下图所示,这里由于取消了验证所以没有发送该首部。注意这两个首部的顺序,当发出登录请求时使用Authorization,然后服务器返回WWW-Authenticate,但是当你取消登录时也会返回WWW-Authenticate,总之一句话,只要你没登陆成功就会返回这个首部。
没有授权:

这里可以看到虽然验证成功了,但是由于该用户对应的角色不在安全约束的范围之内,所以也是被禁止访问该资源的。

2、 摘要访问验证

关于摘要访问验证和基本访问验证类似,只是验证方法使用DIGEST,这种方式不使用Base64算法。这种是使用MD5散列函数计算用户名、密码、realm值三者结合在一起的一个散列值,然后将这个散列值发送到服务器,其中具体的技术见如下链接。
<login-config>
	<auth-method>DIGEST</auth-method>
	<realm-name>Admin only</realm-name>
</login-config>

使用这种方式的请求和响应如下:


3、 表单访问验证 

由于上述两种方式都不不能支持自定义的登录页面,使用基于表单的访问验证可以避免这种尴尬,但是由于这种方式传输的数据是没有加密的,所以它的使用是和其他的加密方法比如SSL等一起使用的。使用这种方式时,首先要自定义登录页面和错误处理页面,验证方式使用FORM,通过下面的声明来完成配置:

<login-config>
	<auth-method>FORM</auth-method>
	<realm-name>Admin only</realm-name>
	<form-login-config>
		<form-login-page>/login.jsp</form-login-page>
		<form-error-page>/error.jsp</form-error-page>
	</form-login-config>
</login-config>
当用户名和密码与数据库或者文件中存储的信息不同时就会返回error.jsp页面,如果匹配成功是不会返回该页面;如果成功匹配,但是该用户的角色并没有权限访问该页面,则不会返回error.jsp页面,而是直接返回403没有权限访问的信息。
关于错误处理页面比较简单这里就不说明了,但是登录页面中有几点要注意,登录页面如下:
<%@ page language="java" contentType="text/html; charset=GBK"
    pageEncoding="GBk"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Login</title>
</head>
<body>
	<form action="j_security_check" method="post">
		用户名: <input type="text" name="j_username" >
		密码: <input type="password" name="j_password">
		<input type="submit">
	</form>
</body>
</html>
在表单中,注意action是j_security_check,用户名是j_username,密码是j_password,这三个字段都是由servlet容器来实现的,这里使用的是tomcat,在tomcat中处理这部分的类是org.apache.catalina.authenticator.FormAuthenticator,对应的代码在authenticate()方法中,如下:
    boolean loginAction = (requestURI.startsWith(contextPath)) && (requestURI.endsWith("/j_security_check"));
    if (!loginAction)
    {
      if ((request.getServletPath().length() == 0) && (request.getPathInfo() == null))
      {
        StringBuilder location = new StringBuilder(requestURI);
        location.append('/');
        if (request.getQueryString() != null)
        {
          location.append('?');
          location.append(request.getQueryString());
        }
        response.sendRedirect(response.encodeRedirectURL(location.toString()));
        return false;
      }
      session = request.getSessionInternal(true);
      if (log.isDebugEnabled()) {
        log.debug("Save request in session '" + session.getIdInternal() + "'");
      }
      try
      {
        saveRequest(request, session);
      }
      catch (IOException ioe)
      {
        log.debug("Request body too big to save during authentication");
        response.sendError(403, sm.getString("authenticator.requestBodyTooBig"));
        
        return false;
      }
      forwardToLoginPage(request, response, config);
      return false;
    }
    request.getResponse().sendAcknowledgement();
    Realm realm = this.context.getRealm();
    if (this.characterEncoding != null) {
      request.setCharacterEncoding(this.characterEncoding);
    }
    String username = request.getParameter("j_username");
    String password = request.getParameter("j_password");
    if (log.isDebugEnabled()) {
      log.debug("Authenticating username '" + username + "'");
    }
    principal = realm.authenticate(username, password);
    if (principal == null)
    {
      forwardToErrorPage(request, response, config);
      return false;
    }
    if (log.isDebugEnabled()) {
      log.debug("Authentication of '" + username + "' was successful");
    }
    if (session == null) {
      session = request.getSessionInternal(false);
    }
    if (session == null)
    {
      if (this.containerLog.isDebugEnabled()) {
        this.containerLog.debug("User took so long to log on the session expired");
      }
      if (this.landingPage == null)
      {
        response.sendError(408, sm.getString("authenticator.sessionExpired"));
      }
      else
      {
        String uri = request.getContextPath() + this.landingPage;
        SavedRequest saved = new SavedRequest();
        saved.setMethod("GET");
        saved.setRequestURI(uri);
        saved.setDecodedRequestURI(uri);
        request.getSessionInternal(true).setNote("org.apache.catalina.authenticator.REQUEST", saved);
        
        response.sendRedirect(response.encodeRedirectURL(uri));
      }
      return false;
    }

二、 编程式安全

现在大多数应用其实都是采用这种方式来实现自己的安全需求的,幸好在servlet中已经有这方面的规范了,让我们很容易使用。但有一点还是没有做好就是虽然不用在配置文件中声明了,但是验证时仍要进行的登录页面还是要在web.xml中使用login-config配置。

1、 使用注解

使用注解时其实完成的就是web.xml中security-constraint的功能,与安全有关的主要是三个注解:
  • @ServletSecurity:包括以下两个注解,对应于security-constraint元素;
  • @HttpConstraint:主要用来添加允许访问该资源的角色,通过rolesAllowed配置;
  • @HttpMethodContraint:主要用来添加被该安全机制所限制的Http方法,它里面也有一个rolesAllowed属性,和上面的注解中的属性要表达的意义是相同的,但是它只作用于;
其中后两个注解的使用可以演变出很多的组合,简单的使用如下:
@WebServlet(name="securityServlet", urlPatterns={"/securityServlet"})
@ServletSecurity(value=@HttpConstraint(rolesAllowed="user"), 
	httpMethodConstraints={@HttpMethodConstraint(value="GET", rolesAllowed="admin")})
public class SecurityServlet extends HttpServlet {}
说明允许user角色使用所有除了GET方法的Http方法访问该资源,但在通过GET方法访问该资源的时候必须验证客户的角色是否是admin。
关于这几个注解的多种组合方式以及具体的含义,可以参考下面的文章。

2、 使用API

除了使用注解,Servlet规范还提供了使用API实现编程式安全,这些API都是在HttpServletRequest中定义,使用如下:
  • getAuthType():返回保护该Servlet的验证方法,对应的是web.xml中的<login-config>中的<auth-method>的值,如果没有验证方法则会返回null;
  • getRemoteUser():返回发出该请求的用户的登录名,如果该用户没有通过验证则会返回null;
  • isUserInRole():标明该经过验证的用户是否属于指定的角色,如果没有经过验证返回false;
  • getUserPrincipal():返回包含被验证用户信息的java.security.Principal,如果未经过验证则返回null;
  • authenticate():命令浏览器显示登录窗口用于对用户进行验证,验证方法使用表单方式时登录窗口为我们自定义的表单,否则使用servlet容器提供给我们的登录窗口;
  • login():用于提供用户名和密码进行登录,登录成功不反返回任何值,登录失败则会抛出ServletException;
  • logout():重置用户信息;
使用实例如下:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
	if(req.authenticate(resp)){
		System.out.println("success");
	}
	else {
		System.out.println("fail");
	}
	
	System.out.println("AuthType: " + req.getAuthType());
	System.out.println("RemoteUser: " + req.getRemoteUser());
	System.out.println("isUserInRole: " + req.isUserInRole("admin"));
	System.out.println("UserPrincipal: " + req.getUserPrincipal());
}
这里使用表单验证方式,所以使用的表单是我们自定义的。
当验证成功时,输出如下:

当验证失败时,输出如下:
从整体上看,servlet提供的针对安全的编程模型还是很方便的,比我们自己通过直接验证数据库中保存的信息要高大上很多。

相关文章:

发布了96 篇原创文章 · 获赞 207 · 访问量 90万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览