尝试启动并运行身份验证服务,但是存在一个相当严重的问题,如果我将salt存储在数据库中,然后检索它并尝试使用用户提交的密码生成哈希,它会生成即使密码相同,也会出现错误的哈希值。
例如,如果我使用我的代码发布
的请求{
"Email":"stackOverflow@gmail.com",
"Password":"foo"
}
经过哈希处理,产生的值为:
如果我然后检索盐并尝试进行身份验证,我会收到哈希密码:
盐是相同的,[B @ 681d1bc7,所以除非有不一致的数据结构,否则我认为没问题。
数据库是Oracle SQL,ID为数字,电子邮件,密码和Password_Salt为Varchars。
以下代码:
AuthenticationController:
/*
* TODO: Change me
*/
package entities;
import importsJSON.JSONObject;
import importsJSON.JSONParser;
import java.io.InputStream;
import java.sql.SQLException;
public class AuthenticationController {
//<editor-fold desc="Logic related to instantiation of an AC object">
/**
* Logic related to instantiation of an AC object.
* To instantiate an AC object, due to the method of the Singleton Pattern,
* AuthenticationController ac = new AuthenticationController();
* will not work, as the constructor is private.
* Instead calls are made as such:
* AuthenticationController ac = AuthenticationController.getInstance();
*
* This means that there will only ever be a single AuthenticationController, which
* bears the benefit that concurrency and multiple instances are eliminated as possibilities,
* something arguably desirable for such functionality as login management.
* It does need to be noted however, that this can cause a potential bottleneck and harms scalability
* of a single server. While it is good we don't have to worry about multiple logins or
* "losing" a logged in user, it's also bad in the sense that we can't have multiple AC instances.
* If 3 users require authentication simultaneously, they cannot have have their requests
* delegated to different AC instances to process them faster.
* Such a system would ideally be better for scalability, but would require a great deal more work in order to ensure
* everything is safe and issues like concurrency are avoided.
*/
private static AuthenticationController instance = new AuthenticationController();
private AuthenticationController(){}
public static AuthenticationController getInstance(){
return instance;
}
//</editor-fold>
public String getMessage(){
return "HelloWorld";
}
public String authenticateUser(String rawData) throws SQLException, Exception{
//Variables for building a User to evaluate
String email = "";
PasswordManager passwordManager = new PasswordManager();
JSONObject user = null;
byte[] salt = null;
int userID;
String results ="";
//Variables for request evaluation
Boolean badInput = false;
String hashedUserPassword = null;
String storedPassword = null;
UserModel userModel = new UserModel();
//Convert the input to a JSON Object
JSONParser parser = new JSONParser();
user = (JSONObject)parser.parse(rawData);
//Get User Email and Password
try{
email = user.get("Email").toString();
passwordManager.setPtPassword(user.get("Password").toString());
}
catch(Exception ex){
badInput = true;
}
//If the user has not provided bad data, eg null values
if(!badInput){
//Create SQL query to search database for a row with matching email
//Note that we only need the ID, Password and Salt, anything else is unnecessary
String parameterlessQuery = "Select ID, PASSWORD, PASSWORD_SALT From Users Where UPPER(EMAIL) = UPPER(?)";
String[] params = {email};
SQLQuery query = new SQLQuery(parameterlessQuery, params);
if((email!=null)&&(email.length()>0)){
query = authenticateUser(query);
}
else{
query.setErrorCode("22000");
}
//Retrieve variables from results list
if(!query.getResults().isEmpty()){
Object[] userToVerify = query.getResults().get(0);
//userModel.setID(userToVerify[0].toString());
userModel.sethPassword(userToVerify[1].toString());
userModel.setSalt(userToVerify[2].toString());
//Use the salt and provided password to create hashed password
String userPassword = passwordManager.getPtPassword();
hashedUserPassword = passwordManager.generateHash(userPassword, userModel.getSalt().getBytes());
System.out.println("New hashed: "+ hashedUserPassword+" Hashed: "+ userModel.gethPassword()+ " Salt:"+userModel.getSalt());
}
}
//Finally we need to convert them
//Compare hashed password from Database to newly hashed password
return (hashedUserPassword+" "+ userModel.gethPassword());
//If match, user is logged in
//If no match, user receieves a negative reply informing them of failure.
}
//Method for submitting a query to a database
/**
* May seem redundant, however this is necessary in order to get back the query
* which was passed in so properties of the query can be examined.
* Initialises a database controller and passes the query to be executed.
* @param query
* @return
*/
public SQLQuery authenticateUser(SQLQuery query){
DatabaseController databaseController = new DatabaseController();
return databaseController.submitQuery(query, 3);
}
的AuthenticationService:
package services;
import entities.AuthenticationController;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.ejb.Stateless;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/Authenticate")
@Stateless
public class AuthenticationService {
@POST
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public String loginUser(InputStream data) throws Exception {
AuthenticationController authController = AuthenticationController.getInstance();
StringBuilder builder = new StringBuilder();
try{
BufferedReader in = new BufferedReader(new InputStreamReader(data));
String line = null;
while((line = in.readLine())!= null){
builder.append(line);
}
} catch (IOException ex) {
}
return authController.authenticateUser(builder.toString());
}
}
DatabaseController:
package entities;
import java.sql.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author Andrew
* Class controlling access to databases using SQL queries.
*/
public class DatabaseController extends AbstractController{
//<editor-fold desc="Constructors and class variables">
String targetDB;//The database it shall use to connect to
String username;//The username needed to connect to the target database
String password;//The password needed to connect to the target database
//By default, this method shall be used.
public DatabaseController(){
this.targetDB = [redacted];
this.username = [redacted];
this.password = [redacted];
}
//Should we have more than one database to manipulate.
public DatabaseController(String targetDB, String username, String password){
this.targetDB = targetDB;
this.username = username;
this.password = password;
}
//</editor-fold>
//Formats and submits the query to the given database.
public SQLQuery submitQuery(SQLQuery query, int columns){
try {
//Using the Oracle DB connection Driver
Class.forName("oracle.jdbc.driver.OracleDriver");
//Open a connection to the target Database
Connection connection = DriverManager.getConnection(targetDB, username, password);
//Build then Execute Query
//This may look a bit weird. Unfortunately it's the result of the nuances of the
//classes used, (PreparedStatement instead of Statement), however the benefit of
//doing so is protection from SQL injections.
PreparedStatement statement = connection.prepareStatement(query.getParameterlessQuery());
if(query.getQueryParams().length!=0){
String[] params = query.getQueryParams();
for(int i=0; i< params.length; i++){
statement.setString((i+1), params[i]);
}
}
//query.setDebug(statement.toString());
query.setResponse(statement.executeQuery());
//If there is a result set expected, then save the results before they are lost on connection close
while(query.getResponse().next()){
Object[] temp = new Object[columns];
for (int i = 0; i < columns; i++){
temp[i] = query.getResponse().getObject(i+1);
}
query.getResults().add(temp);
}
//Close Connection
connection.close();
} catch (ClassNotFoundException ex) {
Logger.getLogger(DatabaseController.class.getName()).log(Level.SEVERE, null, ex);
query.setDebug("driver missing");
} catch (SQLException ex) {
for (Throwable e : ex) {
if (e instanceof SQLException) {
query.queryFail(((SQLException)e).getSQLState(), ((SQLException)e).getMessage());
System.out.println(query.getErrorCode() + "\n"+ query.getErrorMessage());
}
}
}
return query;
}
}
RegistrationService:
package services;
import entities.DatabaseController;
import entities.PasswordManager;
import entities.SOAPEmailValidator;
import entities.SQLQuery;
import importsJSON.JSONObject;
import importsJSON.JSONParser;
import importsJSON.ParseException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Stateless;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
*
* @author Andrew
* A Rather simple class that controls the registration services associated with
* our Web Service.
*/
@Path("/Register")
@Stateless
public class RegistrationService {
//Register user service
/**
* The default method for the /Register context of web services, meaning it is accessed
* by a URI of form host/API/Register.
* Unlike most of the other methods, this is a post method and requires a sample of JSON
* data providing an email and password from the user in a form similar to:
* {
* "Email":"[emailValue]",
* "Password":"[passwordValue]"
* }
*
* Everything else is then auto generated as necessary, and used to insert the user in to
* the database.
*
* @param data
* @return
*/
@POST
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response registerUser(InputStream data){
StringBuilder builder = new StringBuilder();
//Variables for the building of the user to be inserted
String email = "";
String ptPassword = "";
JSONObject user = null;
PasswordManager password = new PasswordManager();
byte[] salt = null;
try{
//Read in the submitted JSON
BufferedReader in = new BufferedReader(new InputStreamReader(data));
String line = null;
while ((line = in.readLine()) != null) {
builder.append(line);
}
//Convert the JSON String submitted by client to a JSON Object
JSONParser p = new JSONParser();
user = (JSONObject)p.parse(builder.toString());
//Extract relevant values from the JSON Object
email = user.get("Email").toString();
ptPassword = user.get("Password").toString();
//Generate Salt
salt = password.generateSalt();
//Use Salt to hash the password
password.setHashedPassword(password.generateHash(ptPassword, salt));
} catch (IOException ex) {
Logger.getLogger(RegistrationService.class.getName()).log(Level.SEVERE, null, ex);
} catch (ParseException ex) {
Logger.getLogger(RegistrationService.class.getName()).log(Level.SEVERE, null, ex);
} catch (NoSuchAlgorithmException ex) {
Logger.getLogger(RegistrationService.class.getName()).log(Level.SEVERE, null, ex);
} catch (Exception ex) {
Logger.getLogger(RegistrationService.class.getName()).log(Level.SEVERE, null, ex);
}
//Attempt to create a query
String parameterlessQuery = "Insert into users (ID, EMAIL, PASSWORD, PASSWORD_SALT) VALUES (SEQ_PERSON_ID.nextval,?,?,?)";
String[] params = {email, password.getHashedPassword(), password.saltToString(salt)};
System.out.println("New Hashed: "+"Hashed: "+ password.getHashedPassword() + " Salt: "+ password.saltToString(salt));
SQLQuery query = new SQLQuery(parameterlessQuery, params);
//Check to ensure that the data is not null
if((email!=null&&ptPassword!=null)&&(email.length()>0&&(ptPassword.length()>0))){
query = registerUser(query);
}else
{
query.setErrorCode("22000");
}
//If the error code suggests that the user has been created successfully, return a 200 OK message
//to client
if (query.getErrorCode().matches("00000")||query.getErrorCode().matches("24000")){
return Response.status(200).entity("User Created successfully.").build();
}
else{//If not, return an error message with the SQLState code thrown
return Response.status(400).entity("Failed, Error Code: "+query.getErrorCode()).build();
}
}
//Method for submitting a query to a database
/**
* May seem redundant, however this is necessary in order to get back the query
* which was passed in so properties of the query can be examined.
* Initialises a database controller and passes the query to be executed.
* @param query
* @return
*/
public SQLQuery registerUser(SQLQuery query){
DatabaseController databaseController = new DatabaseController();
return databaseController.submitQuery(query, 0);
}
}
最后, PasswordManager:
package entities;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.xml.bind.DatatypeConverter;
/**
*
* @author Andrew
* A class controlling the logic relating to the hashing of passwords.
*
*/
public class PasswordManager {
String ptPassword;//Plain text
String hashedPassword;//Hashed
byte[] salt;
public PasswordManager(){
this.ptPassword = null;
this.hashedPassword = null;
this.salt = null;
}
//Generates a salt for hashing a password
/**
* Uses SecureRandom and the SHA-1 Pseudo Random Number Generator to create a
* salt that is extremely difficult to predict.
* Possibly a bit overkill for the security on a simple travel application, however
* this method is good for scalability were we looking to up our game.
*
* @return
* @throws java.security.NoSuchAlgorithmException
*/
public byte[] generateSalt() throws NoSuchAlgorithmException{
SecureRandom secureRandomGenerator = SecureRandom.getInstance("SHA1PRNG");
byte[] randomBytes = new byte[128];
secureRandomGenerator.nextBytes(randomBytes);
return randomBytes;
}
//Uses the plaintext password and the salt to generate a hashed password to store in the database
/**
*
* @param password
* @param salt
* @return
* @throws Exception
*/
public String generateHash(String password, byte[] salt) throws Exception{
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 2000, 128));
String encoded = DatatypeConverter.printBase64Binary(key.getEncoded());
byte[] decoded = DatatypeConverter.parseBase64Binary(encoded);
String output = new String(decoded, "UTF-8");
return output;
}
//Helper method for conversion of salt to a database storable object
/**
*
* @param salt
* @return
*/
public String saltToString(byte[] salt){
return salt.toString();
}
//Helper method for conversion of salt to a hashable object
/**
*
* @param salt
* @return
*/
public byte[] saltToBytes(String salt){
return salt.getBytes();
}
public String getPtPassword() {
return ptPassword;
}
public void setPtPassword(String ptPassword) {
this.ptPassword = ptPassword;
}
public String getHashedPassword() {
return hashedPassword;
}
public void setHashedPassword(String hashedPassword) {
this.hashedPassword = hashedPassword;
}
public byte[] getSalt() {
return salt;
}
public void setSalt(byte[] salt) {
this.salt = salt;
}
}