As the characters/digits can be anywhere within the string, we require lookaheads. Lookaheads are of zero width
meaning they do not consume any string. In simple words the position of checking resets to the original position after each condition of lookahead is met.
Assumption :- Considering non-word characters as special
^(?=.{10,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$
Before proceeding to explanation, let's take a look how the regex ^(?=.*[a-z])
works (length is not considered here) on string 1$d%aA
Image Credit :- https://regex101.com/
Things to notice
^
.Regex Breakdown
^ #Starting of string
(?=.{10,}$) #Check there is at least 10 characters in the string.
#As this is lookahead the position of checking will reset to starting again
(?=.*[a-z]) #Check if there is at least one lowercase in string.
#As this is lookahead the position of checking will reset to starting again
(?=.*[A-Z]) #Check if there is at least one uppercase in string.
#As this is lookahead the position of checking will reset to starting again
(?=.*[0-9]) #Check if there is at least one digit in string.
#As this is lookahead the position of checking will reset to starting again
(?=.*\W) #Check if there is at least one special character in string.
#As this is lookahead the position of checking will reset to starting again
.*$ #Capture the entire string if all the condition of lookahead is met. This is not required if only validation is needed
We can also use the non-greedy version of the above regex
^(?=.{10,}$)(?=.*?[a-z])(?=.*?[A-Z])(?=.*?[0-9])(?=.*?\W).*$