關于web程序中的安全方面,想必大多數(shù)人都不甚了解,或者說感覺沒有必要了解,身邊開發(fā)網(wǎng)站的人主要就是注重后臺的功能和前臺的界面,不要說程序的安全問題,甚至后臺數(shù)據(jù)庫訪問的問題可能都沒有下大力氣解決。但是這又是和我們密切相關的一個問題,每天看到網(wǎng)站哪個系統(tǒng)或者網(wǎng)站又出現(xiàn)安全問題都感覺離自己很遙遠,其實這只是一個錯覺,還是那句話——人生苦短,注意安全(某些人不要理解錯了,說的就是你。。)。寫這篇文章的時候,恰好想起來本屌絲考大學報志愿的時候,那時候北郵新開了一門專業(yè)叫信息安全,那個年代還不是很火,但是憑借本屌絲敏銳的洞察力(其實是情懷啦)一眼就看出來了該專業(yè)的前景,但是遺憾的是剛剛開辦,還不招生。。。遺憾啊??!
保護我們的web程序可以通過聲明和編程兩種方式來完成,但是不管是哪種方式,都要滿足web安全性的這4個方面:
- 驗證:這是我們最熟悉的,每個一開始開發(fā)web程序的人都會做一個登錄頁面,這其實就是在驗證web使用者的身份,這里的web使用者不一定是人,也可以是程序,比如某些爬蟲程序想要爬取一些頁面時,這就需要他們提供用戶名和密碼;
- 授權:關于這個應該也比較熟悉,但是由于我們不太關注,導致忽略了這一點,它主要關注被驗證使用者的級別,是在上一步驗證成功之后進行的,它的作用就是用來限制某個用戶是否有權限進入web程序的某一個部分,直觀點說,一個網(wǎng)站有普通用戶,也有管理員,還有什么內(nèi)容編輯等等,雖然他們都能登錄成功,但是普通用戶和頁面編輯是不能進入網(wǎng)站的管理界面的,這就是他們沒有得到網(wǎng)站擁有者的授權,這里的實現(xiàn)方式是通過建立角色來完成的,給予每個人特定的角色,然后規(guī)定一種角色能夠訪問web程序的哪些部分,最近Facebook獎勵給發(fā)現(xiàn)Instagram漏洞的10歲兒童一萬美金,就是出現(xiàn)了授權的漏洞。
- 加密:這就比較好理解了,因為數(shù)據(jù)自互聯(lián)網(wǎng)上進行傳輸?shù)臅r候是從一臺計算機傳到另一臺計算機,等到了服務器時,可能已經(jīng)經(jīng)過了不止一臺計算機,這就給別人攔截數(shù)據(jù)提供了極大地方便,因此我們需要對傳輸?shù)臄?shù)據(jù)進行加密,關于加密算法有很多,慕課網(wǎng)上有很多加密算法的講解,可以去看一下;
- 完整:關于數(shù)據(jù)的完整性,簡單來說雖然你加密了傳輸?shù)臄?shù)據(jù),但是人家還是可以攔截,可能只是讀不懂是什么意思,但是可以隨便更改,到接收方就無法確認該數(shù)據(jù)是否還是從客戶端發(fā)出的數(shù)據(jù),這樣就無法保證數(shù)據(jù)的完整性了,像數(shù)字電路中還有奇偶校驗位來保證數(shù)據(jù)傳輸?shù)恼_,在web程序中可以通過建立一個安全通道來傳輸數(shù)據(jù)。
- 編程:其實大多數(shù)web程序使用的都是這種方式,我們不用看這篇文章都知道要怎么做,將用戶輸入的用戶名和密碼與存儲在服務器上或者數(shù)據(jù)庫中的進行驗證,如果驗證成功,再看該客戶具體的角色。
- 聲明:最大的好處是避免部分編程,因為驗證和授權的部分是servlet容器完成的,而且在聲明式安全中,瀏覽器可以在將用戶名和密碼發(fā)到服務器前對其進行加密,由于使用聲明,所以所有的安全性約束都不用寫到servlet類中,只要在部署描述符中進行聲明即可,有很大的靈活性;但是聲明這種方式也有缺點,支持數(shù)據(jù)加密的驗證方法只能使用servlet容器(Tomcat)提供的默認登錄框,不能定制,在這個看臉的社會無疑已經(jīng)被淘汰出局,另外如果要使用定制的登錄表單就不會對所傳輸?shù)臄?shù)據(jù)加密。
一、 聲明式安全
- <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的默認狀態(tài),這是因為在eclipse中初次配置tomcat服務器時,eclipse會將tomcat的配置文件拷貝到自己的workspace下的server文件夾,每次啟動讀取的配置文件都是從這里讀取的,而且還會用這里的配置覆蓋tomcat目錄下的配置文件,所以為了避免出現(xiàn)這種麻煩,我們將配置好的文件拷貝到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算法進行編碼傳輸?shù)椒掌鳎@種算法很弱,在網(wǎng)上隨便找一個解碼的網(wǎng)站都能知道你的用戶名和密碼。下面實現(xiàn)出現(xiàn)驗證錯誤和沒有授權時的截圖:這里可以看到雖然驗證成功了,但是由于該用戶對應的角色不在安全約束的范圍之內(nèi),所以也是被禁止訪問該資源的。
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容器來實現(xiàn)的,這里使用的是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():返回發(fā)出該請求的用戶的登錄名,如果該用戶沒有通過驗證則會返回null;
- isUserInRole():標明該經(jīng)過驗證的用戶是否屬于指定的角色,如果沒有經(jīng)過驗證返回false;
- getUserPrincipal():返回包含被驗證用戶信息的java.security.Principal,如果未經(jīng)過驗證則返回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());



