DEV Community

Ukjin Yang
Ukjin Yang

Posted on

A Little guide of Spring Web(MVC) with Custom Security for REST API

Getting started

If you want security feature in your REST API App, USE Spring security. BETTER THAN PLAIN Interceptor for WebMVC or WebFilter for Webflux your spring app.
I'll show you small tutorial with code. Let's get started.

Step 1. AuthenticationToken

Prepare your Authentication Token. If your authentication data is nothing special, just use UsernamePasswordAuthenticationToken. Are you using JWT? No worries. or, you can implement class with AbstractAuthenticationToken for identity your authentication token.

Step 2. AuthenticationFilter

When client request your app, You will get authentication infomation by request entities. like header, request body, etc.

How to make about getting authentication infomation from HTTP request? then you need to define a servlet filter. usually implements of GenericFilterBean or OncePerRequestFilter. I chose OncePerRequestFilter because it can implements method with HttpServletRequest and HttpServletResponse rather then more abstract interface like ServletRequest and ServletResponse.

@Component
public class ApiAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String principal = "Your Principal(ex. User ID) from request";
        String credentials = "Your Credentials(ex. User Password) from request";
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal , credentials, /* DEFAULT ROLES */);
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(request, response);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3. AuthenticationProvider

Next, You will need verify the authentication infomation. you need implement AuthenticationProvider for what kind of verify authentication in your app. such as Verfy JWT Signature, get from your DB, etc.

@Component
public class ApiAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        Object principal = authentication.getPrincipal();
        Object credentials = authentication.getCredentials();

        // TODO: Your verify authentication logic from principal and credentials.
        if(verified) {
            return new UsernamePasswordAuthenticationToken(principal , credentials, /* REAL ROLES FOR authenticated */);
        } else return null;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // ... or equals your AuthenticationToken class.
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4. AuthenticationEntryPoint

If you have plan of REST API, when authentication failed or unauthorized request. You have to implement own class of AuthenticationEntryPoint, you'll see unwanted unauthorizaed response by Spring Security.

@Component
public class ApiAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.error("UnAuthorizaed!!! message : " + e.getMessage());

        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        try (OutputStream os = response.getOutputStream()) {
            // write your own unauthorized response here.
            objectMapper.writeValue(os, e);
            os.flush();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5. AccessDeniedHandler

also you need implement class of AccessDeniedHandler for your own response when authentication successful but no have permission roles.

@Component
public class ApiAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        log.error("Forbidden!!! message : " + e.getMessage());

        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.FORBIDDEN.value());
        try (OutputStream os = response.getOutputStream()) {
            // write your own FORBIDDEN response here.
            objectMapper.writeValue(os, e);
            os.flush();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6. WebSecurityConfigurerAdapter

At last, configure your app for secure, implement WebSecurityConfigurerAdapter and import security classes you made.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
@Import({ ApiAuthenticationFilter.class, ApiAuthenticationProvider.class, ApiAuthenticationEntryPoint.class, ApiAccessDeniedHandler.class})
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final ApiAuthenticationFilter apiAuthenticationFilter;
    private final ApiAuthenticationProvider apiAuthenticationProvider;
    private final ApiAuthenticationEntryPoint apiAuthenticationEntryPoint;
    private final ApiAccessDeniedHandler apiAccessDeniedHandler;

    @Autowired
    public SecurityConfig() {
        // TODO: Autowire these beans...
        // or use @RequiredArgsConstructor instead if you are using lombok.
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(apiAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable()
            .csrf().disable() // required if rest api.
            .cors().and() // reuired if client is browser.
            // Required if you want stateless authentication method like JWT.
             .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(apiAuthenticationEntryPoint)
            .accessDeniedHandler(apiAccessDeniedHandler)
            .and()
            .authorizeRequests()
            .antMatchers("/public/**").permitAll() // for public resources
            .anyRequest().authenticated()
            .expressionHandler(defaultWebSecurityExpressionHandler())
            .and()
            .addFilterBefore(apiAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .formLogin().disable();
    }

    // optional: if you want role names without prefix.
    private DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler() {
        DefaultWebSecurityExpressionHandler result = new DefaultWebSecurityExpressionHandler();
        result.setDefaultRolePrefix("");
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

Done. next start your spring boot app and check it works.
Happy Coding!

Discussion (0)