关于web程序中的安全方面,想必大多数人都不甚了解,或者说感觉没有必要了解,身边开发网站的人主要就是注重后台的功能和前台的界面,不要说程序的安全问题,甚至后台数据库访问的问题可能都没有下大力气解决。但是这又是和我们密切相关的一个问题,每天看到网站哪个系统或者网站又出现安全问题都感觉离自己很遥远,其实这只是一个错觉,还是那句话——人生苦短,注意安全(某些人不要理解错了,说的就是你。。)。写这篇文章的时候,恰好想起来本屌丝考大学报志愿的时候,那时候北邮新开了一门专业叫信息安全,那个年代还不是很火,但是凭借本屌丝敏锐的洞察力(其实是情怀啦)一眼就看出来了该专业的前景,但是遗憾的是刚刚开办,还不招生。。。遗憾啊!!
保护我们的web程序可以通过声明和编程两种方式来完成,但是不管是哪种方式,都要满足web安全性的这4个方面:
- 验证:这是我们最熟悉的,每个一开始开发web程序的人都会做一个登录页面,这其实就是在验证web使用者的身份,这里的web使用者不一定是人,也可以是程序,比如某些爬虫程序想要爬取一些页面时,这就需要他们提供用户名和密码;
- 授权:关于这个应该也比较熟悉,但是由于我们不太关注,导致忽略了这一点,它主要关注被验证使用者的级别,是在上一步验证成功之后进行的,它的作用就是用来限制某个用户是否有权限进入web程序的某一个部分,直观点说,一个网站有普通用户,也有管理员,还有什么内容编辑等等,虽然他们都能登录成功,但是普通用户和页面编辑是不能进入网站的管理界面的,这就是他们没有得到网站拥有者的授权,这里的实现方式是通过建立角色来完成的,给予每个人特定的角色,然后规定一种角色能够访问web程序的哪些部分,最近Facebook奖励给发现Instagram漏洞的10岁儿童一万美金,就是出现了授权的漏洞。
- 加密:这就比较好理解了,因为数据自互联网上进行传输的时候是从一台计算机传到另一台计算机,等到了服务器时,可能已经经过了不止一台计算机,这就给别人拦截数据提供了极大地方便,因此我们需要对传输的数据进行加密,关于加密算法有很多,慕课网上有很多加密算法的讲解,可以去看一下;
- 完整:关于数据的完整性,简单来说虽然你加密了传输的数据,但是人家还是可以拦截,可能只是读不懂是什么意思,但是可以随便更改,到接收方就无法确认该数据是否还是从客户端发出的数据,这样就无法保证数据的完整性了,像数字电路中还有奇偶校验位来保证数据传输的正确,在web程序中可以通过建立一个安全通道来传输数据。
- 编程:其实大多数web程序使用的都是这种方式,我们不用看这篇文章都知道要怎么做,将用户输入的用户名和密码与存储在服务器上或者数据库中的进行验证,如果验证成功,再看该客户具体的角色。
- 声明:最大的好处是避免部分编程,因为验证和授权的部分是servlet容器完成的,而且在声明式安全中,浏览器可以在将用户名和密码发到服务器前对其进行加密,由于使用声明,所以所有的安全性约束都不用写到servlet类中,只要在部署描述符中进行声明即可,有很大的灵活性;但是声明这种方式也有缺点,支持数据加密的验证方法只能使用servlet容器(Tomcat)提供的默认登录框,不能定制,在这个看脸的社会无疑已经被淘汰出局,另外如果要使用定制的登录表单就不会对所传输的数据加密。
一、 声明式安全
- <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文件夹一份即可,如下:- <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、 基本访问验证
- <login-config>
- <auth-method>BASIC</auth-method>
- <realm-name>Admin only</realm-name>
- </login-config>
使用这种Http验证方式,是将用户名和密码按照"用户名:密码"这种形式组合并且使用Base64算法进行编码传输到服务器,这种算法很弱,在网上随便找一个解码的网站都能知道你的用户名和密码。下面实现出现验证错误和没有授权时的截图:这里可以看到虽然验证成功了,但是由于该用户对应的角色不在安全约束的范围之内,所以也是被禁止访问该资源的。
2、 摘要访问验证
- <login-config>
- <auth-method>DIGEST</auth-method>
- <realm-name>Admin only</realm-name>
- </login-config>
使用这种方式的请求和响应如下:
3、 表单访问验证
- <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>
- <%@ page language="java" contentType="text/html; charset=GBK"
- pageEncoding="GBk"%>
- <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www./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;
- }
二、 编程式安全
1、 使用注解
- @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 {}
2、 使用API
- 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());