Javanomicon01 - Case Study 3 - Password Protection

From Monkeys @ Keyboards
Jump to: navigation, search

Case Study 3 - Password Protection

Previous TOC Next
Javanomicon01 - Inheritance The Javanomicon Javanomicon01 - Free Standing Applications


Introduction

We've already looked at a case study of sorts that concerns the idea of object orientation, but it is such a fundamentally important part of development in Java that it won't hurt to look at another example of how this technique can be applied in the building of programming solutions.

In this case study, we'll look at a scenario concerning when we have a set of requirements and a predefined object that we must not change (for compatibility purposes). The code of course is going to be of a necessity somewhat more simplistic than would be the case in a corresponding real world scenario, but it will hopefully demonstrate how object orientation can be used to good effect through the use of extension and specialisation.

The Scenario

The company 'Secure Systems Inc' has requested an upgrade to their popular password security system. The security system is provided as an applet to corporations seeking to secure their intranet systems.

12.1: Applet Front-End

As the system currently stands, it only provides a simple interface that permits or denies access based on the input of a suitable login/password combination. By default the security applet provided comes with three accounts:

12.2: Default Passwords

The code package comes with three files. The first file is the HTML file that runs the applet. The second file is a Java file containing the applet code, and the third file is a file called Password.java that contains the details of individual accounts.

Secure Systems Inc have requested that you provide some additional functionality to the package and update the interface accordingly. They have specified that the original Password.java file is not to be altered... this is for reasons of backwards compatibility. Instead any new functionality should be done as an extension of the original file.

The new functionality required is as follows:

Access Granularity

The application must allow for accounts to have different levels of access rights. This is expressed as a 'true' or 'false' in several categories:

  • Read
  • Delete
  • Create

Default Acess

The default accounts that are created should have differing levels of access:

  • Guest should have read access only.
  • System should have read and write access.
  • Root should have Read, Write, Delete and Create access.

Bits and Bobs

  • Upon logging into an account, the applet should present a summary of what access rights the account has.
  • The account should hold details of when the user last logged in, and this information should be presented to them upon a successful login.
  • Upon three unsuccessful login attempts in a row, the applet should forbid any more attempts being made.

Password.java

The code we are given for password.java is as follows:

public class Password {
 
    private String password;
    private String name;
 
    public Password(String n, String p) {
        password = p;
        name = n;
    }
 
    public boolean checkPassword(String n, String p) {
        if (!name.equalsIgnoreCase(n)) {
            return false;
        }
        if (!password.equalsIgnoreCase(p)) {
            return false;
        }
        return true;
    }
 
    public String queryPassword() {
        return password;
    }
 
    public String queryName() {
        return name;
    }
}

The Applet

We are given the following code for the applet itself:

import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import javax.swing.*;
import java.util.*;
 
public class PasswordApplet extends JApplet implements ActionListener {
 
    ArrayList<Password> myPasswords;
    JLabel Login, Password;
    JTextField loginText, passText;
    JButton loginButton;
 
    public void init() {
        setLayout(null);
        myPasswords = new ArrayList<Password>();
        Login = new JLabel("Login:");
        Password = new JLabel("Password:");
        loginText = new JTextField(100);
        passText = new JTextField(100);
        loginButton = new JButton("Login");
        Login.setBounds(10, 10, 80, 20);
        Password.setBounds(10, 50, 80, 20);
        loginText.setBounds(100, 10, 200, 20);
        passText.setBounds(100, 50, 200, 20);
        loginButton.setBounds(120, 100, 80, 20);
        add(Login);
        add(Password);
        add(loginText);
        add(passText);
        add(loginButton);
        loginButton.addActionListener(this);
        populateAccounts();
    }
 
    public void populateAccounts() {
        addAccount("Guest", "Guest");
        addAccount("Root", "Root");
        addAccount("System", "System");
    }
 
    public boolean addAccount(String name, String password) {
        Password temp;
        for (temp : myPasswords) {
            if (temp.queryName().equalsIgnoreCase(name)) {
                return false;
            }
        }
        temp = new Password(name, password);
        myPasswords.add(temp);
        return true;
    }
 
    public boolean checkPassword() {
        String name, pass;
        ;
        Password temp;
        name = loginText.getText();
        pass = passText.getText();
        for (temp : myPasswords) {
            if (temp.checkPassword(name, pass) == true) {
                return true;
            }
        }
        return false;
    }
 
    public void actionPerformed(ActionEvent e) {
        if (checkPassword() == true) {
            JOptionPane.showMessageDialog(null,
                    "Access Granted!");
        } else {
            JOptionPane.showMessageDialog(null,
                    "Access Denied!");
        }
    }
}

Using the UML notation we have discussed in the two previous chapters, we can provide an 'at a glance' diagram to show the relationship:

12.3: First Glance UML Diagram

The Provided System

The code we have been provided is not complex. The password class stores the username and password, and provides two get methods for these attributes. It provides no method of setting the name or password other than through the constructor - this is for reasons of security.

We have a method called checkPassword that takes in a user name and a password, and checks to see if they match the details in an instance of the class. If they do, it returns true. If they don't, it returns false.

Our applet on the other hand maintains an ArrayList of password objects, and deals with checking through these to see if a particular user name and password combination should be provided with access:

    public boolean checkPassword() {
        String name, pass;
        Password temp;
        name = loginText.getText();
        pass = passText.getText();
        for (temp : myPasswords) {
            if (temp.checkPassword(name, pass) == true) {
                return true;
            }
        }
        return false;
    }

This method loops over every password object in the ArrayList called myPasswords and calls the checkPassword method on each with the name and password provided in the text components. If any of these objects return true for the method call, then the checkPassword method as a whole returns true and then the applet displays an 'access granted' message.

If no object matches the combination of user name and password, then it displays 'access denied'.

All of the valid accounts are added through a method called addAccount:

    public boolean addAccount(String name, String password) {
        Password temp;
        for (temp : myPasswords) {
            if (temp.queryName().equalsIgnoreCase(name)) {
                return false;
            }
        }
        temp = new Password(name, password);
        myPasswords.add(temp);
        return true;
    }

This is a simple method that just checks to see if a given username exists already, and if it doesn't it creates a new instance of the password class and adds it to the ArrayList.

The three default accounts are setup in the populateAccounts method:

    public void populateAccounts() {
        addAccount("Guest", "Guest");
        addAccount("Root", "Root");
        addAccount("System", "System");
    }

So, that's what we have just now - we need to expand on this to meet the requested additional functionality. We are not allowed to alter the Password class, which means all our changes must be made in either the applet or in an extension to Password.

How to Begin

The first thing we must do is decide what elements of functionality should go within which classes. We have to decide between both an applet and a new password class that we will extend from the existing code. Deciding on within which object a new piece of functionality should be placed is a difficult task that becomes easier only with experience.

There are elements of object orientation that are considerably beyond the scope of this textbook - the intention here is not to teach you how to design proper class hierarchies. The details of association and such are the province of other books. In this book we are concerned primarily with actually writing object oriented programs - we are not particularly concerned with designing them. We will of a necessity touch on the basics of OO design, but they are not our primary focus.

However, we must make some design decisions in a scenario like this, and the principle that should guide our decisions is that of reuse - where can we put things so that we get the most reusability out of them?

The principle of reuse is tightly coupled to the idea of generalisation - the more general an object, the more reusable it is. The downside of this is that the more general an object it is, the less suitable it is for specific purposes. This is why we use object orientation in the first place.

We must also take into account the model view controller architecture - in this book we have mostly grouped the role of view and controller into a single piece of code. In the past two chapters we have looked at separating out the model from the rest.

This particular scenario is an example of this principle. The model for the applet is the Password class - all of the behaviour and attributes that are specific to modelling a user account are stored in this object.

The applet is concerned primarily with providing the interface for the user, and for providing the relevant code for integrating the password class into the interface so that we have a useable piece of code that provides all the required functionality.

Ideally, the model should be so separate from the rest of the application that an entirely new front-end could be implemented, and no-one would have to write any extra code to deal with it. We could write a front-end with console based IO and we shouldn't need to rewrite the password functionality.

There seems to be a strange mix-mash of implementation in this particular case - although the details of individual passwords are stored in a separate class, the functionality for dealing with logons is stored in the applet.

The reason for this is that different applications will make use of password privileges in different ways, and that the Password class itself is only a container for password and username combinations. Before we begin implementing new functionality, we should look at making sure our applet is as object oriented as possible.

We are going to go through the steps of this as an exercise in this section rather than provide the code in a pre-designed, proper OO style. It is hoped that going through the decoupling process explicitly will be enlightening as to why it must be done at all.

Fixing the Application

At the moment, should someone wish to use our Password class to power their own login system, they will have to write all the login handling code themselves. It is not located in the Password class, and our applet is a front-end that also includes some key functionality... the fact that it is an applet means that it cannot easily be used in other applications because of the way the code is structured. This is not a particularly reusable application... it demonstrates low cohesion.

The only way someone could make use of our code would be to take the Password class and copy and paste the relevant pieces of code from the applet into their own projects... either that, or write the login handling code themselves from scratch.

It would be much better if they could simply include a Password class, and another class that implements the login logic - the fact that we don't have such a class is what renders this as a bad OO design.

Our login checking requires no event handling code in the abstract - all our applet should really be doing is providing a front-end. The applet should deals with the event handling, and then pass the required information to an object that deals with all the actual login logi

In order to implement this, we need a general class that will deal with the login logic. It needs to implement the following aspects of functionality:

  • The accounts database
  • Adding a new account
  • The checkPassword method

In essence, we are moving all of these pieces of functionality out of the applet and into a new class. Let's call this class LoginLogic.

We can't simply copy and paste the code directly - in checkPassword for example, we have the following two lines of code (in othe words, the program makes use of common coupling):

name = loginText.getText();
pass = passText.getText();

Our new class won't have access to these JTextFields, so we must instead pass the information to the method as parameters.

import java.util.*;
 
public class LoginLogic {
 
    private ArrayList<Password> myPasswords;
 
    public LoginLogic() {
        myPasswords = new ArrayList<Password>();
    }
 
    public boolean checkPassword(String name, String pass) {
        Password temp;
        for (temp : myPasswords) {
            if (temp.checkPassword(name, pass) == true) {
                return true;
            }
        }
        return false;
    }
}

Next, we need the addAccount method and the populateDatabase methods.

We can just steal these from our applet. In the end, we get a class that looks like this:

import java.util.*;
 
public class LoginLogic {
 
    private ArrayList<Password> myPasswords;
 
    public LoginLogic() {
        myPasswords = new ArrayList<Password>();
        populateAccounts();
    }
 
    public boolean checkPassword(String name, String pass) {
        Password temp;
        for (temp : myPasswords) {
            if (temp.checkPassword(name, pass) == true) {
                return true;
            }
        }
        return false;
    }
 
    public void populateAccounts() {
        addAccount("Guest", "Guest");
        addAccount("Root", "Root");
        addAccount("System", "System");
    }
 
    public boolean addAccount(String name, String password) {
        Password temp;
        for (temp : myPasswords) {
            if (temp.queryName().equalsIgnoreCase(name)) {
                return false;
            }
        }
        temp = new Password(name, password);
        myPasswords.add(temp);
        return true;
    }
}

Then, we modify our applet so that it now makes use of our LoginLogic class to deal with password checking:

import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import javax.swing.*;
import java.util.*;
 
public class Block3 extends JApplet implements ActionListener {
 
    JLabel Login, Password;
    JTextField loginText, passText;
    JButton loginButton;
    LoginLogic myLogic;
 
    public void init() {
        setLayout(null);
        Login = new JLabel("Login:");
        Password = new JLabel("Password:");
        loginText = new JTextField(100);
        passText = new JTextField(100);
        loginButton = new JButton("Login");
        Login.setBounds(10, 10, 80, 20);
        Password.setBounds(10, 50, 80, 20);
        loginText.setBounds(100, 10, 200, 20);
        passText.setBounds(100, 50, 200, 20);
        loginButton.setBounds(120, 100, 80, 20);
        add(Login);
        add(Password);
        add(loginText);
        add(passText);
        add(loginButton);
        loginButton.addActionListener(this);
        myLogic = new LoginLogic();
    }
 
    public void actionPerformed(ActionEvent e) {
        if (myLogicheckPassword(loginText.getText(), passText.getText())
                == true) {
            JOptionPane.showMessageDialog(null, "Access Granted!");
        } else {
            JOptionPane.showMessageDialog(null, "Access Denied!");
        }
    }
}

Back to our UML diagram... we need to alter it to reflect the changes that we've just made:

12.4: UML Diagram

Now that we have three classes in our model, our UML diagram starts to get a little more complicated... but we can still tell at a glance how the classes interact. In a few moments, we'll see how we incorporate inheritance to make the class structure even more complex.

And there we have it, our login functionality has been completely separated from the interface, and now if anyone wants to make use of our code they can simply use the Password class and the LoginLogic class to emulate our login procedure. If they need something different, they can make use of extension and specialisation to meet their needs.

Say, that sounds like us!

Indeed it does - we need to make use of these classes to implement something different! Luckily, we're already familiar with the ideas of extension and specialisation, so we're well equipped to make use of this setup to meet our requirements.

We're going to have to extend both Password and LoginLogic to meet our needs. Let's start with a new ExtendedPassword class.

We need to add functionality for the following:

  • Access rights
  • Last login time

There are a number of ways we could implement the access rights, but we'll go for the simple way first - four boolean variables, with accompanying accessor methods:

public class ExtendedPassword extends Password {
 
    private boolean read, write, delete, create;
 
    public ExtendedPassword(String name, String pass) {
        super(name, pass);
    }
 
    public boolean getRead() {
        return read;
    }
 
    public boolean getWrite() {
        return write;
    }
 
    public boolean getDelete() {
        return delete;
    }
 
    public boolean getCreate() {
        return create;
    }
 
    public void setRead(boolean value) {
        read = value;
    }
 
    public void setWrite(boolean value) {
        write = value;
    }
 
    public void setDelete(boolean value) {
        delete = value;
    }
 
    public void setCreate(boolean value) {
        create = value;
    }
}

We should also provide another constructor method so that we can set these values when we create an instance of the class:

    public ExtendedPassword(String name, String pass, boolean r, boolean w,
            boolean d, boolean c) {
        super(name, pass);
        setRead(r);
        setWrite(w);
        setDelete(d);
        setCreate(c);
    }

And that's us finished implementing the functionality to deal with the access rights. Neat!

We can follow a similar procedure to deal with login time. We need an attribute to store it, and it needs to be set on a successful login. We have a method that returns true or false dependant on a successful login, so we can specialise that to update the last login time when the correct name and password are used.

There is a class in the Java API called Date that will do exactly what we need, so let's include an attribute of type Date and appropriate accessor methods. The Date class can be found in the java.util package, so we will need to import that if it hasn't been imported already:

    Date lastLogin;
 
    public Date getDate() {
        return lastLogin;
    }
 
    public void setDate(Date newLogin) {
        lastLogin = newLogin;
    }

Now, we specialise our existing checkPassword method. We don't want to do anything to alter the underlying code - we just want to do something extra if it's a successful login attempt:

    public boolean checkPassword(String n, String p) {
        boolean returnValue;
        returnValue = super.checkPassword(n, p);
        if (returnValue == true) {
            setDate(new Date());
        }
 
        return returnValue;
    }

And with that, we've implemented the two pieces of functionality required for the ExtendedPassword class.

We now need to make use of this in the LoginLogic class. There is nothing to stop us changing the base object if we need to - after all, we are only prohibited from altering the Password class. However, in the interests of good OO design we want to make our code as reusable as possible and so we will create an extended LoginLogic class to deal with our new functionality.

There are two things we need to change - one is that we need to populate our password database with instances of ExtendedPassword that are configured with access information, so we'll need to specialise the populateDatabase method.

The other thing we need to do is change our addAccount method so that we're adding ExtendedPassword objects rather than ordinary Password objects. This however introduces a new complication.

In our LoginLogic object, we've defined an ArrayList as being full of Passwords. Now, polymorphism (more on this later) means that if something is an ExtendedPassword, then it's still a Password, just a more complicated one. Alas, the generic syntax does not allow for this argument to work.

We must cast the Password objects into our ArrayList into their proper form - that of an ExtendedPassword. We've already seen casting in previous chapters - there's nothing new here that we need to worry about:

        public boolean addAccount(String name, String password, boolean read, boolean write, boolean create, boolean delete) {
            ExtendedPassword newPass;
            for (Password temp : myPasswords) {
                newPass = (ExtendedPassword) temp;
 
                if (newPass.queryName().equalsIgnoreCase(name)) {
                    return false;
                }
            }
            temp = new ExtendedPassword(name, password, read, write, create, delete);
            myPasswords.add(temp);
            return true;
        }

That's all we need to change - we don't need to alter the checkPassword to deal with ExtendedAccounts. We'll talk about why such a change is unnecessary later in the book when we discuss polymorphism in more depth.

Finally, we need to change our applet so that it makes use of our new ExtendedLoginLogic as opposed to the normal LoginLogic. The syntax for this should be familiar by now.

Finally, we compile the application, and... disaster! It doesn't compile!

This is because when we wrote the LoginLogic object, we had the following line:

private ArrayList<Password> myPasswords;

This means that only LoginLogic has access to the variable, and yet we are referring to it in ExtendedLoginLogi Because we need to have access to this, we'll change it so that instead it is declared as protected:

protected ArrayList<Password> myPasswords;

Compile again, and victory is ours! Warm glasses of cheap champagne all around!

The Finishing Touch

Internally, this is all working correctly - but it behaves exactly the same way as it did before. We still need to display the appropriate login information.

Again, there are a number of ways to do this. To make it as simple as possible, we'll just have our ExtendedLoginLogic object flash up a JOptionPane with the summary information.

The only place where we have access to all the information we need is in our ExtendedAccount object - at the same place we set the last login. Here, we'll flash up a summary message box with all our details:

       public boolean checkPassword(String n, String p) {
            boolean returnValue;
            String access = "";
            returnValue = super.checkPassword(n, p);
            if (returnValue == true) {
                setDate(new Date());
                if (getRead()) {
                    access = access + "Read access.";
                }
                if (getWrite()) {
                    access = access + "Write access.";
                }
                if (getCreate()) {
                    access = access + "Create access.";
                }
                if (getDelete()) {
                    access = access + "Delete access.";
                }
                JOptionPane.showMessageDialog(null, "You last logged on " + getDate()
                        + ".  You have the following access rights:  " + access);
            }
            return returnValue;
        }

This of course raises an ugly problem - we've now destroyed the MVC architecture we spent so much time ensuring. Our checkPassword method now makes the assumption that it will be deployed in a system where Swing is available, and that may not be true. This solution works, but it's a terrible hack.

What we really need to do is find some method to ensure that the MVC architecture is still enforced - we need to change the structure of the program a little to do this however, and so it has been left to the end. First of all, let's break all the details out into a separate method:

        public String getDetails() {
            String access = "";
            if (getRead()) {
                access = access + "Read access.  ";
            }
            if (getWrite()) {
                access = access + "Write access.  ";
            }
            if (getCreate()) {
                access = access + "Create access.  ";
            }
            if (getDelete()) {
                access = access + "Delete access.  ";
            }
            return "You last logged on " + getDate() + 
              ".  You have the following access rights" + access;
        }

The way our system is currently setup means there is no place for our View (the applet) to know which account has just logged in - all we get is a true or false value. This can easily be changed so that instead of the boolean we get either a null value (indicating no account is active), or the account that successfully logged in. We change LoginLogic to do this:

        public Password checkPassword(String name, String pass) {
            for (Password temp : myPasswords) {
                if (temp.checkPassword(name, pass) == true) {
                    return temp;
                }
            }
            return null;
        }

Now we need to change our applet a little bit to deal wih the new structure:

    public void actionPerformed(ActionEvent e) {
        ExtendedPassword temp;
        temp = (ExtendedPassword) myLogic.checkPassword(loginText.getText(), passText.getText());
        if (temp == null) {
            JOptionPane.showMessageDialog(null, "Access Denied!");
        } else {
            JOptionPane.showMessageDialog(null, "Access Granted!");
            JOptionPane.showMessageDialog(null, temp.getDetails());
        }
    }

Now we have a system that does not require an ugly hack that destroys our MVC, but we did have to alter the internal structure of the system somewhat. This is a common theme in software development - even the best laid plans of mice and men aft gang agley, so it is sometimes necessary to do some restructuring to meet changing requirements. Object orientation doesn't remove this risk, but it mitigates it somewhat.

Template:Applet


The Final UML Diagram

Now that we've finished the applet, we should look at updating our UML diagram so that it correctly reflects all the alterations. We now have five classes in this project, and the relationships between them are becoming fairly difficult to track. It is important that our documentation reflects this:

12.5: Final UML Diagram

Conclusion

And there we go - all extra functionality accounted for, without requiring a single line of code in Password to be changed. True, we had to change almost everything else, but that's the way these things go. All that's important is that we have been able to make use of both the MVC and the powers of object orientation to develop a solution to a particular coding problem.

Since this is a very simplified example that does not need to deal with things like actual security, we can afford to be somewhat lax in our implementation of particular methods. The fundamental principles still apply however, and they are relevant to all object oriented programs developed in Java.