Spring Security:认证和授权深入
阅读原文时间:2021年04月25日阅读:1

您可以使用本指南来了解什么是Spring Security,以及其核心功能(如身份验证,授权或常见漏洞利用保护)如何工作。 此外,还有全面的常见问题解答。

(编者注:大约6500个字,您可能不想尝试在移动设备上阅读。 将其添加为书签,稍后再返回。)

Introduction

Sooner or later everyone needs to add security to his project and in the Spring ecosystem you do that with the help of the 小号pring Security library.

So you go along, add Spring Security to your Spring Boot (or plain 小号pring) project and suddenly…​

  • …您有自动生成的登录页面。…您无法再执行POST请求。…您的整个应用程序处于锁定状态,并提示您输入用户名和密码。

在随后的精神崩溃中幸存下来之后,您可能会对所有这些工作原理感兴趣。

What is Spring Security and how does it work?

简短的答案:

At its core, Spring Security is really just a bunch of servlet filters that help you add authentication and authorization to your web application.

It also integrates well with frameworks like Spring Web MVC (or Spring Boot), as well as with standards like OAuth2 or SAML. And it auto-generates login/logout pages and protects against common exploits like CSRF.

现在,那真的没有帮助,对吗?

幸运的是,答案也很长:

本文的其余部分。

Web Application Security: 101

在成为Spring Security Guru之前,您需要了解三个重要概念:

  1. 认证方式授权书Servlet过滤器

家长咨询:请勿跳过此部分,因为这是一切Spring Security所做的。 另外,我将使其尽可能有趣。

1. Authentication

首先,如果您正在运行典型的(Web)应用程序,则需要您的用户认证。 这意味着您的应用程序需要验证用户是否WHO他声称通常是通过用户名和密码检查来完成的。

用户:“我是美国总统。我的用户名是:potus!”

您的网络应用:“当然,您的身份是什么密码那么,总统先生?”

用户:“我的密码是:th3don4ld”。

您的网络应用:“正确。欢迎您,先生!”

2. Authorization

在更简单的应用程序中,身份验证可能就足够了:用户进行身份验证后,便可以访问应用程序的每个部分。

但是大多数应用程序都有权限(或角色)的概念。 想象一下:有权访问您的Webshop面向公众的前端的客户,以及有权访问单独的管理区域的管理员。

两种类型的用户都需要登录,但是仅凭身份验证并不能说明允许他们在系统中执行的操作。 因此,您还需要检查经过身份验证的用户的权限,即您需要授权用户。

用户:“让我玩那个核足球…”。

您的网络应用:“一秒钟,我需要检查您的权限首先…..是的,主席先生,您的通关等级合适。 请享用。”

用户:“那个红色按钮又是什么… ??

3. Servlet Filters

Last but not least, let’s have a look at Servlet Filters. What do they have to do with authentication and authorization? (If you are completely new to Java Servlets or Filters, I advise you to read the old, but still very valid Head First Servlets book.)

Why use Servlet Filters?

Think back to my other article, where we found out that basically any Spring web application is just one servlet: Spring’s good old DispatcherServlet, that redirects incoming HTTP requests (e.g. from a browser) to your @Controllers or @RestControllers.

关键是:该DispatcherServlet中没有安全编码,您也很可能不想在@Controllers中摸索原始的HTTP Basic Auth标头。 最好应进行身份验证和授权之前请求命中您的@Controllers。

Luckily, there’s a way to do exactly this in the Java web world: you can put filters in front of servlets, which means you could think about writing a SecurityFilter and configure it in your Tomcat (servlet container/application server) to filter every incoming HTTP request before it hits your servlet.

+-------------------------------+           +-----------------------------------+
| Browser                       |           | SecurityFilter (Tomcat)           |
|-------------------------------|           |-----------------------------------|
|                               |           |                                   |
| https://my.bank/account       |  -------> | Check if user is authenticated/   |
|                               |           |    1. authenticated               |
|                               |           |    2. authorized                  |
|                               |           |                                   |
|                               |           | -- if false: HTTP 401/403         |  ---------> +-----------------------------------+
|                               |           | -- if true:  continue to servlet  |             | DispatcherServlet (Tomcat)        |
|                               |           |                                   |             | @RestController/@Controller       |
|                               |           +-----------------------------------+             +-----------------------------------+
+-------------------------------+

A naive SecurityFilter

一个SecurityFilter大约有4个任务,一个过分简单的实现可能看起来像这样:

    import javax.servlet.*;
    import javax.servlet.http.HttpFilter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;

    public class SecurityServletFilter extends HttpFilter {

        @Override
        protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

            UsernamePasswordToken token = extractUsernameAndPasswordFrom(request);  

            if (notAuthenticated(token)) {  
                // either no or wrong username/password
                // unfortunately the HTTP status code is called "unauthorized", instead of "unauthenticated"
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401.
                return;
            }

            if (notAuthorized(token, request)) { 
                // you are logged in, but don't have the proper rights
                response.setStatus(HttpServletResponse.SC_FORBIDDEN); // HTTP 403
                return;
            }

            // allow the HttpRequest to go to Spring's DispatcherServlet
            // and @RestControllers/@Controllers.
            chain.doFilter(request, response); 
        }

        private UsernamePasswordToken extractUsernameAndPasswordFrom(HttpServletRequest request) {
            // Either try and read in a Basic Auth HTTP Header, which comes in the form of user:password
            // Or try and find form login request parameters or POST bodies, i.e. "username=me" & "password="myPass"
            return checkVariousLoginOptions(request);
        }


        private boolean notAuthenticated(UsernamePasswordToken token) {
            // compare the token with what you have in your database...or in-memory...or in LDAP...
            return false;
        }

        private boolean notAuthorized(UsernamePasswordToken token, HttpServletRequest request) {
           // check if currently authenticated user has the permission/role to access this request's /URI
           // e.g. /admin needs a ROLE_ADMIN , /callcenter needs ROLE_CALLCENTER, etc.
           return false;
        }
    }
  1. First, the filter needs to extract a username/password from the request. It could be via a Basic Auth HTTP Header, or form fields, or a cookie, etc.

  2. Then the filter needs to validate that username/password combination against something, like a database.

  3. The filter needs to check, after successful authentication, that the user is authorized to access the requested URI.

  4. If the request survives all these checks, then the filter can let the request go through to your DispatcherServlet, i.e. your @Controllers.

FilterChains

现实检查:上面的代码可以编译时,迟早会导致一个带有大量用于各种身份验证和授权机制的代码的怪兽过滤器。

但是,在现实世界中,您可以将此过滤器拆分为多过滤器,然后链一起。

例如,传入的HTTP请求将…

  1. 首先,通过LoginMethodFilter …然后,通过AuthenticationFilter …然后,通过AuthorizationFilter …最后,点击您的servlet。

这个概念叫做过滤链上面过滤器中的最后一个方法调用实际上是委托给那个链:

    chain.doFilter(request, response);

使用这种过滤器(链),您基本上可以处理应用程序中存在的每个身份验证或授权问题,而无需更改实际的应用程序实现(请考虑:@RestControllers / @Controllers)。

有了这些知识,让我们了解一下Spring Security如何利用这种过滤魔术。

FilterChain & Security Configuration DSL

我们将以与上一章相反的方向开始,从Spring Security的FilterChain开始,以非常规的方式介绍Spring Security。

Spring’s DefaultSecurityFilterChain

Let’s assume you set up Spring Security correctly and then boot up your web application. You’ll see the following log message:

    2020-02-25 10:24:27.875  INFO 11116 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46320c9a, org.springframework.security.web.context.SecurityContextPersistenceFilter@4d98e41b, org.springframework.security.web.header.HeaderWriterFilter@52bd9a27, org.springframework.security.web.csrf.CsrfFilter@51c65a43, org.springframework.security.web.authentication.logout.LogoutFilter@124d26ba, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@61e86192, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@10980560, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32256e68, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52d0f583, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5696c927, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5f025000, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e7abaf7, org.springframework.security.web.session.SessionManagementFilter@681c0ae6, org.springframework.security.web.access.ExceptionTranslationFilter@15639d09, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4f7be6c8]|

如果将那一行扩展到列表中,看起来Spring Security不仅会安装一过滤器,而是安装由15(!)个不同的过滤器组成的整个过滤器链。

因此,当HTTP请求进入时,它将通过所有 these 15 filters, before your request fin所有y hits your @RestControllers. The order is important, too, starting at the top of that list and going down to the bottom.

+----------------------------------+           +----------------------------------------+          +---------------------------------------+
| Browser HTTP Request             |---------> | SecurityContextPersistenceFilter       | -------> | HeaderWriterFilter                    | ----->
+----------------------------------+           +----------------------------------------+          +---------------------------------------+

+----------------------------------+           +----------------------------------------+          +---------------------------------------+
| CsrfFilter                       |---------> |            LogoutFilter                | -------> | UsernamePasswordAuthenticationFilter  | ----->
+----------------------------------+           +----------------------------------------+          +---------------------------------------+

+----------------------------------+           +----------------------------------------+          +--------------------------------------+
| DefaultLoginPageGeneratingFilter |---------> | DefaultLogoutPageGeneratingFilter      | -------> | BasicAuthenticationFilter            | ----->
+----------------------------------+           +----------------------------------------+          +--------------------------------------+

+----------------------------------+           +----------------------------------------+          +--------------------------------------+
| RequestCacheAwareFilter          |---------> | SecurityContextHolderAwareRequestFilter| -------> | AnonymousAuthenticationFilter        | ----->
+----------------------------------+           +----------------------------------------+          +--------------------------------------+

+----------------------------------+           +----------------------------------------+          +--------------------------------------+
| SessionManagementFilter          |---------> | ExceptionTranslationFilter             | -------> | FilterSecurityInterceptor            | ----->
+----------------------------------+           +----------------------------------------+          +--------------------------------------+

+----------------------------------+
| your @RestController/@Controller |
+----------------------------------+

Analyzing Spring’s FilterChain

It would go too far to have a detailed look at every filter of this chain, but here’s the explanations for a few of those filters. Feel free to look at Spring Security’s source code to understand the other filters.

  • BasicAuthenticationFilter:尝试在请求中找到基本身份验证HTTP标头,如果找到,则尝试使用标头的用户名和密码对用户进行身份验证。UsernamePasswordAuthenticationFilter:尝试查找用户名/密码请求参数/ POST正文,如果找到,则尝试使用这些值对用户进行身份验证。DefaultLoginPageGeneratingFilter:如果您未明确禁用该功能,则会为您生成一个登录页面。 这是启用Spring Security时获得默认登录页面的原因。DefaultLogoutPageGeneratingFilter:如果您未明确禁用该功能,则会为您生成一个注销页面。FilterSecurityInterceptor:请您授权。

因此,通过这两个过滤器,Spring Security为您提供了一个登录/注销页面,并提供了使用基本身份验证或表单登录进行登录的功能,以及一些其他功能,例如CsrfFilter,我们将拥有一个 以后再看。

中场休息:这些过滤器在很大程度上是春季安全。 不多不少。 他们完成所有工作。 剩下的就是配置它们是如何工作的,即要保护哪些URL,要忽略哪些URL以及要用于身份验证的数据库表。

因此,接下来我们需要看看如何配置Spring Security。

How to configure Spring Security: WebSecurityConfigurerAdapter

使用最新的Spring Security和/或Spring Boot版本,配置Spring Security的方法是通过具有以下类:

  1. 用@EnableWebSecurity注释。扩展了WebSecurityConfigurer,它基本上为您提供了配置DSL /方法。 使用这些方法,您可以指定应用程序中要保护的URI或要启用/禁用的漏洞利用保护。

典型的WebSecurityConfigurerAdapter如下所示:

    @Configuration
    @EnableWebSecurity 
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 

      @Override
      protected void configure(HttpSecurity http) throws Exception {  
          http
            .authorizeRequests()
              .antMatchers("/", "/home").permitAll() 
              .anyRequest().authenticated() 
              .and()
           .formLogin() 
             .loginPage("/login") 
             .permitAll()
             .and()
          .logout() 
            .permitAll()
            .and()
          .httpBasic(); 
      }
    }
  1. A normal Spring @Configuration with the @EnableWebSecurity annotation, extending from WebSecurityConfigurerAdapter.

  2. By overriding the adapter’s configure(HttpSecurity) method, you get a nice little DSL with which you can configure your FilterChain.

  3. All requests going to / and /home are allowed (permitted) - the user does not have to authenticate. You are using an antMatcher, which means you could have also used wildcards (*, \*\*, ?) in the string.

  4. Any other request needs the user to be authenticated first, i.e. the user needs to login.

  5. You are allowing form login (username/password in a form), with a custom loginPage (/login, i.e. not Spring Security’s auto-generated one). Anyone should be able to access the login page, without having to log in first (permitAll; otherwise we would have a Catch-22!).

  6. The same goes for the logout page

  7. On top of that, you are also allowing Basic Auth, i.e. sending in an HTTP Basic Auth Header to authenticate.

How to use Spring Security’s configure DSL

It takes some time getting used to that DSL, but you’ll find more examples in the FAQ section: AntMatchers: Common Examples.

现在重要的是这个配置methodiswhereyouspecify:

  1. 要保护的URL(authenticated())和允许的URL(permitAll())。允许哪些身份验证方法(formLogin(),httpBasic())及其配置方式。简而言之:您的应用程序的完整安全配置。

注意:您不需要立即重写适配器的configure方法,因为它带有相当合理的实现-默认情况下。 看起来是这样的:

    public abstract class WebSecurityConfigurerAdapter implements
            WebSecurityConfigurer<WebSecurity> {

        protected void configure(HttpSecurity http) throws Exception {
                http
                    .authorizeRequests()
                        .anyRequest().authenticated()  
                        .and()
                    .formLogin().and()   
                    .httpBasic();  
            }
    }
  1. 进入任何URI(任何Request())在您的应用程序上,您需要进行身份验证(authenticated())。表单登录(formLogin())的默认设置已启用。和HTTP基本身份验证一样(httpBasic())。

这个默认配置是您在向其添加Spring Security之后立即将其锁定的原因。 很简单,不是吗?

Summary: WebSecurityConfigurerAdapter’s DSL configuration

我们了解到,Spring Security由使用WebSecurityConfigurerAdapter @Configuration类配置的几个过滤器组成。

但是,还有一个关键的环节缺失。 让我们以Spring的BasicAuthFilter为例。 它可以从HTTP Basic Auth标头中提取用户名/密码,但是它能做什么?认证这些凭证反对吗?

这自然使我们想到了身份验证如何与Spring Security一起工作的问题。

Authentication with Spring Security

在身份验证和Spring Security方面,您大致有以下三种情况:

  1. The default: You can access the (encrypted) password of the user, because you have his details (username, password) saved in e.g. a database table.

  2. Less common: You cannot access the (encrypted) password of the user. This is the case if your users and passwords are stored somewhere else, like in a 3rd party identity management product offering REST services for authentication. Think: Atlassian Crowd.

  3. Also popular: You want to use OAuth2 or "Login with Google/Twitter/etc." (OpenID), likely in combination with JWT. Then none of the following applies and you should go straight to the OAuth2 chapter.

注意:根据您的情况,您需要指定不同的@Beans才能使Spring Security正常工作,否则最终将获得令人困惑的异常(例如,如果您忘记指定PasswordEncoder,则为NullPointerException)。 记在脑子里。

让我们来看看前两种情况。

1. UserDetailsService: Having access to the user’s password

假设您有一个存储用户的数据库表。 它有几列,但最重要的是,它有一个用户名和密码列,您可以在其中存储用户的加密(!)密码。

create table users (id int auto_increment primary key, username varchar(255), password varchar(255));

在这种情况下,Spring Security需要您定义两个bean以启动并运行身份验证。

  1. 一个UserDetailsS​​ervice。密码编码器。

指定UserDetailsS​​ervice就是这样简单:

    @Bean
    public UserDetailsService userDetailsService() {
        return new MyDatabaseUserDetailsService(); 
    }
  1. MyDatabaseUserDetailsS​​ervice实现了UserDetailsS​​ervice,这是一个非常简单的接口,由一个返回UserDetails对象的方法组成:

    public class MyDatabaseUserDetailsService implements UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
         // 1. Load the user from the users table by username. If not found, throw UsernameNotFoundException.
         // 2. Convert/wrap the user to a UserDetails object and return it.
        return someUserDetails;
    }
    } public interface UserDetails extends Serializable {
    String getUsername();
    
    String getPassword();
    
    // &lt;3&gt; more methods:
    // isAccountNonExpired,isAccountNonLocked,
    // isCredentialsNonExpired,isEnabled
    }
  2. UserDetailsS​​ervice通过用户的用户名加载UserDetails。 请注意,该方法需要只要一个参数:用户名(不是密码)。UserDetails界面具有获取(加密!)密码的方法和获取用户名的方法。UserDetails拥有更多方法,例如帐户是处于活动状态还是已被阻止,凭据已过期或用户具有什么权限-但我们将不在此处介绍。

因此,您可以像上面一样自行实现这些接口,也可以使用Spring Security提供的现有接口。

Off-The-Shelf Implementations

简要说明一下:您始终可以自己实现UserDetailsS​​ervice和UserDetails接口。

但是,您还会发现Spring Security提供的现成的实现,您可以改为使用/配置/扩展/覆盖。

  1. JdbcUserDetailsManager,这是基于JDBC(数据库)的UserDetailsS​​ervice。 您可以配置它以匹配您的用户表/列结构。InMemoryUserDetailsManager, which keeps all 用户details in-memory and is great for testing.org.springframework.security.core.用户detail.User, which is a sensible, default UserDetails implementation that you could use. That would mean potentially mapping/copying between your entities/database tables and this 用户 class. Alternatively, you could simply make your entities implement the UserDetails interface.

Full UserDetails Workflow: HTTP Basic Authentication

现在回想一下您的HTTP基本认证,这意味着您正在使用Spring Security和Basic Auth保护您的应用程序。 当您指定UserDetailsS​​ervice并尝试登录时,将发生以下情况:

  1. 从过滤器中的HTTP Basic Auth标头中提取用户名/密码组合。 您无需为此做任何事情,它会在后台进行。呼叫你的MyDatabaseUserDetailsS​​ervice从数据库中加载相应的用户,并包装为UserDetails对象,该对象公开了用户的加密密码。从HTTP Basic Auth标头中提取提取的密码,对其进行加密自动地 and compare it with the encrypted password from 你的 UserDetails object. If both match, the user is successfully authenticated.

这里的所有都是它的。 但是等等怎么样Spring Security如何加密客户端的密码(步骤3)? 用什么算法?

PasswordEncoders

Spring Security无法神奇地猜测您首选的密码加密算法。 因此,您需要指定另一个@Bean,一个密码编码器。 例如,如果您想使用BCrypt加密(Spring Security的默认设置)您所有的密码, you would specify this @Bean in your SecurityConfig。

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

如果有的话多密码加密算法,因为您有一些旧用户的密码是用MD5存储的(不这样做),而较新的用户是使用Bcrypt甚至是SHA-256之类的第三种算法存储的? 然后,您将使用以下编码器:

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

该委托编码器如何工作? 它将查看UserDetail的加密密码(来自您的数据库表),该密码现在必须以{字首}。 该前缀是您的加密方法! 您的数据库表将如下所示:

username

password

john@doe.com

{bcrypt}$2y$12$6t86Rpr3llMANhCUt26oUen2WhvXr/A89Xo9zJion8W7gWgZ/zA0C

my@user.com

{sha256}5ffa39f5757a0dad5dfada519d02c6b71b61ab1df51b4ed1f3beed6abe0ff5f6

Spring Security将:

  1. 读入这些密码并删除前缀({bcrypt}或{sha256})。根据前缀值,使用正确的PasswordEncoder(即BCryptEncoder或SHA256Encoder)使用该PasswordEncoder加密传入的未加密密码,并将其与存储的密码进行比较。

这就是PasswordEncoders的全部内容。

Summary: Having access to the user’s password

本部分的要点是:如果您正在使用Spring Security并有权访问用户的密码,则:

  1. 指定一个UserDetailsS​​ervice。 定制实现或使用并配置Spring Security提供的实现。指定一个PasswordEncoder。

简而言之,就是Spring Security认证。

2. AuthenticationProvider: Not having access to the user’s password

Now, imagine that you are using Atlassian Crowd for centralized identity management. That means all your users and passwords for all your applications are stored in Atlassian Crowd and not in your database table anymore.

这有两个含义:

  1. 你做没有用户密码将在您的应用程序中删除,因为您不能要求Crowd仅向您提供这些密码。但是,您确实拥有可以使用您的用户名和密码登录的REST API。 (向POST请求/ rest / usermanagement / 1 /身份验证REST端点)。

在这种情况下,您将无法再使用UserDetailsS​​ervice,而需要实现并提供一个身份验证提供者@豆。

        @Bean
        public AuthenticationProvider authenticationProvider() {
            return new AtlassianCrowdAuthenticationProvider();
        }

AuthenticationProvider主要由一种方法组成,并且一个简单的实现可能看起来像这样:

    public class AtlassianCrowdAuthenticationProvider implements AuthenticationProvider {

            Authentication authenticate(Authentication authentication)  
                    throws AuthenticationException {
                String username = authentication.getPrincipal().toString(); 
                String password = authentication.getCredentials().toString(); 

                User user = callAtlassianCrowdRestService(username, password); 
                if (user == null) {                                     
                    throw new AuthenticationException("could not login");
                }
                return new UserNamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities()); 
            }
            // other method ignored
    }
  1. 与仅可以访问用户名的UserDetails load()方法相比,现在可以访问完整的身份验证尝试,通常包含用户名和密码。您可以执行想要验证用户身份的任何操作,例如 调用REST服务。如果身份验证失败,则需要引发异常。如果身份验证成功,则需要返回一个完全初始化的UsernamePasswordAuthenticationToken。 它是Authentication接口的一种实现,需要将authenticated字段设置为true(上面使用的构造函数将自动设置该字段)。 我们将在下一章介绍有关权限。

Full AuthenticationProvider Workflow: HTTP Basic Authentication

现在回想一下您的HTTP基本认证,这意味着您正在使用Spring Security和Basic Auth保护您的应用程序。 当您指定AuthenticationProvider并尝试登录时,将发生以下情况:

  1. 从过滤器中的HTTP Basic Auth标头中提取用户名/密码组合。 您无需为此做任何事情,它会在后台进行。呼叫你的 AuthenticationProvider (e.g. AtlassianCrowdAuthenticationProvider) with that username and password for you to do the authentication (e.g. REST call) 你的self.

没有进行密码加密或类似操作,因为您实际上是委派第三方进行实际的用户名/密码检查。 简而言之就是AuthenticationProvider身份验证!

Summary: AuthenticationProvider

本节的要点是:如果您正在使用Spring Security和不要有权访问用户的密码,然后实现并提供AuthenticationProvider @Bean

Authorization with Spring Security

到目前为止,我们仅讨论了身份验证,例如 用户名和密码检查。

现在让我们看一下权限,或者说角色和当局在Spring Security中发言。

What is Authorization?

以典型的电子商务网上商店为例。 它可能由以下部分组成:

  • 网上商店本身。 假设它的网址是www。youramazinshop。com。Maybe an area for callcenter agents, where they can login and see what a customer recently bought or where their parcel is。 Its URL could be www。youramazinshop。com/callcenter。A separate admin area, where administrators can login and manage callcenter agents or other technical aspects (like themes, performance, etc。) of the web-shop。 Its URL could be www。youramazinshop。com/admin。

这具有以下含义,因为仅对用户进行身份验证已不再足够:

  • 客户显然不应该能够访问呼叫中心要么管理区。 只允许他在网站上购物。呼叫中心座席应该不能访问管理区域。管理员可以访问网上商店,呼叫中心区域和管理员区域。

简而言之,您要允许不同的用户访问,具体取决于他们当局要么角色。

What are Authorities? What are Roles?

简单:

  • 权限(以最简单的形式)只是一个字符串,它可以是:user,管理员,角色_管理员或53cr37_r0l3。角色是具有角色_字首。 所以一个角色叫做管理员与称为角色_管理员。

角色和权限之间的区别纯粹是概念上的,这常常使Spring Security的新手感到困惑。

Why is there a distinction between roles and authorities?

老实说,我已经阅读了Spring Security文档以及有关此问题的几个相关StackOverflow线程,我无法给您确切的信息,好回答。

What are GrantedAuthorities? What are SimpleGrantedAuthorities?

当然,Spring Security不能让您脱身只是使用字符串。 有一个Java类代表您的权限String,一种流行的类型是SimpleGrantedAuthority。

    public final class SimpleGrantedAuthority implements GrantedAuthority {

        private final String role;

        @Override
        public String getAuthority() {
            return role;
        }
    }

(请注意:还有其他权限类,可让您在字符串旁边存储其他对象(例如主体),这里不再赘述。目前,我们仅使用SimpleGrantedAuthority。)

1. UserDetailsService: Where to store and get authorities?

Assuming you are storing the users in your own application (think: UserDetailsService), you are going to have a Users table.

现在,您只需在其中添加一个名为“ authorities”的列即可。 对于本文,我在这里选择了一个简单的字符串列,尽管它可以包含多个逗号分隔的值。 另外,我也可以有一个完全独立的表AUTHORITIES,但是在本文的范围内,可以这样做。

ñote: Referring back to section_title: You save authorities, i.e. Strings, to the database. It so happens that these authorities start with the ROLE_ prefix, so, in terms of Spring Security these authorities are also roles.

username

password

authorities

john@doe.com

{bcrypt}…​

ROLE_ADMIN

my@callcenter.com

{sha256}…​

ROLE_CALLCENTER

剩下要做的就是调整UserDetailsS​​ervice以便在该权限列中阅读。

    public class MyDatabaseUserDetailsService implements UserDetailsService {

      UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         User user = userDao.findByUsername(username);
         List<SimpleGrantedAuthority> grantedAuthorities = user.getAuthorities().map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList()); 
         return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities); 
      }

    }
  1. 您只需将数据库列中的内容映射到SimpleGrantedAuthority列表。 做完了同样,我们在这里使用Spring Security的UserDetails的基本实现。 您也可以使用自己的类在此处实现UserDetails,甚至不必进行映射。

2. AuthenticationManager: Where to store and get authorities?

当用户来自第三方应用程序(例如Atlassian Cloud)时,您需要找出他们用于支持权限的概念。 Atlassian Crowd具有“角色”的概念,但不赞成使用“组”。

因此,根据您使用的实际产品,您需要在AuthenticationProvider中将此映射到Spring Security授权。

    public class AtlassianCrowdAuthenticationProvider implements AuthenticationProvider {

        Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            String username = authentication.getPrincipal().toString();
            String password = authentication.getCredentials().toString();

            atlassian.crowd.User user = callAtlassianCrowdRestService(username, password); 
            if (user == null) {
                throw new AuthenticationException("could not login");
            }
            return new UserNamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), mapToAuthorities(user.getGroups())); 
        }
            // other method ignored
    }
  1. 注意:这不是实际Atlassian人群代码,但有其用途。 您针对REST服务进行身份验证,并获取一个JSON User对象,然后将其转换为atlassian.crowd.User对象。该用户可以是一个或多个组的成员,这里假定只是字符串。 然后,您只需将这些组映射到Spring的“ SimpleGrantedAuthority”。

Revisiting WebSecurityConfigurerAdapter for Authorities

到目前为止,我们已经讨论了很多有关在Spring Security中为经过身份验证的用户存储和检索授权的信息。 但是你怎么样保护具有Spring Security DSL的具有不同权限的URL? 简单:

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
              .authorizeRequests()
                .antMatchers("/admin").hasAuthority("ROLE_ADMIN") 
                .antMatchers("/callcenter").hasAnyAuthority("ROLE_ADMIN", "ROLE_CALLCENTER") 
                .anyRequest().authenticated() 
                .and()
             .formLogin()
               .and()
             .httpBasic();
        }
    }
  1. 要访问/管理员您(即用户)需要验证的区域和拥有权限(简单字符串)ROLE_ADMIN。要访问/呼叫中心您需要认证的区域和拥有权限ROLE_ADMIN要么ROLE_CALLCENTER。对于任何其他请求,您不需要特定角色,但仍需要进行身份验证。

注意,上面的代码(1,2)是当量到以下内容:

      http
        .authorizeRequests()
          .antMatchers("/admin").hasRole("ADMIN") 
          .antMatchers("/callcenter").hasAnyRole("ADMIN", "CALLCENTER") 
  1. 现在,您无需调用“ hasAuthority”,而是调用“ hasRole”。注意:Spring Security将寻找一个称为ROLE_ADMIN在经过身份验证的用户上。现在,您无需调用“ hasAnyAuthority”,而是调用“ hasAnyRole”。注意:Spring Security将寻找一个称为ROLE_ADMIN要么ROLE_CALLCENTER在经过身份验证的用户上。

hasAccess and SpEL

最后但并非最不重要的是,配置授权最强大的方法是使用访问方法。 它使您几乎可以指定任何有效的SpEL表达式。

      http
        .authorizeRequests()
          .antMatchers("/admin").access("hasRole('admin') and hasIpAddress('192.168.1.0/24') and @myCustomBean.checkAccess(authentication,request)") 
  1. 您正在检查用户是否具有ROLE_ADMIN,特定的IP地址以及自定义bean检查。

To get a full overview of what’s possible with Spring’s Expression-Based Access Control, have a look at the official documentation.

Common Exploit Protections

Spring Security可帮助您防御多种常见攻击。 它从定时攻击开始(即,即使用户不存在,Spring Security也会始终在登录时对提供的密码进行加密),最后以针对缓存控制攻击,内容嗅探,单击Jacking,跨站点脚本等的保护措施进行保护。

在本指南的范围内,不可能详细介绍每种攻击。 因此,我们将只关注使大多数Spring Security新手失去最大支持的一种保护:跨站点请求伪造。

Cross-Site-Request-Forgery: CSRF

If you are completely new to CSRF, you might want to watch this YouTube video to get up to speed with it. However, the quick takeaway is, that by default Spring Security protects any incoming POST (or PUT/DELETE/PATCH) request with a valid CSRF token.

这意味着什么?

CSRF & Server-Side Rendered HTML

想象一下与此有关的银行转帐表格或任何表格(例如登录表格),这些表格将由您的@Controllers在模板技术(如Thymeleaf或Freemarker)的帮助下呈现。

    <form action="/transfer" method="post">  <!-- 1 -->
      <input type="text" name="amount"/>
      <input type="text" name="routingNumber"/>
      <input type="text" name="account"/>
      <input type="submit" value="Transfer"/>
    </form>

启用S​​pring Security后,您可以将无法再提交该表格。 因为Spring Security的CSRFFilter正在寻找其他隐藏参数上任何职位(PUT / DELETE)请求:所谓的CSRF令牌。

默认情况下,它会生成这样的令牌,每个HTTP会话并将其存储在那里。 而且,您需要确保将其注入到任何HTML表单中。

CSRF Tokens & Thymeleaf

由于Thymeleaf与Spring Security(与Spring Boot结合使用)具有良好的集成,因此您只需将以下代码段添加到任何表单中,即可从会话中自动将令牌注入到表单中。 更好的是,如果您在表单中使用“ th:action”,Thymeleaf将自动地为您注入该隐藏字段,而无需手动进行。

    <form action="/transfer" method="post">  <!-- 1 -->
      <input type="text" name="amount"/>
      <input type="text" name="routingNumber"/>
      <input type="text" name="account"/>
      <input type="submit" value="Transfer"/>
      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    </form>

    <!-- OR -->

    <form th:action="/transfer" method="post">  <!-- 2 -->
      <input type="text" name="amount"/>
      <input type="text" name="routingNumber"/>
      <input type="text" name="account"/>
      <input type="submit" value="Transfer"/>
    </form>
  1. 在这里,我们正在手动添加CSRF参数。在这里,我们正在使用Thymeleaf的表单支持。

Note: For more information on Thymeleaf’s CSRF support, see the official documentation.

CSRF & Other Templating Libraries

我不能在本节中介绍所有模板库,但作为最后的选择,您始终可以将CSRFToken注入任何@Controller方法中,并将其简单地添加到模型中以在视图中呈现它,或直接将其作为HttpServletRequest请求属性访问 。

    @Controller
    public class MyController {
        @GetMaping("/login")
        public String login(Model model, CsrfToken token) {
            // the token will be injected automatically
            return "/templates/login";
        }
    }

CSRF & React or Angular

对于Javascript应用(例如React或Angular单页应用),情况有所不同。 这是您需要做的:

  1. Configure Spring Security to use a CookieCsrfTokenRepository, which will put the CSRFToken into a cookie "XSRF-TOKEN" (and send that to the browser).

  2. Make your Javascript app take that cookie value, and send it as an "X-XSRF-TOKEN" header with every POST(/PUT/PATCH/DELETE) request.

For a full copy-and-paste React example, have a look at this great blog post: https://developer.okta.com/blog/2018/07/19/simple-crud-react-and-spring-boot.

Disabling CSRF

如果仅提供CSRF保护没有任何意义的无状态REST API,则将完全禁用CSRF保护。 这是您的操作方式:

    @EnableWebSecurity
    @Configuration
    public class WebSecurityConfig extends
       WebSecurityConfigurerAdapter {

      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http
          .csrf().disable();
      }
    }

OAuth2

坏蛋! 本部分只是下一篇文章的预告片:Spring Security&OAuth2。 为什么?

由于Spring Security的OAuth2集成实际上是一个复杂的主题,足以容纳另外7,000-10,000个单词,因此不属于本文的范围。

敬请关注。

Spring Integrations

Spring Security & Spring Framework

对于本文的大部分内容,您仅在网络层您的应用程序。 您通过WebSecurityConfigurerAdapter的DSL使用antMatcher或regexMatchers保护了某些URL。 这是完美的安全标准方法。

除了保护您的网络层之外,还有“纵深防御”的想法。 这意味着除了保护URL外,您可能还希望保护业务逻辑本身。 想想:您的@ Controllers,@ Components,@ Services甚至@Repositories。 简而言之,您的Spring bean。

Method Security

这种方法称为方法安全 and works through annotations that you can basically put on any public method of your Spring beans. You also need to explicitly enable 方法安全 by putting the @EnableGlobalMethodSecurity annotation on your ApplicationContextConfiguration.

    @Configuration
    @EnableGlobalMethodSecurity(
      prePostEnabled = true, 
      securedEnabled = true, 
      jsr250Enabled = true) 
    public class YourSecurityConfig extends WebSecurityConfigurerAdapter{
    }
  1. prePostEnabled属性可支持Spring的@PreAuthorize和@PostAuthorize注释。 支持意味着,除非将标志设置为true,否则Spring将忽略此注释。secureEnabled属性启用对@安全注解。 支持意味着,除非将标志设置为true,否则Spring将忽略此注释。jsr250Enabled属性启用对@RolesAllowed注解。 支持意味着,除非将标志设置为true,否则Spring将忽略此注释。

What is the difference between @PreAuthorize, @Secured and @RolesAllowed?

@Secured和@RolesAllowed基本相同,尽管@Secured是Spring特定的批注,带有spring-security-core依赖项,而@RolesAllowed是标准化的批注,位于javax.annotation-api依赖项中。 这两个注释都将一个授权/角色字符串作为值。

@ PreAuthorize / @ PostAuthorize也是(较新的)Spring特定注释,并且比上述注释更强大,因为它们不仅可以包含授权/角色,还可以包含任何有效的SpEL表达式。

最后,所有这些注释将引发AccessDeniedException如果您尝试访问权限/角色不足的受保护方法。

因此,让我们最后来看一下这些注解。

    @Service
    public class SomeService {

        @Secured("ROLE_CALLCENTER") 
        // == @RolesAllowed("ADMIN")
        public BankAccountInfo get(...) {

        }

        @PreAuthorize("isAnonymous()") 
        // @PreAuthorize("#contact.name == principal.name")
        // @PreAuthorize("ROLE_ADMIN")
        public void trackVisit(Long id);

        }
    }
  1. As mentioned, @Secured takes an authority/role as parameter. @RolesAllowed, likewise. Note: Remember that @RolesAllowed("ADMIN") will check for a granted authority ROLE_ADMIN.

  2. As mentioned, @PreAuthorize takes in authorities, but also any valid SpEL expression. For a list of common built-in security expressions like isAnonymous() above, as opposed to writing your own SpEL expressions, check out the official documentation.

Which annotation should I use?

这主要是同质性问题,而不是过多地将自己与特定于Spring的API捆绑在一起(通常会提出这种观点)。

如果使用@Secured,请坚持使用它,不要在其他28%的bean中跳到@RolesAllowed注释,以努力标准化,但永远不要完全克服。

首先,您可以始终使用@Secured并在需要时立即切换到@PreAuthorize。

Spring Security & Spring Web MVC

至于与Spring WebMVC的集成,Spring Security允​​许您做几件事:

  1. In addition to antMatchers and regexMatchers, you can also use mvcMatchers. The difference is, that while antMatchers and regexMatchers basically match URI strings with wildcards, mvcMatchers behave exactly like @RequestMappings.

  2. Injection of your currently authenticated principal into a @Controller/@RestController method.

  3. Injection of your current session CSRFToken into a @Controller/@RestController method.

  4. Correct handling of security for async request processing.

    @Controller
    public class MyController {
    @RequestMapping("/messages/inbox")
    public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser, CsrfToken token) {  
    
    // .. find messages for this user and return them ...
    }
    }
  5. 如果对用户进行了身份验证,则@AuthenticationPrincipal将注入主体;如果没有对用户进行身份验证,则将注入null。 该主体是来自UserDetailsS​​ervice / AuthenticationManager的对象!或者,您可以将当前会话CSRFToken注入每个方法中。

如果不使用@AuthenticationPrincipal批注,则必须通过SecurityContextHolder自己获取主体。 在旧版Spring Security应用程序中经常看到的一种技术。

    @Controller
    public class MyController {

        @RequestMapping("/messages/inbox")
        public ModelAndView findMessagesForUser(CsrfToken token) {
             SecurityContext context = SecurityContextHolder.getContext();
             Authentication authentication = context.getAuthentication();

             if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
                 CustomUser customUser = (CustomUser) authentication.getPrincipal();
                 // .. find messages for this user and return them ...
             }

             // todo
        }
    }

Spring Security & Spring Boot

Spring Boot really only pre-configures 小号pring Security for you, whenever you add the spring-boot-starter-security dependency to your Spring Boot project.

除此之外,所有安全性配置都是通过普通的Spring Security概念(例如WebSecurityConfigurerAdapter,身份验证和授权规则)完成的,而这些概念本身与Spring Boot无关。

因此,您在本指南中阅读的所有内容都将1:1应用于将Spring Security与Spring Boot结合使用。 而且,如果您不了解一般的安全性,那就不要期望正确地了解这两种技术如何协同工作。

Spring Security & Thymeleaf

Spring Security与Thymeleaf集成良好。 它提供了特殊的Spring Security Thymeleaf方言,可让您将安全性表达式直接放入Thymeleaf HTML模板中。

    <div sec:authorize="isAuthenticated()">
      This content is only shown to authenticated users.
    </div>
    <div sec:authorize="hasRole('ROLE_ADMIN')">
      This content is only shown to administrators.
    </div>
    <div sec:authorize="hasRole('ROLE_USER')">
      This content is only shown to users.
    </div>

For a full and more detailed overview of how both technologies work together, have a look at the official documentation.

FAQ

What is the latest Spring Security version?

截至2020年4月,即{springsecurityversion}。

请注意,如果您使用的是Spring Boot定义的Spring Security依赖关系,则可能使用的是较旧的Spring Security版本,如5.2.1。

Are older Spring Security versions compatible with the latest version?

Spring Security最近已经发生了相当大的变化。 因此,您需要找到目标版本的迁移指南并逐步进行操作:

What dependencies do I need to add for Spring Security to work?

Plain Spring Project

如果您使用的是普通的Spring项目(不Spring Boot),您需要将以下两个Maven / Gradle依赖项添加到您的项目中:

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>5.2.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>5.2.2.RELEASE</version>
    </dependency>

You’ll also need to configure the SecurityFilterChain in your web.xml or Java config. See how to do it here.

Spring Boot Project

如果您正在使用Spring Boot项目,则需要在项目中添加以下Maven / Gradle依赖项:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

其他所有内容将自动为您配置,您可以立即开始编写WebSecurityConfigurerAdapter。

How do I programmatically access the currently authenticated user in Spring Security?

如本文所述,Spring Security将当前已通过身份验证的用户(或更确切地说是SecurityContext)存储在SecurityContextHolder内部的线程局部变量中。 您可以这样访问它:

    SecurityContext context = SecurityContextHolder.getContext();
    Authentication authentication = context.getAuthentication();
    String username = authentication.getName();
    Object principal = authentication.getPrincipal();
    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

注意,Spring Security默认情况下将设置一个匿名身份验证令牌如果您尚未登录,则作为SecurityContextHolder上的身份验证。这会引起一些混乱,因为人们自然会期望在那里有一个null值。

AntMatchers: Common Examples

一个无意义的示例,显示了最有用的antMatcher(和regexMatcher / mvcMatcher)可能性:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          .authorizeRequests()
          .antMatchers("/api/user/**", "/api/ticket/**", "/index").hasAuthority("ROLE_USER")
          .antMatchers(HttpMethod.POST, "/forms/**").hasAnyRole("ADMIN", "CALLCENTER")
          .antMatchers("/user/**").access("@webSecurity.check(authentication,request)");
    }

How to use a custom login page with Spring Security?

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http
          .authorizeRequests()
              .anyRequest().authenticated()
              .and()
          .formLogin()
              .loginPage("/login") 
              .permitAll();
    }
  1. 您的自定义登录页面的URL。 指定此选项后,自动生成的登录页面将消失。

How to do a programmatic login with Spring Security?

    UserDetails principal = userDetailsService.loadUserByUsername(username);
    Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authentication);

How to disable CSRF just for certain paths?

    @Override
        protected void configure(HttpSecurity http) throws Exception {
          http
           .csrf().ignoringAntMatchers("/api/**");
        }

Fin

如果到目前为止,您已经了解了Spring Security生态系统的复杂性,即使没有OAuth2也是如此。 总结一下:

  1. 如果您对Spring Security Filter Chain的工作原理及其默认的漏洞利用防护有基本的了解,将对您有所帮助(请考虑:CSRF)。确保了解身份验证和授权之间的区别。 另外,还需要为特定的身份验证工作流程指定@Beans。确保您了解Spring Security的WebSecurityConfigurerAdapter的DSL以及基于注释的方法安全性。最后但并非最不重要的一点是,它有助于仔细检查Spring Security与其他框架和库(例如Spring MVC或Thymeleaf)的集成。

今天足够了,因为那是一个很大的旅程,不是吗? 谢谢阅读!

Acknowledgments

A big "thank you" goes out to Patricio "Pato" Moschcovich, who not only did the proofreading for this article but also provided invaluable feedback!

More

You might also be interested in my newly announced
Spring Security exercise course, which will teach Spring Security & OAuth2 in a rather unique way.

from: https://dev.to//marcobehler/spring-security-authentication-and-authorization-in-depth-m9g

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章