简体   繁体   中英

Spring Security AuthenticationManager set a PasswordEncoder

I am using SpringBoot 2.3.1.RELEASE with Java 14.

I have Spring Security working correctly, ie it can receive a username & password and return a jwt token. Various api calls are validated successfully against the token.

Here is my WebSecurityConfigurerAdapter

SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("datasource1")
    private DataSource dataSource;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource)
        .usersByUsernameQuery("SELECT username, password, (NOT disabled) as enabled FROM members "+
                "WHERE username = ?")
        .authoritiesByUsernameQuery("SELECT m.username, t.name as authority FROM members m " +
                "JOIN administrator a ON a.member_id = m.member_id " +
                "JOIN admin_type t ON t.admin_type_id = a.admin_type_id "+
                "WHERE m.username = ?");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // set up the jwt auth
        http.cors().disable();
        http.csrf().disable().authorizeRequests().antMatchers("/authenticate").permitAll()//.anyRequest().authenticated()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .and().exceptionHandling()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // don't manage sessions, using jwt
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        // define the role mappings
        http.authorizeRequests()
                .antMatchers("/admin").hasAuthority("approver admin")
                .antMatchers("/approvals").hasAuthority("approver admin")
                //.antMatchers("/rest/*").hasAuthority("approver admin")
                .antMatchers("/hello").permitAll();

        // INSERT INTO admin_type (admin_type_id, name, description) VALUES ((SELECT MAX(admin_type_id) +1 FROM admin_type), 'approver admin', 'Able to alter approval requests');
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new NexctPasswordEncoder();
    }
}

I also have a RESTful Resource. This receives an AuthenticationRequest containing a username and raw password.

ApprovalsResource.java

@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
    try {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
        );
    } catch (BadCredentialsException e) {
        logger.info("Incorrect username or password for "+authenticationRequest.getUsername());
        throw new Exception("Incorrect username or password", e);
    }
    final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
    final String jwt = jwtTokenUtil.generateToken(userDetails);
    final String username = jwtTokenUtil.extractUserName(jwt);
    logger.info("User just logged in: "+username);
    return ResponseEntity.ok(new AuthenticationResponse(jwt));
}

NexctPasswordEncoder.java

public class NexctPasswordEncoder implements PasswordEncoder {

    Logger logger = LoggerFactory.getLogger(NexctPasswordEncoder.class);

    @Override
    public String encode(CharSequence rawPassword) {
        return encodeString(rawPassword.toString());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        String encoded = encodeString(rawPassword.toString());
        boolean match = encoded.equals(encodedPassword);
        return match;
    }

    private String encodeString(String s) {
        String encryptedPassword = null;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-384");
            byte[] pt = s.getBytes();
            byte[] out = messageDigest.digest(pt);
            encryptedPassword = HexConvert.ByteToHexString(out);
        } catch (NoSuchAlgorithmException e) {
            logger.error("Error trying to encode password");
        }
        return encryptedPassword;
    }
}

This all works perfectly with the NexctPasswordEncoder when a raw (unencrypted) password is received. The NexctPasswordEncoder encrypts the password to be compared it to the encrypted password in the database.

Problem

I also need to cater for the case when I receive the encrypted password rather then the raw password.

Solution

I need to get the above to work with two different PasswordEncoder 's.

Ideally, in the ApprovalsResource where the request with the username and password is received, and the following is called:

authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())

I would like to set the relevant PasswordEncoder (one that encrypts the password and one that does not).

Question

How do I swap the PasswordEncoder depending on a parameter from the request?

Checkout the DelegatingPasswordEncoder: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/password/DelegatingPasswordEncoder.html

It allows to determine which other PasswordEncoder to use based on a prefix in the password value.

Actually solution is very simple.

You can't use DelegatePasswordEncoder. Because passwords need to be stored with prefix. And when they stored with prefix, spring security will use the password encoder for that prefix.

You can do one of the following:

  • Enhance your PasswordEncoder to do plain match as well as encoded match.
  • Create a wrapper for NexctPasswordEncoder which will do exact match and then inject it as password encoder. boolean match = encoded.equals(encodedPassword) || rawPassword.equals(encodedPassword);
public class NexctPasswordEncoder implements PasswordEncoder {

    Logger logger = LoggerFactory.getLogger(NexctPasswordEncoder.class);

    @Override
    public String encode(CharSequence rawPassword) {
        return encodeString(rawPassword.toString());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        String encoded = encodeString(rawPassword.toString());
        boolean match = encoded.equals(encodedPassword) || rawPassword.equals(encodedPassword);
        return match;
    }

    private String encodeString(String s) {
        String encryptedPassword = null;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-384");
            byte[] pt = s.getBytes();
            byte[] out = messageDigest.digest(pt);
            encryptedPassword = HexConvert.ByteToHexString(out);
        } catch (NoSuchAlgorithmException e) {
            logger.error("Error trying to encode password");
        }
        return encryptedPassword;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM