# Spring Security ## Spring Security简介 Spring Security作为一个安全框架,向使用者提供了用户认证、授权、常规攻击保护等功能。 ## Spring Security自动配置 默认情况下,在引入Spring Security的启动器依赖之后,Spring Boot自动配置会做如下工作: - 启用Spring Security的默认配置,创建一个的bean对象,bean对象名称为“springSecurityFilterChain”,bean对象类型为SecurityFilterChain,实现了Filter。该bean对象为负责应用中所有与安全相关的操作(例如验证提交的username和password,保护应用的url等) - 创建一个UserDetailsService的bean对象,并且产生一个为”user“的username和一个随机产生的password,随机产生的password会输出在console日志中 - 将名为springSecurityFilterChain的bean对象注册到servlet容器中,用来对每个servlet请求进行处理 > Spring Security会通过Spring Security会通过BCtypt(Hash解密算法)来对密码的存储进行保护 ## Spring Security结构 ### DelegatingFilterProxy Spring提供了Filter的一个实现类DelegatingFilterProxy,其将Servlet容器的生命周期和Spring的ApplicationContext连接在一起。 ***DelegatingFilterProxy会通过标准的servlet容器机制被注册,但是将所有工作都委托给Spring容器中实现了Filter的bean对象***。 > DelegatingFilterProxy会在ApplicationContext中查找实现了Filter的bean对象,并且调用该bean对象的doFilter方法 > ```java > public void doFilter(ServletRequest request, > ServletResponse response, FilterChain chain) { > // Lazily get Filter that was registered as a Spring Bean > // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0 > Filter delegate = getFilterBean(someBeanName); > // delegate work to the Spring Bean > delegate.doFilter(request, response); > } > ``` ### FilterChainProxy Spring Security支持FilterChainProxy,FilterChainProxy是一个由Spring Security提供的特殊Filter,FilterChainProxy通过SecurityFilterChain允许向多个Filters委托工作。 ***FilterChainProxy是一个bean对象,通过被包含在DelegatingFilterProxy中。*** ### SecurityFilterChain SecurityFilterChain通常被FilterChainProxy使用,用来决定在该次请求中调用那些Spring Security Filters。 ### SecurityFilters Security Filters通过SecurityFilterChain API被插入到FilterChainProxy中。 ### 处理Security异常 ExceptionTranslationFilter将认证异常和权限异常翻译为http response。 > ExceptionTranslationFilter被插入到FilterChainProxy中,作为SecurityFilterChain中的一个。 > 如果应用程序没有抛出AccessDeniedException或AuthenticationException,那么ExceptionTranslationFilter并不会做任何事情。 ```java // ExceptionTranslationFilter的伪代码 try { filterChain.doFilter(request, response); } catch (AccessDeniedException | AuthenticationException ex) { if (!authenticated || ex instanceof AuthenticationException) { startAuthentication(); } else { accessDenied(); } } ``` ## Spring Security身份认证的结构 ### SecurityContextHolder Spring Security身份认证的核心模型,SecurityContextHolder包含有SecurityContext。 > Spring Security将被认证用户的详细信息(details)存储在SecurityContextHolder中。Spring Security不在乎该SecurityContextHolder是如何被填充的,只要该SecurityContextHolder有值,那么其将会被用作当前已经被认证的用户。 > ***将用户标识为已认证的最简单的方法是为该用户设置SecurityContextHolder。*** ```java // 设置SecurityContextHolder SecurityContext context = SecurityContextHolder.createEmptyContext(); Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER"); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); ``` 默认情况下,SecurityContextHolder通过ThreadLocal来存储SecurityContext,故而SecurityContext对于位于同一线程之下的方法来说都可以访问。 > 使用ThreadLocal来存储SecurityContext是相当安全的,如果想要在该已认证主体的请求被处理完成之后清除SecurityContext,Spring Security中的FilterChainProxy会保证该SecurityContext被清除。 ### SecurityContext SecurityContext从SecurityContextHolder中获得,SecurityContext中含有Authentication对象。 ### Authentication Authentication在Spring Security中具有两个目的: - 作为AuthenticationManager的输入,用于提供待认证用户的认证凭证。当用于该场景下时,isAuthenticated()方法返回值应该为false - 代表当前已经认证过的用户。当前的Authentication可以从SecurityContext中获取,而默认情况下SecurityContext是存储在ThreadLocal中的 Authentication含有如下部分: - 主体(principal):用于标识用户,当通过username/password进行认证时,其通常是UserDetails类的实例 - 凭据(credentials):通常是password,在许多场景下凭据会在用户认证成功之后被清空,为了保证凭据不会被泄露 - 权限(authorities):该GrantedAuthority集合是用户被授予的高层次许可。许可通常是用户角色或者作用域范围。 ### GrantedAuthority GrantedAuthority是用户被授予的高层次许可,譬如用户角色或者作用域范围。 GrantedAuthority可以通过Authentication.getAuthorities()方法来获得,该方法会返回一个GrantedAuthentication的集合。每个GrantedAuthentication都是一项被授予该用户的权限。 ### AuthenticationManager AuthenticationManager的API定义了Security Filters如何来执行身份认证。对于身份认证返回的Authentication,会被调用AuthenticationManager的controller设置到SecurityContextHolder中。 > AuthenticationManager的实现可以是任何类,但是最通用的实现仍然是ProviderManager ### ProviderManager ProviderManager是AuthenticationManager的最通用实现。ProviderManager将工作委托给一系列AuthenticationProvider。 > 对于每个ProviderManager,都可以决定将该认证标识为成功、失败,或者将认证工作委托给下游AuthenticationProvider。 > 如果所有的AuthenticationProvider都没有将该认证标识为成功或者失败,那么整个认证流程失败,并且抛出ProviderNotFoundException异常。 > ProviderNotFoundException是一个特殊的AuthenticationException,该异常代表对传入Authentication的类型并没有配置该类型的ProviderManager > 在实践中,每个AuthenticationProvider知道如何处理一种特定类型的Authentication 默认情况下,ProviderManager在认证请求成功后会尝试清除返回的Authentication对象中任何敏感的凭据信息,这将会保证password等敏感信息保存时间尽可能地短,减少泄露的风险。 ### AuthenticationProvider 复数个AuthenticationProvider可以被注入到ProviderManager中,每个AuthenticationProvider可以负责一种专门类型的认证,例如DaoAuthenticationProvider负责username/password认证,JwtAuthenticationProvider负责jwt token的认证。 ### AuthenticationEntryPoint AuthenticationEntryPoint用来发送一个Http Response,用来向客户端请求认证凭据。 某些情况下,客户端在请求资源时会主动在请求中包含凭据,如username/password等。在这种情况下,服务端并不需要再专门发送Http Response来向客户端请求认证凭据。 > AuthenticationEntryPoint用来向客户端请求认证凭据,AuthenticationEntryPoint的实现可能会执行一个从定向操作,将请求重定向到一个登录页面用于获取凭据,并且返回一个WWW-Authentication Header。 ### AbstractAuthenticationProcessingFilter 该类作为base filter用来对用户的凭据进行认证。再凭据被认证之前,Spring Security通常会通过AuthenticationEntryPoint向客户端请求认证凭据。 之后,AbstractAuthenticationProcessingFilter会对提交的任何认证请求进行认证。 #### 认证流程 1. 当用户提交认证凭据之后,AbstractAuthenticationProcesingFilter会根据HttpServletRequest对象创建一个Authentication对象用于认证,该Authentication的类型取决于AbstractAuthenticationProcessingFilter的子类类型。例如,UsernamePasswordAuthenticationFilter会通过提交request中的username和password创建UsernamePasswordAuthenticationToken。 2. 然后将构建产生的Authentication对象传入到AuthenticationManager中,进行认证 3. 如果认证失败,那么会失败,SecurityContextHolder会被清空,RememberMeService.logFail方法将会被调用,AuthenticationFailureHandler也会被调用 4. 如果认证成功,那么SessionAuthenticationStrategy将会收到登录的通知 5. 该Authentication对象再认证成功之后将会被设置到SecurityContextHolder中 6. RemeberMeService.logSuccess将会被调用 7. ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent. 8. AuthenticationSuccessHandler被调用 ## 用户名/密码认证 ### Form Login Spring Security为以表单形式提供的用户名和密码认证提供支持。 #### 用户被重定向到登录页面的过程 1. 用户发送了一个没有经过身份认证的请求到指定资源,并且待请求的资源对该用户来说是未授权的 2. Spring Security中FilterSecurityInterceptor抛出AccessDeniedException,代表该未授权的请求被拒绝 3. 因为该用户没有经过认证,故而ExceptionTransactionFilter发起了开始认证的过程,并且使用配置好的AuthenticationEntryPoint向登录页面发起了重定向。在大多数情况下AuthenticationEntryPoint都是LoginUrlAuthenticationEntryPoint 4. 浏览器接下来会请求重定向到的登陆页面 当用户名和密码提交后,UsernamePasswordAuthenticationFilter会对username和password进行认证。UsernamePasswordAuthenticationFilter继承了AbstractAuthenticationProcessingFilter。 #### 认证用户名和密码过程 1. 当用户提交了其用户名和密码之后,UsernamePasswordAuthenticationFilter会创建一个UsernamePasswordAuthenticationToken 2. 创建的UsernamePasswordAuthenticationToken会传入AuthenticationManager中进行认证 3. 如果认证失败,那么SecurityContextHolder会被清除,RememberMeService.logFailure和AuthenticationFailureHandler会被调用 4. 如果认证成功,那么SessionAuthenticationStrategy将会收到登录的通知,RemeberMeService.logSuccess和AuthenticationSuccessHandler会被调用,ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent Spring Security Form Login默认情况下是开启的,但是,一旦任何基于servlet的配置被提供,那么基于表单的login也必须要显式指定。 ```java // 显式指定form login的配置 @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http .formLogin(withDefaults()); // ... } ``` 如果想要自定义login form page,可以使用如下配置 ```java public SecurityFilterChain filterChain(HttpSecurity http) { http .formLogin(form -> form .loginPage("/login") .permitAll() ); // ... } ``` ### 基本Authentication #### 基本Authentication的认证流程 1. 用户向私有资源发送未认证请求,其中对私有资源的访问并没有被授权 2. SpringSecurity的FilterSecurityInterceptor表明该未认证的请求被拒绝访问,抛出AccessDeniedException 3. 由于该请求没有经过身份认证,故而ExceptionTranslationFilter启动身份认证,被配置好的AuthenticationEntryPoint是一个BasicAuthenticationEntryPoint类的实例,该实例会发送WWW-Authentication的header。RequestCache通常是一个NullRequestCache,不会保存任何的http request请求,因为客户端能够重新发送其原来发送过的请求。 4. 当客户端获取到WWW-Authentication的header,客户端会知道其接下来会通过username和password重新尝试,重新发送http请求。 默认情况下,basic authentication是被开启的。但是,如果有任何基于基于servlet的配置被提供,那么必须通过如下方式显式开启basic authentication。 ```java @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http // ... .httpBasic(withDefaults()); return http.build(); } ``` ### Digest Authentication(摘要认证,***不安全***) 在目前,不应该在现代应用程序中使用Digest Authentication,因为使用摘要认证时必须将password通过纯文本、加密或MD5的格式存储(MD5已经被证实不安全)。相对的,应该使用单向的密码散列(如bCrypt, PBKDF2, SCrypt)来存储认证凭证,但是这些都不被Digest Authentication所支持。 > 摘要认证主要用来解决Basic Authentication中存在的问题,摘要认证确保了认证凭证在网络上不会以明文的方式传输。 > 如果想要使用非https的方式并且最大限度的加强认证过程,那么可以考虑使用Digest Authentication。 #### 摘要认证中的随机数 摘要认证中的核心是随机数,该随机数的值由服务端产生,Spring Security中随机数次啊用如下格式: ```txt base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) expirationTime: The date and time when the nonce expires, expressed in milliseconds key: A private key to prevent modification of the nonce token ``` 需要为存储不安全的密码文本配置使用NoOpPasswordEncoder。可以通过如下方式来配置Digest Authentication。 ```java @Autowired UserDetailsService userDetailsService; DigestAuthenticationEntryPoint entryPoint() { DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint(); result.setRealmName("My App Relam"); result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92"); } DigestAuthenticationFilter digestAuthenticationFilter() { DigestAuthenticationFilter result = new DigestAuthenticationFilter(); result.setUserDetailsService(userDetailsService); result.setAuthenticationEntryPoint(entryPoint()); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint())) .addFilterBefore(digestFilter()); return http.build(); } ``` ## 密码存储方式 ### 内存中存储密码 Spring Security中InMemoryUserDetailsManager实现了UserDetailsService,用于向基于存储在内存中的密码认证提供支持。 InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。基于UserDetails的认证主要用来接受基于用户名/密码的认证。 InMemoryUserDetailsManager可以通过如下方式进行配置: ```java @Bean public UserDetailsService users() { UserDetails user = User.builder() .username("user") .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") .roles("USER", "ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); } ``` #### 内存中存储密码时使用defaultPasswordEncoder ***通过defaultPasswordEncoder来指定密码编码器时,无法防止通过反编译字节码来获取密码的攻击。*** ```java @Bean public UserDetailsService users() { // The builder will ensure the passwords are encoded before saving in memory UserBuilder users = User.withDefaultPasswordEncoder(); UserDetails user = users .username("user") .password("password") .roles("USER") .build(); UserDetails admin = users .username("admin") .password("password") .roles("USER", "ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); } ``` ### JDBC Authentication Spring Security的JdbcDaoImpl实现了UserDetailsService来基于username/password的认证提供从jdbc获取密码的支持。JdbcUserDetailsManager继承了JdbcDaoImpl来通过DetailsManager的接口提供对UserDetails的管理。 Spring Security为基于jdbc的认证提供了默认的查询语句。 #### User Schema JdbcDaoImpl需要数据表来导入密码、账户状态和用户的一系列权限。JdbcDaoImpl默认需要的schema如下: ```sql # 创建用户表和权限表,并且将用户表和权限表之间用外键关联 # 用户表需要提供username、password、用户状态 # 权限表需要提供用户名和权限名称 create table users( username varchar_ignorecase(50) not null primary key, password varchar_ignorecase(500) not null, enabled boolean not null ); create table authorities ( username varchar_ignorecase(50) not null, authority varchar_ignorecase(50) not null, constraint fk_authorities_users foreign key(username) references users(username) ); create unique index ix_auth_username on authorities (username,authority); ``` #### Group Schema 如果你的程序中使用了Group,那么还额外需要一张group的表,默认如下: ```sql # 如果要为group配置权限,需要引入三张表,group表,权限表和group_member表 create table groups ( id bigint auto_increment primary key, group_name varchar_ignorecase(50) not null ); create table group_authorities ( group_id bigint not null, authority varchar(50) not null, constraint fk_group_authorities_group foreign key(group_id) references groups(id) ); create table group_members ( id bigint auto_increment primary key, username varchar(50) not null, group_id bigint not null, constraint fk_group_members_group foreign key(group_id) references groups(id) ); ``` #### 配置Datasource ```java // 生产环境时,应该通过对外部数据库的连接来建立数据源 @Bean DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(H2) .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION) .build(); } ``` #### 创建JdbcUserDetailsManager Bean对象 ```java @Bean UserDetailsManager users(DataSource dataSource) { UserDetails user = User.builder() .username("user") .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") .roles("USER", "ADMIN") .build(); JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource); users.createUser(user); users.createUser(admin); return users; } ``` ### UserDetails UserDetails是通过UserDetailsService返回的。DaoAuthenticationProvider对UserrDetails进行验证并且返回