我可以在SpringSecurity中有多种配置来保护web应用程序和RESTAPI吗?

kx5bkwkv  于 2021-07-26  发布在  Java
关注(0)|答案(2)|浏览(195)

我正在尝试在spring中创建restapi和web/mvc应用程序。它们应该使用相同的服务层。我能在spring中使用两种完全不同的配置吗(api的令牌认证、web的cookies、web的404页等等)?或者我应该做两个独立的spring应用程序?

qhhrdooz

qhhrdooz1#

springmvc和springsecurity

默认情况下,springmvc配置
控制器可以返回modelandview用于web应用程序视图服务。
控制器可用作 RestController 其中响应默认由 HttpMessageConverters 其中控制器方法用作 Rest-API 但是我们可以使用 Spring-Security 这是一个基于过滤器的框架,它充当
RESTAPI和使用RESTAPI的客户端应用程序之间的安全墙(http防火墙)

之间的安全墙(http防火墙) Spring-MVC 应用程序和最终用户

如果要求是

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

然后是实施考虑

实现类型1。restapi应该只在auth令牌存在并且有效的情况下访问。
这种实现类型的限制是,如果web应用程序希望对restapi进行ajax调用,即使浏览器有有效的会话,它也不允许访问web api。
这里restapi只用于无状态访问。
实现类型2。restapi可以通过auth令牌和session进行访问。
在这里,任何第三方应用程序(跨源)都可以通过auth令牌访问restapi。
在这里,restapi可以通过ajax调用在web应用程序(同一来源)中访问。

实现类型1

它有多个http安全配置(两个http安全配置)
其中@order(1)的http配置将仅授权 "/api/**" 此配置将不考虑url的其余部分。此http配置将配置为无状态。您应该配置 OncePerRequestFilter (说 JwtAuthFilter )过滤顺序可以在 UsernamePasswordAuthenticationFilter 或者 BasicAuthenticationFilter . 但是您的过滤器应该读取auth token的头,验证它并创建 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");
        }
    }
}

实现类型2

它只有一个http安全配置
其中http配置将授权所有 "/**" 在这里,此http配置为 UsernamePasswordAuthenticationFilter 以及 JwtAuthFilter 但是 JwtAuthFilter 应在 UsernamePasswordAuthenticationFilter .
这里使用的技巧是,若并没有授权头过滤器链只是继续 UsernamePasswordAuthenticationFilter 和尝试的身份验证方法 UsernamePasswordAuthenticationFilter 如果中没有有效的身份验证对象,则将被调用 SecurityContext . 如果 JwtAuthFilter 验证令牌并将身份验证对象设置为 SecurityContext 即使过滤链达到 UsernamePasswordAuthenticationFilter 将不会调用attemptauthentication方法,因为中已设置了身份验证对象 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();
    }
}

这两种类型的实现都是如此,您可以根据自己的需求选择任何类型的实现。对于这两种实现类型 JwtAuthenticationTokenFilter 以及 JwtTokenUtil 是常见的,如下所示。 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));
}

}

您可以从下面给出的github存储库链接下载工作示例。
实施类型1
实施类型2
如果您对springsecurity中的执行顺序感兴趣,可以在这里参考我的答案->springsecurity过滤器链是如何工作的
bwitn5fc

bwitn5fc2#

可以为所有端点编写rest控制器和normal控制器。springsecurity将在您添加身份验证流时自动添加它,如果您想覆盖它,您可以在配置中这样做。
休息控制器 /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
}
}

正常控制器 `/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
    }

}

通过这种方式,您可以在普通控制器中分离cookie逻辑并管理重定向和验证。

相关问题