Implement Spring Security for Rest Api

Viewed 2362

I use this code for Rest API authentication:

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        Optional<String> basicToken = Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
                .filter(v -> v.startsWith("Basic"))
                .map(v -> v.split("\\s+")).filter(a -> a.length == 2).map(a -> a[1]);
        if (!basicToken.isPresent()) {
            return sendAuthError(response);
        }

        byte[] bytes = Base64Utils.decodeFromString(basicToken.get());
        String namePassword = new String(bytes, StandardCharsets.UTF_8);
        int i = namePassword.indexOf(':');
        if (i < 0) {
            return sendAuthError(response);
        }
        String name = namePassword.substring(0, i);
        String password = namePassword.substring(i + 1);
//        Optional<String> clientId = authenticationService.authenticate(name, password, request.getRemoteAddr());
        Merchants merchant = authenticationService.authenticateMerchant(name, password, request.getRemoteAddr());
        if (merchant == null) {
            return sendAuthError(response);
        }
        request.setAttribute(CURRENT_CLIENT_ID_ATTRIBUTE, merchant.getId());
        return true;
    }

How I can rewrite the code with Spring Security in order to get the same result but for different links to have authentication? For example:

localhost:8080/v1/notification - requests should NOT be authenticated.
localhost:8080/v1/request - requests should be authenticated.
2 Answers

Here you can find a working project https://github.com/angeloimm/springbasicauth

I know in the pom.xml file there are a lot of useless dependencies but I started from an already existing project and I had no time to depure it

Basically you must:

  • configure spring security
  • configure spring mvc
  • implements your own authentication provider according to spring security. Note I used an inMemoryAuthentication. Please modify it according to yuor own wishes

Let me explain the code.

Spring MVC Configuration:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages= {"it.olegna.test.basic"})
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}

Here we don't do anything else that configuring spring MVC by telling it where to find controllers and so on and to use a single message converter; the MappingJackson2HttpMessageConverter in order to produce JSON responses

Spring Security Configuration:

@Configuration
@EnableWebSecurity
@Import(value= {WebMvcConfig.class})
public class WebSecConfig extends WebSecurityConfigurerAdapter {
     @Autowired private RestAuthEntryPoint authenticationEntryPoint;

        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth
              .inMemoryAuthentication()
              .withUser("test")
              .password(passwordEncoder().encode("testpwd"))
              .authorities("ROLE_USER");
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
              .authorizeRequests()
              .antMatchers("/securityNone")
              .permitAll()
              .anyRequest()
              .authenticated()
              .and()
              .httpBasic()
              .authenticationEntryPoint(authenticationEntryPoint);
        }
        @Bean
        public PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
}

Here we configure Spring Security in order to use HTTP Basic Authentication for all requests except the ones starting with securityNone. We use a NoOpPasswordEncoder in order to encode the provided password; this PasswrodEncoder does absolutly nothing... it leaves the passwrod as it is.

RestEntryPoint:

@Component
public class RestAuthEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,  AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

This entrypoint disables all requests not containg the Authentication header

SimpleDto: a very simple DTO representing the JSON answer form a controller

public class SimpleDto implements Serializable {

    private static final long serialVersionUID = 1616554176392794288L;
    private String simpleDtoName;

    public SimpleDto() {
        super();
    }
    public SimpleDto(String simpleDtoName) {
        super();
        this.simpleDtoName = simpleDtoName;
    }
    public String getSimpleDtoName() {
        return simpleDtoName;
    }
    public void setSimpleDtoName(String simpleDtoName) {
        this.simpleDtoName = simpleDtoName;
    }

}

TestBasicController: a very simple controller

@RestController
@RequestMapping(value= {"/rest"})
public class TestBasicController {
    @RequestMapping(value= {"/simple"}, method= {RequestMethod.GET}, produces= {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public ResponseEntity<List<SimpleDto>> getSimpleAnswer()
    {
        List<SimpleDto> payload = new ArrayList<>();
        for(int i= 0; i < 5; i++)
        {
            payload.add(new SimpleDto(UUID.randomUUID().toString()));
        }
        return ResponseEntity.ok().body(payload);
    }
}

So if you try this project by using postman or any other tester you can have 2 scenarios:

  • authentication required
  • all ok

Let's suppose you want to invoke the URL http://localhost:8080/test_basic/rest/simple without passing the Authentication header. The HTTP Status code will be 401 Unauthorized

This means that the Authentication Header is required

By adding this header to the request Authorization Basic dGVzdDp0ZXN0cHdk all works pretty good Note that the String dGVzdDp0ZXN0cHdk is the Base64 encoding of the string username:password; in our case is the Base64 encoding of test:testpwd defined in the inMemoryAuthentication

I hope this is usefull

Angelo

WEB SECURITY USER DATAIL SERVICE

In order to configure Spring security to retrieve user details from DB you must do the following:

create a org.springframework.security.core.userdetails.UserDetailsService implementation like this:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private BasicService svc;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        BasicUser result = svc.findByUsername(username);
        if( result == null )
        {
            throw new UsernameNotFoundException("No user found with username "+username);
        }
        return result;
    }

}

Inject it to the spring security configuration and use it like this:

public class WebSecConfig extends WebSecurityConfigurerAdapter {
    @Autowired private RestAuthEntryPoint authenticationEntryPoint;
    @Autowired
    UserDetailsService userDetailsService;
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//      auth
//      .inMemoryAuthentication()
//      .withUser("test")
//      .password(passwordEncoder().encode("testpwd"))
//      .authorities("ROLE_USER");
        auth.userDetailsService(userDetailsService);
        auth.authenticationProvider(authenticationProvider());
    }
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .authorizeRequests()
        .antMatchers("/securityNone")
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .httpBasic()
        .authenticationEntryPoint(authenticationEntryPoint);
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

I pushed the code on the github link I provided. There you can find a full working example based on:

  • spring 5
  • spring security 5
  • hibernate
  • h2 DB

Feel free to adapt it to your own scenario

You can use a default spring-security configuration described on various websites, like baeldung.com or mkyong.com. The trick in your sample seems to be the call to get the Merchant. Depending on the complexity of the authenticationService and the Merchant object, you can either use the following code, or implement a facade to get similar behaviour.

@Autowired
public void authenticationManager(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(new AuthenticationProvider() {
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            Merchants merchant = authenticationService.authenticateMerchant(name, password, request.getRemoteAddr());
            if(merchant == null) {
                throw new AuthenticationException("No Merchant found.");
            }
            return new UsernamePasswordAuthenticationToken(name, password, merchant.getAuthorities());
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
        }
    });
}

Setting the attribute on the request, if necessary could be done by a separate filter which takes the Principal from the SecurityContext and puts it on the request as an attribute.

Related