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

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

问题描述

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

解决方案

Spring-MVCSpring-Security

Spring-MVC 配置默认方便

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

  2. Controller 可以用作 RestController,其中响应默认由 HttpMessageConverters 处理,其中控制器方法用作 Rest-API

但是我们可以使用Spring-Security,它是一个基于过滤器的框架,它充当
安全墙(http-firewall)介于您的 Rest-API 和使用 Rest API 的客户端应用之间

Spring-MVC 应用程序和最终用户之间的 security-wall(http-firewall)


如果要求是

<块引用>

  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 的实现(比如 JwtAuthFilter)并且过滤顺序可以在 UsernamePasswordAuthenticationFilterBasicAuthenticationFilter 之前.但是您的过滤器应该读取身份验证令牌的标头,对其进行验证,并且应该创建 Authentication 对象并将其设置为 SecurityContext,而不会失败.
  • 如果请求不符合一阶 http 配置的要求,@order(2) 的 http 配置将授权.而这个配置配置JwtAuthFilter而是配置UsernamePasswordAuthenticationFilter(.formLogin()为你做这个)莉>
下面给出了这个实现的配置代码

@Configuration@启用网络安全@ComponentScan(basePackages = "com.gmail.nlpraveennl")公共类 SpringSecurityConfig{@豆角,扁豆public PasswordEncoder passwordEncoder(){返回新的 BCryptPasswordEncoder();}@配置@订单(1)公共静态类 RestApiSecurityConfig 扩展了 WebSecurityConfigurerAdapter{@自动连线私人 JwtAuthenticationTokenFilter jwtauthFilter;@覆盖protected void configure(HttpSecurity http) 抛出异常{http.csrf().disable().antMatcher("/api/**").authorizeRequests().antMatchers("/api/authenticate").permitAll().antMatchers("/api/**").hasAnyRole("APIUSER").和().addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}}@配置@订单(2)公共静态类 LoginFormSecurityConfig 扩展了 WebSecurityConfigurerAdapter{@自动连线私人密码编码器密码编码器;@自动连线public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) 抛出异常{auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin@123#")).roles("ADMIN");}@覆盖protected void configure(HttpSecurity http) 抛出异常{http.csrf().disable().antMatcher("/**").authorizeRequests().antMatchers("/resources/**").permitAll().antMatchers("/**").hasRole("管理员").and().formLogin();http.sessionManagement().maximumSessions(1).expiredUrl("/login?expired=true");}}}

实施类型 2

  • 它只有一个 http 安全配置
  • http 配置将授权所有 "/**"
  • 这里这个 http 配置是为 UsernamePasswordAuthenticationFilterJwtAuthFilter 配置的,但是 JwtAuthFilter 应该在 UsernamePasswordAuthenticationFilter 之前配置.
  • 这里使用的技巧是如果没有授权头过滤器链就继续UsernamePasswordAuthenticationFilter 并且如果UsernamePasswordAuthenticationFilter 的attemptAuthentication 方法将被调用,如果<代码>安全上下文.如果 JwtAuthFilter 验证令牌并将 auth 对象设置为 SecurityContext 那么即使过滤器链到达 UsernamePasswordAuthenticationFilter 也不会调用尝试验证方法,因为已经有一个身份验证SecurityContext 中设置的对象.
下面给出了这个实现的配置代码

@Configuration@启用网络安全@ComponentScan(basePackages = "com.gmail.nlpraveennl")公共类 SpringSecurityConfig 扩展了 WebSecurityConfigurerAdapter{@自动连线私人 JwtAuthenticationTokenFilter jwtauthFilter;@自动连线私人密码编码器密码编码器;@自动连线public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) 抛出异常{auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin@123#")).roles("ADMIN");}@覆盖protected void configure(HttpSecurity http) 抛出异常{http.csrf().disable().antMatcher("/**").authorizeRequests().antMatchers("/resources/**").permitAll().antMatchers("/api/authenticate").permitAll().antMatchers("/api/**").hasAnyRole("APIUSER","ADMIN").antMatchers("/**").hasRole("管理员").和().formLogin().和().addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);http.sessionManagement().maximumSessions(1).expiredUrl("/login?expired=true");}@豆角,扁豆public PasswordEncoder passwordEncoder(){返回新的 BCryptPasswordEncoder();}}

这都是关于两种类型的实现,您可以根据自己的要求选择任何类型的实现.对于这两种实现类型,JwtAuthenticationTokenFilterJwtTokenUtil 是通用的,如下所示.

JwtAuthenticationTokenFilter

@Component公共类 JwtAuthenticationTokenFilter 扩展了 OncePerRequestFilter{@自动连线私有 JwtTokenUtil jwtTokenUtil;@覆盖protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException{final String header = request.getHeader("授权");if (header != null && header.startsWith("Bearer ")){String authToken = header.substring(7);尝试{String username = jwtTokenUtil.getUsernameFromToken(authToken);如果(用户名!= null){if (jwtTokenUtil.validateToken(authToken, username)){//这里用户名应该用数据库验证,如果有效,从数据库中获取权限//只说硬编码列表authList = new ArrayList<>();authList.add(new SimpleGrantedAuthority("ROLE_APIUSER"));UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, authList);usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsS​​ource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}别的{System.out.println("令牌已过期");response.sendError(HttpServletResponse.SC_UNAUTHORIZED);返回;}}}捕获(例外 e){System.out.println("无法获取JWT Token,可能已过期");response.sendError(HttpServletResponse.SC_FORBIDDEN);返回;}}chain.doFilter(请求,响应);}}

JwtTokenUtil

@Component公共类 JwtTokenUtil 实现了 Serializable{私有静态最终长serialVersionUID = 8544329907338151549L;//public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60 * 1000;//5个小时public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 1000;//5分钟私人字符串秘密=我的秘密";public String getUsernameFromToken(String token){返回 getClaimFromToken(token, Claims::getSubject);}公共日期 getExpirationDateFromToken(String token){返回 getClaimFromToken(token, Claims::getExpiration);}公共 <T>T getClaimFromToken(String token, Function claimResolver){最终索赔声明 = getAllClaimsFromToken(token);返回 claimResolver.apply(claims);}私有声明 getAllClaimsFromToken(String token){返回 Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}私有布尔值 isTokenExpired(String token){最终日期到期 = getExpirationDateFromToken(token);返回到期前(新日期());}公共字符串 generateToken(字符串用户名){映射<字符串,对象>声明 = 新的 HashMap<>();返回 doGenerateToken(声明,用户名);}private String doGenerateToken(Map claim, String subject){返回承载"+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){最终字符串用户名 = getUsernameFromToken(token);return (username.equals(usernameFromToken) && !isTokenExpired(token));}}

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

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

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-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

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

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