Can I have multiple Configurations in Spring Secur

2020-02-14 05:51发布

I'm trying to create REST API and web/MVC application in Spring. They both should use the same service layer. Can I somehow use two completely different configurations in Spring (Token authentication for API, cookies for web, 404 page for web, etc)? Or should I make two independent Spring applications?

2条回答
我只想做你的唯一
2楼-- · 2020-02-14 06:06

Spring-MVC and Spring-Security

Spring-MVC configuration by default facilitates

  1. Controller can return ModelAndView for Web application view serving purpose.

  2. Controller can be used as RestController where response is by default processed by HttpMessageConverters where controller methods used as Rest-API

However we can use Spring-Security which is a filter based framework and it acts as a
security-wall(http-firewall) between your Rest-APIs and client-app consuming Rest API
Or
security-wall(http-firewall) between Spring-MVC application and end-user


If requirement is

  1. Secure web application
    • Login form for authenticating first time.
    • Session for subsequent requests authentication.
    • Hence Every requests will have state i.e, stateful requests
  2. Secure Rest API(Token based authentication)
    • Every requests will be stateless
    • Token based authentication should be preferred
    • Session will not work in case if request is from cross-origin(different origin)

then Implementation considerations

Implementation-type 1. Rest APIs should only accessed if auth token is present and valid.

  • Limitation of this implementation type is, if web application wants to make AJAX calls to Rest API even though browser has valid session it won't allow to access Web-APIs.
  • Here Rest API is only for stateless access.

Implementation-type 2. Rest APIs can be accessed by auth token as well as session.

  • Here Rest API's can be accessed by any third party applications(cross-origin) by auth token.
  • Here Rest API's can be accessed in web application(same-origin) through AJAX calls.

Implementation-type 1

  • It has multiple http security configuration(two http security configuration)
  • where http configuration of @order(1) will authorize only "/api/**" rest of url's will not be considered by this configuration. This http configuration will be configured for stateless. And you should configure an implementation of OncePerRequestFilter(Say JwtAuthFilter) and filter order can be before UsernamePasswordAuthenticationFilter or BasicAuthenticationFilter. But your filter should read the header for auth token, validate it and should create Authentication object and set it to SecurityContext without fail.
  • And http configuration of @order(2) will authorize if request is not qualified for first order http configuration. And this configuration does not configures JwtAuthFilter but configures UsernamePasswordAuthenticationFilter(.formLogin() does this for you)
And the configuration code for this implementation is given below
@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.gmail.nlpraveennl")
public class SpringSecurityConfig
{
    @Bean
    public PasswordEncoder passwordEncoder() 
    {
        return new BCryptPasswordEncoder();
    }

    @Configuration
    @Order(1)
    public static class RestApiSecurityConfig extends WebSecurityConfigurerAdapter
    {
        @Autowired
        private JwtAuthenticationTokenFilter jwtauthFilter;

        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http
                .csrf().disable()
                .antMatcher("/api/**")
                .authorizeRequests()
                .antMatchers("/api/authenticate").permitAll()
                .antMatchers("/api/**").hasAnyRole("APIUSER")
            .and()
                .addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);

            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }

    @Configuration
    @Order(2)
    public static class LoginFormSecurityConfig extends WebSecurityConfigurerAdapter
    {
        @Autowired
        private PasswordEncoder passwordEncoder;

        @Autowired
        public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) throws Exception
        {
            auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin@123#")).roles("ADMIN");
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http
                .csrf().disable()
                .antMatcher("/**").authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/**").hasRole("ADMIN")
            .and().formLogin();

            http.sessionManagement().maximumSessions(1).expiredUrl("/login?expired=true");
        }
    }
}

Implementation-type 2

  • It has only one http security configuration
  • where http configuration will authorize all "/**"
  • Here this http configuration is configured for both UsernamePasswordAuthenticationFilter and JwtAuthFilter but JwtAuthFilter should be configured before UsernamePasswordAuthenticationFilter.
  • Trick used here is if there is no Authorization header filter chain just continues to UsernamePasswordAuthenticationFilter and attemptAuthentication method of UsernamePasswordAuthenticationFilter will get invoked if there is no valid auth object in SecurityContext. If JwtAuthFilter validates token and sets auth object to SecurityContext then even if filter chain reaches UsernamePasswordAuthenticationFilter attemptAuthentication method will not be invoked as there is already an authentication object set in SecurityContext.
And the configuration code for this implementation is given below
@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.gmail.nlpraveennl")
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter
{
    @Autowired
    private JwtAuthenticationTokenFilter jwtauthFilter;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin@123#")).roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http
            .csrf().disable()
            .antMatcher("/**").authorizeRequests()
            .antMatchers("/resources/**").permitAll()
            .antMatchers("/api/authenticate").permitAll()
            .antMatchers("/api/**").hasAnyRole("APIUSER","ADMIN")
            .antMatchers("/**").hasRole("ADMIN")
        .and()
            .formLogin()
        .and()
            .addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);

        http.sessionManagement().maximumSessions(1).expiredUrl("/login?expired=true");
    }

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

This is all about both type of implementation, you can go for any type of implementation depending upon your requirement. And for both implementation type JwtAuthenticationTokenFilter and JwtTokenUtil is common and is given below.

JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException
    {
        final String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) 
        {
            String authToken = header.substring(7);

            try
            {
                String username = jwtTokenUtil.getUsernameFromToken(authToken);
                if (username != null)
                {
                    if (jwtTokenUtil.validateToken(authToken, username))
                    {
                        // here username should be validated with database and get authorities from database if valid
                        // Say just to hard code

                        List<GrantedAuthority> authList = new ArrayList<>();
                        authList.add(new SimpleGrantedAuthority("ROLE_APIUSER"));

                        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, authList);
                        usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                    }
                    else
                    {
                        System.out.println("Token has been expired");
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                        return;
                    }
                }
            }
            catch (Exception e)
            {
                System.out.println("Unable to get JWT Token, possibly expired");
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }
        }

        chain.doFilter(request, response);
    }
}
JwtTokenUtil
@Component
public class JwtTokenUtil implements Serializable
{
    private static final long   serialVersionUID    = 8544329907338151549L;
//  public static final long    JWT_TOKEN_VALIDITY  = 5 * 60 * 60 * 1000; // 5 Hours
    public static final long    JWT_TOKEN_VALIDITY  = 5 * 60 * 1000; // 5 Minutes
    private String              secret              = "my-secret";

    public String getUsernameFromToken(String token)
    {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getExpirationDateFromToken(String token)
    {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver)
    {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token)
    {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token)
    {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    public String generateToken(String username)
    {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, username);
    }

    private String doGenerateToken(Map<String, Object> claims, String subject)
    {
        return "Bearer "+Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY)).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    public Boolean validateToken(String token, String usernameFromToken)
    {
        final String username = getUsernameFromToken(token);
        return (username.equals(usernameFromToken) && !isTokenExpired(token));
    }
}

You can download working example from my github repository link given below.
Implementation type-1
Implementation type-2

If you are curious about sequence of execution in Spring Security you can refer my answer here -> How spring security filter chain works

查看更多
在下西门庆
3楼-- · 2020-02-14 06:06

You can write a rest controller and normal controller for all endpoints. Spring security will automatically add an auth flow when you add it, and if you want to override you can do that in the configuration.

Rest Controller for /api/foo

@RestController
@RequestMapping("/api/foo")
public class FooRestController {
  //All the methods must conform to a rest api
  @GetMapping
  public String fooGet() {
    return "foo"; // this will return foo as string
  }
}

Normal controller for /ui/foo

@Controller
@RequestMapping("/ui/foo")
public class FooController {
   @RequestMapping(method = RequestMethod.GET) // You can use @GetMapping
    public ModelView homePage(Model model) {
      // set model attributes
      return "home"; // this will be mapped to home view jsp/thyme/html
    }

}

This way you can separate cookie logic and manage redirects and validations, in the normal controller.

查看更多
登录 后发表回答