Summary
In a password-protected web application, it is not only necessary to call the invalidate () method of httpsession to correctly handle the user exit process. Now most browsers have buttons for moving back and forward, allowing users to move back or forward to a page. If a user presses the back button after exiting a web application and the browser presents the cached page to the user, the user may be confused and worry about whether their personal data is secure. Many Web applications force users to close the entire browser when exiting, so that users cannot click the back button. Some others use JavaScript, but it does not work in some client browsers. These solutions are clumsy and cannot be guaranteed to be 100% effective under any circumstances. They also require user experience.
This article provides an example to illustrate how to correctly solve the user exit problem. The author Kevin le first describes a password-protected web application, and then explains how the problem arises and discusses solutions to the problem in the example program. Although this article elaborates on JSP pages, the concepts described by the author can easily be understood as applicable to other web technologies. Finally, the author shows how to use Jakarta Struts to elegantly solve this problem.
Most Web applications do not contain confidential information like bank accounts or credit card data, but once sensitive data is involved, we need to provide a type of password protection mechanism. For example, in a factory, workers access their schedules through web, enter their training courses, and view their salaries. At this time, the application SSL (Secure Socket Layer) is a bit cool, but it is undeniable that we must provide password protection for these applications. Otherwise, workers (that is, Web Application Users) you can snoop the Private confidential information of other employees in the factory.
Similar to the above, there are computers in public places such as libraries and hospitals. In these places, many users use several computers together, so it is vital to protect users' personal data. Well-designed and well-written applications have few requirements on user expertise.
Let's take a look at how a perfect web application is displayed in the Real World: A User accesses a page through a browser. The Web application displays a login page that requires users to enter valid verification information. The user has entered the user name and password. In this case, we assume that the authentication information provided by the user is correct. After the authentication process, the Web application allows the user to browse the areas he or she has access. When a user wants to exit, click the exit button. The web application requires the user to confirm that the user is not; otherwise, the user needs to exit. If the user is sure to exit, the session ends, and the Web application locates on the logon page again. You can leave without worrying about information leakage. Another user sat in front of the same computer and clicked the back button. The web application should not display any page accessed by the previous user. In fact, web applications should stay on the login page until the second user provides the correct authentication information.
Through the example program, the article explains how to implement this function in a web application.
JSP Example
In order to describe the implementation scheme more effectively, this article will begin with the problems encountered in the example application logoutsamplejsp1. This example shows many web applications that do not properly solve the exit process. Logoutsamplejsp1 contains the following JSP pages: Login. jsp, home. jsp, secure1.jsp, secure2.jsp, logout. jsp, loginaction. jsp, and logoutaction. jsp. The page home. JSP, secure1.jsp, secure2.jsp, and logout. JSP is not accessible to unauthenticated users. That is to say, these pages contain important information and should not appear in the browser before or after the user logs in or exits. Login. jsp contains the form used for the user to enter the user name and password. The logout. jsp page contains a form that requires the user to confirm whether to exit. Loginaction. jsp and logoutaction. jsp, as controllers, respectively contain login and exit codes.
The second example application logoutsamplejsp2 shows how to solve the problem in the example logoutsamplejsp1. However, the second application itself has doubts. In specific circumstances, the exit problem still occurs.
The third example application logoutsamplejsp3 has been improved in the second example to completely solve the exit problem.
The last example, logoutsamplestruts, shows how struts effectively solves the login problem.
Note: The examples attached to this document are successfully tested on the latest Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, Firefox, and avantbrowsers.
Login action
Brian pontarelli's classic article J2EE Security: Container versus M discusses different J2EE authentication methods. The article also pointed out that the HTTP protocol and form-based authentication do not provide a mechanism to process user exit. Therefore, the solution is to introduce a custom security implementation mechanism.
The custom security authentication mechanism is generally used to obtain authentication information from the form, and then authenticate to the security domain such as LDAP (Lightweight Directory Access Protocol) or relational database. If the authentication information provided by the user is valid, the login action injects an object into the httpsession object. If an injection object exists in httpsession, it indicates that the user has logged on. For ease of understanding, only one user name is written into httpsession to indicate that the user has logged on. Listing 1 illustrates the login action by extracting a piece of code from the loginaction. jsp page:
Listing 1 //... // Initialize requestdispatcher object; set forward to home page by default Requestdispatcher RD = request. getrequestdispatcher ("home. jsp ");// Prepare connection and statement Rs = stmt.exe cutequery ("select password from user where username = '" + username + "'"); If (Rs. Next () {// query only returns 1 record in the result set; only 1 Password per username which is also the primary key If (Rs. getstring ("password"). Equals (password) {// if valid Password Session. setattribute ("user", username); // saves username string in the Session Object } Else {// password does not match, I. e., invalid user password Request. setattribute ("error", "invalid password ."); RD = request. getrequestdispatcher ("login. jsp "); } } // No record in the result set, I. e., invalid username Else { Request. setattribute ("error", "invalid user name ."); RD = request. getrequestdispatcher ("login. jsp "); } } // As a controller, loginaction. jsp finally either forwards to "login. jsp" or "home. jsp" Rd. Forward (request, response ); //... |
The examples attached to this document use relational databases as security domains, but the ideas described in this article are applicable to any type of security domains.
Logout action
The exit action includes deleting the user name and calling the invalidate () method for the user's httpsession object. Listing 2 illustrates the exit action by extracting a piece of code from the loginoutaction. jsp page:
Listing 2 //... Session. removeattribute ("user "); Session. invalidate (); //... |
Prevents unauthorized access to protected JSP pages
After the authentication information submitted by the user is obtained from the form and verified, the login action simply writes a user name to the httpsession object, and the logout action does the opposite, it deletes the user name from the user's httpsession object and calls the invalidate () method to destroy the httpsession. To make the login and exit actions take effect, all protected JSP pages should first verify that httpsession contains the user name to check whether the current user has logged on. If httpsession contains the user name, that is, the user has logged on to the web application, the Web application will send the remaining JSP pages to the browser. Otherwise, the JSP page will jump to the login page login. jsp. The home. jsp, secure1.jsp, secure2.jsp, and logout. JSP pages all contain the code snippet in listing 3:
Listing 3 //... String username = (string) Session. getattribute ("user "); If (null = username ){ Request. setattribute ("error", "session has ended. Please login ."); Requestdispatcher RD = request. getrequestdispatcher ("login. jsp "); Rd. Forward (request, response ); } //... // Allow the rest of the dynamic content in this JSP to be served to the browser //... |
In this Code segment, the program removes the username string from httpsession. If the string is blank, the Web application automatically suspends the execution of the current page and jumps to the logon page. session has ended is also provided. please log in. if it is not empty, the Web application continues to execute, that is, the remaining page is provided to the user.
Run logoutsamplejsp1
Logoutsamplejsp1 may run in the following situations:
? If the user does not log on, the Web application will correctly stop the protected page home. JSP, secure1.jsp, secure2.jsp, and logout. JSP execution, that is, if you try to access the address of the protected JSP page directly in the browser address bar, the Web application will automatically jump to the login page and prompt session has ended. please log in.
? Similarly, when a user exits, the Web application correctly suspends the execution of the protected pages home. jsp, secure1.jsp, secure2.jsp, and logout. jsp.
? After the user exits, if you click the back button on the browser, the Web application cannot properly protect the protected page-after the session is destroyed (the user exits) protected JSP pages are displayed in the browser again. However, if you click any link on the return page, the Web application will jump to the login page and prompt session has ended. Please log in.
Block browser cache
The root cause of the above problem is that most browsers have a back button. When you click the back button, the browser loads the page from the browser cache instead of re-retrieving the page from the Web server by default. Java-based Web applications do not limit this function. This problem also exists in Web applications based on PHP, ASP, And. net.
After the user clicks the "back" button, the HTTP loop, which usually means from the browser to the server and then from the server to the browser, is not established. It is only the user that interacts with the cache. Therefore, even if the code in listing 3 is included to protect the JSP page, the code will not be executed when the back button is clicked.
The cache is good or bad. Cache does provide some convenience, but you can only feel it when using static html pages or pages based on graphics or influences. On the other hand, web applications are usually data-based and data is frequently changed. It is more important to provide the latest data than to read and display expired data from the cache!
Fortunately, the HTTP header "expires" and "cache-control" provide an application server with a mechanism to control the cache on the browser and proxy server. The HTTP header expires tells the proxy server when its cache page will expire. The header information cache-control defined in the http1.1 specification can notify the browser not to cache any page. When you click the back button, the browser re-accesses the page that the server has obtained. The basic method for using cache-control is as follows:
? No-Cache: forces the cache to obtain a new page from the server.
? No-store: No pages are cached in any environment
The Pragma: No-cache in the http1.0 specification is equivalent to the cache-control: No-cache in the http1.1 specification and can also be contained in header information.
By using the Cache Control of the HTTP header information, the second example uses logoutsamplejsp2 to solve the problem of logoutsamplejsp1. Logoutsamplejsp2 differs from logoutsamplejsp1 in the following code segment, which is added to all protected pages:
//... Response. setheader ("cache-control", "No-Cache"); // forces caches to obtain a new copy of the page from the origin server Response. setheader ("cache-control", "No-store"); // directs caches not to store the page under any circumstance Response. setdateheader ("expires", 0); // causes the proxy cache to see the page as "stale" Response. setheader ("Pragma", "No-Cache"); // HTTP 1.0 backward compatibility String username = (string) Session. getattribute ("user "); If (null = username ){ Request. setattribute ("error", "session has ended. Please login ."); Requestdispatcher RD = request. getrequestdispatcher ("login. jsp "); Rd. Forward (request, response ); } //... |
By setting the header information and checking the user name in httpsession, the browser ensures that the page is not cached. If the user does not log on, the protected JSP page will not be sent to the browser, instead, log on to the login page. JSP.
Run logoutsamplejsp2
After logoutsamplejsp2 is run, the following results are displayed:
? When the user exits and tries to click the back button, the browser will not display the protected page, it will only display the real login page login. jsp at the same time the prompt message session has ended. Please log in.
? However, when you press the back button to return a page that processes user submitted data, the following message is displayed in IE and avantbrowser:
Warning page expired ...... (You must have met)
After you select refresh, the previous JSP page will be displayed in the browser again. Obviously, this is not what we want to see because it violates the purpose of the logout action. In this case, it is likely that a malicious user is trying to obtain data from other users. However, this problem only occurs when the back button corresponds to a page for processing post requests.
Record the Last Logon Time
The reason for the above problem is that the browser resubmit the data in the cache. In this example, the data includes the user name and password. Whether or not security warning information is provided, the browser plays a negative role.
To solve problems in logoutsamplejsp2, login of logoutsamplejsp3. based on username and password, JSP also contains a hidden form field called lastlogon, Which is dynamically initialized with a long value. The long value is the number of milliseconds that have been obtained since January 1, 1970 by calling system. currenttimemillis. When the form in login. jsp is submitted, loginaction. jsp first compares the value in the hidden field with the value in the user database. Only when the value in the form field of lastlogon is greater than the value in the database does the web application think this is a valid login.
To verify login, The lastlogon field in the database must be updated with the lastlogon Value in the form. In the preceding example, when the browser repeatedly submits data, the value of lastlogon in the form is no greater than the value of lastlogon in the database. Therefore, loginaction is transferred to login. JSP page, and prompt session has ended. please log in. listing 5 is the excerpt from loginaction:
Listing 5 //... Requestdispatcher RD = request. getrequestdispatcher ("home. jsp"); // forward to homepage by default //... If (Rs. getstring ("password"). Equals (password) {// if valid Password Long lastlogondb = Rs. getlong ("lastlogon "); If (lastlogonform> lastlogondb ){ Session. setattribute ("user", username); // saves username string in the Session Object Stmt.exe cuteupdate ("update user set lastlogon =" + lastlogonform + "where username = '" + username + "'"); } Else { Request. setattribute ("error", "session has ended. Please login ."); RD = request. getrequestdispatcher ("login. jsp ");} } Else {// password does not match, I. e., invalid user password Request. setattribute ("error", "invalid password ."); RD = request. getrequestdispatcher ("login. jsp "); } //... Rd. Forward (request, response ); //... |
To implement the preceding method, you must record the last logon time of each user. This can be easily implemented by adding the lastlogin field to a table using the relational database security domain. LDAP and other security domains require a little bit of effort, but it is obviously feasible.
There are many methods to indicate the Last Logon Time. The example logoutsamplejsp3 uses the number of milliseconds since January 1, January 1, 1970. This method is also feasible when many people log in with a user account in different browsers.
Run logoutsamplejsp3
The running example logoutsamplejsp3 shows how to properly handle exit issues. Once the user exits, clicking the back button on the browser will not display a protected page in the browser under any circumstances. This example shows how to properly handle exit issues without additional training.
To make the code more concise and effective, some redundant code can be removed. One way is to write the code in Listing 4 to a separate JSP page, which can be referenced through the tag <JSP: Include> other pages.
Exit implementation under Struts Framework
Another alternative solution compared to directly using JSP or JSP/Servlets is to use struts. Adding a framework for processing exit problems for a Struts-based Web application can be elegantly implemented without any effort. This is partly because struts adopts the MVC design pattern, so the model and view are clearly separated. In addition, Java is an object-oriented language that supports inheritance and is easier to reuse code than JSP scripts. In struts, the code in Listing 4 can be transplanted from the JSP page to the execute () method of the action class.
In addition, we can also define a basic class that inherits the struts action class. Its execute () method contains the code in Listing 4. By using the class inheritance mechanism, other classes can inherit the general logic in the basic class to set the HTTP header information and retrieve the username string in the httpsession object. This basic class is an abstract class and defines an abstract method executeaction (). All subclasses that inherit from the base class should implement the exectuteaction () method instead of overwriting it. Listing 6 is part of the code for the base class:
Listing 6 Public abstract class baseaction extends action { Public actionforward execute (actionmapping mapping, actionform form, Httpservletrequest request, httpservletresponse response) Throws ioexception, servletexception {
Response. setheader ("cache-control", "No-Cache"); // forces caches to obtain a new copy of the page from the origin server Response. setheader ("cache-control", "No-store"); // directs caches not to store the page under any circumstance Response. setdateheader ("expires", 0); // causes the proxy cache to see the page as "stale" Response. setheader ("Pragma", "No-Cache"); // HTTP 1.0 backward compatibility
If (! This. userisloggedin (request )){ Actionerrors errors = new actionerrors ();Errors. Add ("error", new actionerror ("Logon. sessionended ")); This. saveerrors (request, errors ); Return Mapping. findforward ("sessionended "); } Return executeaction (mapping, form, request, response ); } Protected abstract actionforward executeaction (actionmapping mapping, Actionform form, httpservletrequest request, httpservletresponse response) Throws ioexception, servletexception; Private Boolean userisloggedin (httpservletrequest request ){ If (request. getsession (). getattribute ("user") = NULL ){ Return false; } Return true; } } |
The code in Listing 6 is very similar to that in Listing 4. It only replaces requestdispatcher forward with actionmapping findforward. In Listing 6, if the username string is not found in httpsession, The actionmapping object will find the forward element named sessionended and jump to the corresponding path. If yes, The subclass executes the business logic that implements the executeaction () method. Therefore, declaring a forward element named sessionended for all subclasses in the profile struts-web.xml is required. Listing 7 uses secure1 action to clarify such a statement:
Listing 7 <Action Path = "/secure1" Type = "com. kevinhle. logoutsamplestruts. secure1action" Scope = "request"> <Forward name = "success" Path = "/WEB-INF/JSPs/secure1.jsp"/> <Forward name = "sessionended" Path = "/login. jsp"/> </Action>
|
Secure1action, a subclass inherited from the baseaction class, implements the executeaction () method instead of overwriting it. The secure1action class does not execute any exit code, such as listing 8:
Public class secure1action extends baseaction { Public actionforward executeaction (actionmapping mapping, actionform form, Httpservletrequest request, httpservletresponse response) Throws ioexception, servletexception {
Httpsession session = request. getsession (); Return (mapping. findforward ("success ")); } } |
You only need to define a base class without additional code work. The above solution is elegant and effective. In any case, writing a common behavior method as a base class that inherits strutsaction is a common experience of many struts projects and is recommended.
Limitations
The above solution is very simple and practical for JSP or struts-based Web applications, but it still has some limitations. In my opinion, these limitations are not crucial.(The limitations are not translated. See the original article)
Conclusion
This article describes the solution to the exit problem. Although the solution is simple and surprising, it works effectively in all circumstances. For JSP and struts, all you need to do is write a code of up to 50 lines and a method to record the user's last logon time. Hybrid use of these solutions in Web applications can protect private data from leakage and increase user experience.