我可以在 Spring Security 中有多个配置来保护 Web 应用程序和 Rest API 吗?

Mig*_*eJa 5 java rest spring web-applications spring-security

我正在尝试在 Spring 中创建 REST API 和 web/MVC 应用程序。他们都应该使用相同的服务层。我可以以某种方式在 Spring 中使用两种完全不同的配置(API 的令牌认证、Web 的 cookie、Web 的 404 页面等)?或者我应该制作两个独立的 Spring 应用程序?

Pra*_*ngi 5

Spring-MVCSpring-Security

Spring-MVC 配置默认方便

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

  2. 控制器可以用作RestController响应默认处理的HttpMessageConverters地方,控制器方法用作Rest-API

但是我们可以使用Spring-Security它是一个过滤器为基础的框架,并将其作为一个
安全壁(HTTP-防火墙)的REST的API和客户端应用程序使用REST API之间
或者
安全墙(HTTP的防火墙)之间的Spring-MVC应用和最终用户


如果要求是

  1. 安全的网络应用程序
    • 用于首次身份验证的登录表单。
    • 用于后续请求身份验证的会话。
    • 因此每个请求都会有状态,即有状态的请求
  2. Secure Rest API(基于令牌的身份验证)
    • 每个请求都将是无状态的
    • 应首选基于令牌的身份验证
    • 如果请求来自跨域(不同来源),会话将不起作用

然后实现注意事项

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

  • 这种实现类型的限制是,如果 Web 应用程序想要对 Rest API 进行 AJAX 调用,即使浏览器具有有效会话,它也不允许访问 Web-API。
  • 这里 Rest API 仅用于无状态访问。

实施类型 2。可以通过身份验证令牌和会话访问 Rest API。

  • 在这里,任何第三方应用程序(跨域)都可以通过身份验证令牌访问 Rest API。
  • 在这里,可以通过 AJAX 调用在 Web 应用程序(同源)中访问 Rest API。

实施类型 1

  • 它有多个http安全配置(两个http安全配置)
  • 其中@order(1) 的http 配置将仅授权"/api/**"其余的url 不会被此配置考虑。此 http 配置将配置为无状态。并且您应该配置OncePerRequestFilter(Say JwtAuthFilter)的实现,并且过滤器顺序可以在UsernamePasswordAuthenticationFilter或之前BasicAuthenticationFilter。但是您的过滤器应该读取身份验证令牌的标头,对其进行验证并应该创建Authentication对象并将其设置为SecurityContext不会失败。
  • 如果请求不符合一阶 http 配置的要求,@order(2) 的 http 配置将授权。而这个配置不是配置JwtAuthFilter而是配置UsernamePasswordAuthenticationFilter.formLogin()为你做这个)
下面给出了这个实现的配置代码
@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");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

实施类型 2

  • 它只有一个 http 安全配置
  • 其中 http 配置将授权所有 "/**"
  • 这里这个 http 配置是为两者配置的UsernamePasswordAuthenticationFilterJwtAuthFilterJwtAuthFilter应该在UsernamePasswordAuthenticationFilter.
  • 这里使用的技巧是,如果没有授权头过滤器链只是继续UsernamePasswordAuthenticationFilter和attemptAuthentication方法UsernamePasswordAuthenticationFilter如果在没有有效身份验证的对象将被调用SecurityContext。如果JwtAuthFilter验证令牌并将 auth 对象设置为,SecurityContext那么即使过滤器链到达UsernamePasswordAuthenticationFilter尝试验证方法也不会被调用,因为在 中已经设置了一个验证对象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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这都是关于两种类型的实现,您可以根据您的要求选择任何类型的实现。并且对于这两种实现类型JwtAuthenticationTokenFilterJwtTokenUtil是常见的并且在下面给出。

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);
    }
}
Run Code Online (Sandbox Code Playgroud) 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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

如果您对 Spring Security 中的执行顺序感到好奇,可以在此处参考我的回答 -> Spring Security过滤器链的工作原理