The best approach I can propose you is to capture every type of characters in a capture group and make sure that at least 3/4 capture groups have a value (if the group can't match anything, it should be an empty string).
^(?:([a-z])|([A-Z])|(\d)|(_|[^\w]))+$
You can also add a positive lookahead to make sure that the password have the required length (by example 8 to 32 characters).
^(?=.{8,32}$)(?:([a-z])|([A-Z])|(\d)|(_|[^\w]))+$
Edit: ([\W_])
is the equivalent of (_|[^\w])
. Putting the "W" in upper case reverse it sense (match all the non-word characters). Moreover, using a single character class is faster than alternation (more details here)
If you are willing to use javascript, I adapted a function presented in "Regular Expression cookbook second edition" for the needs of my website:
var PASSWORD_RANKING = {
TOO_SHORT: 0,
WEAK: 1,
MEDIUM: 2,
STRONG: 3,
VERY_STRONG: 4
};
/**
* Take a password and returns it's ranking
* based of the strength of the password (length, numeric character, alphabetic character, special character, etc.)
*
* @param password String
* @param minLength Int
*
* @return Int
*/
function rankPassword(password, minLength){
var rank = PASSWORD_RANKING.TOO_SHORT;
var score = 0;
if (typeof minLength !== 'number' || minLength < 6){
minLength = 8;
}
if (typeof password === 'string' && password.length >= minLength){
if (/[A-Z]/.test(password)){ score++;}
if (/[a-z]/.test(password)){ score++;}
if (/[0-9]/.test(password)){ score++;}
if (/[_~!@.#$%^&]/.test(password)){ score++;}
score += Math.floor((password.length - minLength) / 2);
if (score < 3){
rank = PASSWORD_RANKING.WEAK;
}
else if (score < 4){
rank = PASSWORD_RANKING.MEDIUM;
}
else if (score < 6){
rank = PASSWORD_RANKING.STRONG;
}
else {
rank = PASSWORD_RANKING.VERY_STRONG;
}
}
return rank;
}
The section 4.19 present many regexes to enforce password strength. You can see all the code samples online :
http://examples.oreilly.com/0636920023630/Regex_Cookbook_2_Code_Samples.html