Spring Security中可以有多个配置来保护Web应用程序和Rest API的安全吗? [英] Can I have multiple Configurations in Spring Security for securing web application and Rest API?

查看:119
本文介绍了Spring Security中可以有多个配置来保护Web应用程序和Rest API的安全吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试在Spring中创建REST API和Web/MVC应用程序.他们俩应该使用相同的服务层.我可以在Spring中使用两种完全不同的配置(用于API的令牌身份验证,用于Web的cookie,用于Web的404页面等)吗?还是应该制作两个独立的Spring应用程序?

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?

推荐答案

Spring-MVCSpring-Security

默认情况下,Spring-MVC配置会方便

Spring-MVC and Spring-Security

Spring-MVC configuration by default facilitates

  1. 控制器可以返回ModelAndView以用于Web应用程序视图服务.

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

控制器可用作RestController,其中默认情况下,响应由HttpMessageConverters处理,其中控制器方法用作Rest-API

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

但是我们可以使用Spring-Security,这是一个基于过滤器的框架,它可以作为
您的Rest-API和使用客户端应用程序的Rest API之间的 security-wall(http-firewall)
Spring-MVC应用程序和最终用户之间的安全墙(http-firewall)


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


  1. 保护网络应用程序
    • 用于首次认证的登录表单.
    • 用于后续请求身份验证的会话.
    • 因此,每个请求都将具有状态,即有状态请求
  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
  • 每个请求都是无状态的
  • 基于令牌的身份验证应该是首选
  • 如果请求来自跨域(不同来源),会话将无法正常工作
  • 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)

然后考虑实施

实施类型1 .仅当存在身份验证令牌且有效时,才可以访问Rest API.

then Implementation considerations

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

  • 这种实现方式的模仿是,即使浏览器具有有效的会话,如果Web应用程序想要对Rest API进行AJAX调用,它也将不允许访问Web-API.
  • Rest API仅用于无状态访问.

实施类型2 . REST API可以通过身份验证令牌以及会话进行访问.

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

  • 任何第三方应用程序(跨域)都可以通过身份验证令牌访问Rest API.
  • 可以通过AJAX调用在Web应用程序(相同来源)中访问Rest API.
  • 它具有多个http安全配置(两个http安全配置)
  • 其中@order(1)的http配置将仅授权"/api/**"其余URL,此配置将不考虑.此http配置将配置为无状态.并且您应该配置OncePerRequestFilter(说JwtAuthFilter)的实现,并且过滤器顺序可以在UsernamePasswordAuthenticationFilterBasicAuthenticationFilter之前.但是您的过滤器应读取auth令牌的标头,对其进行验证,并应创建Authentication对象并将其设置为SecurityContext.
  • 如果请求不符合一阶HTTP配置的资格,则@order(2)的HTTP配置将授权.而且此配置不会配置JwtAuthFilter而是配置UsernamePasswordAuthenticationFilter(.formLogin()为您完成此操作)
  • 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)
@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");
        }
    }
}

实施类型2

  • 只有一个http安全配置
  • 其中http配置将授权所有"/**"
  • 此处为UsernamePasswordAuthenticationFilterJwtAuthFilter都配置了此http配置,但是JwtAuthFilter应该在UsernamePasswordAuthenticationFilter之前配置.
  • 这里使用的技巧是,如果没有Authorization标头过滤器链继续到UsernamePasswordAuthenticationFilter,并且如果SecurityContext中没有有效的auth对象,则将调用UsernamePasswordAuthenticationFilter的tryAuthentication方法.如果JwtAuthFilter验证令牌并将auth对象设置为SecurityContext,则即使过滤器链到达UsernamePasswordAuthenticationFilter,由于SecurityContext中已设置了身份验证对象,也不会调用tryAuthentication方法.
  • 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.
    • @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();
          }
      }
      

      这都是关于两种类型的实现的,您可以根据需要选择任何类型的实现.对于JwtAuthenticationTokenFilterJwtTokenUtil两种实现方式,都是常见的,如下所示.

      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.

      @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));
          }
      }
      

      您可以从下面给出的github存储库链接中下载工作示例.
      实施类型1
      实施类型2

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

      如果您对Spring Security中的执行顺序感到好奇,可以在这里参考我的答案-> Spring Security筛选器链的方式有效

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

      这篇关于Spring Security中可以有多个配置来保护Web应用程序和Rest API的安全吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆