5. Java Configuration

在 Spring 3.1 中,对Java Configuration的常规支持已添加到 Spring Framework 中。从 Spring Security 3.2 开始,Spring Security Java Configuration 支持使用户可以轻松配置 Spring Security,而无需使用任何 XML。

如果您熟悉第 6 章,安全命名空间配置,则应该在第 6 章,安全命名空间配置和 Security Java Configuration 支持之间找到很多相似之处。

Note

Spring Security 提供了许多示例应用程序,它们演示了 Spring Security Java 配置的使用。

5.1 Hello Web Security Java 配置

第一步是创建我们的 Spring Security Java 配置。该配置将创建一个称为springSecurityFilterChain的 Servlet 过滤器,该过滤器负责应用程序中的所有安全性(保护应用程序 URL,验证提交的用户名和密码,重定向到登录表单等)。您可以在下面找到 Spring Security Java 配置的最基本示例:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Bean
	public UserDetailsService userDetailsService() throws Exception {
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(User.withUsername("user").password("password").roles("USER").build());
		return manager;
	}
}

此配置的确没有太多,但是它做了很多。您可以找到以下功能的摘要:

5.1.1 AbstractSecurityWebApplicationInitializer

下一步是向 War 注册springSecurityFilterChain。可以在 Servlet 3.0 环境中使用Spring 的 WebApplicationInitializer 支持在 Java 配置中完成此操作。毫不奇怪,Spring Security 提供了一个 Base ClassAbstractSecurityWebApplicationInitializer,它将确保springSecurityFilterChain为您注册。使用AbstractSecurityWebApplicationInitializer的方式因我们是否已经在使用 Spring 或 Spring Security 是应用程序中唯一的 Spring 组件而异。

5.1.2 不存在 Spring 的 AbstractSecurityWebApplicationInitializer

如果您不使用 Spring 或 Spring MVC,则需要将WebSecurityConfig传递到超类中,以确保配置被接受。您可以在下面找到一个示例:

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

	public SecurityWebApplicationInitializer() {
		super(WebSecurityConfig.class);
	}
}

SecurityWebApplicationInitializer将执行以下操作:

  • 为应用程序中的每个 URL 自动注册 springSecurityFilterChain 过滤器

  • 添加一个加载WebSecurityConfig的 ContextLoaderListener。

5.1.3 使用 Spring MVC 的 AbstractSecurityWebApplicationInitializer

如果我们在应用程序的其他地方使用 Spring,则可能已经有一个WebApplicationInitializer用来加载 Spring 配置。如果我们使用以前的配置,将会得到一个错误。相反,我们应该使用现有的ApplicationContext注册 Spring Security。例如,如果我们使用 Spring MVC,则SecurityWebApplicationInitializer如下所示:

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

}

这只会为应用程序中的每个 URL 仅注册 springSecurityFilterChain 过滤器。之后,我们将确保WebSecurityConfig已加载到我们现有的 ApplicationInitializer 中。例如,如果我们使用的是 Spring MVC,则将其添加到getRootConfigClasses()

public class MvcWebApplicationInitializer extends
		AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getRootConfigClasses() {
		return new Class[] { WebSecurityConfig.class };
	}

	// ... other overrides ...
}

5.2 HttpSecurity

到目前为止,我们的WebSecurityConfig仅包含有关如何验证用户身份的信息。 Spring Security 如何知道我们要要求所有用户进行身份验证? Spring Security 如何知道我们要支持基于表单的身份验证?原因是WebSecurityConfigurerAdapterconfigure(HttpSecurity http)方法中提供了默认配置,如下所示:

protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.and()
		.formLogin()
			.and()
		.httpBasic();
}

上面的默认配置:

  • 确保对我们应用程序的任何请求都需要对用户进行身份验证

  • 允许用户使用基于表单的登录进行身份验证

  • 允许用户使用 HTTP Basic 身份验证进行身份验证

您会注意到该配置与 XML 命名空间配置非常相似:

<http>
	<intercept-url pattern="/**" access="authenticated"/>
	<form-login />
	<http-basic />
</http>

使用and()方法表示等效于关闭 XML 标记的 Java 配置,该方法允许我们 continue 配置父级。如果您阅读该代码,这也很有意义。我要配置授权请求,并配置表单登录,并配置 HTTP 基本身份验证。

但是,Java 配置具有不同的默认 URL 和参数。创建自定义登录页面时,请记住这一点。结果是我们的 URL 更 RESTful。另外,使用 Spring Security 可以防止information leaks并不是很明显。例如:

5.3 Java 配置和表单登录

您可能想知道提示您登录时登录表单的来源,因为我们没有提及任何 HTML 文件或 JSP。由于 Spring Security 的默认配置没有显式设置登录页面的 URL,因此 Spring Security 将基于已启用的功能并使用处理提交的登录的 URL 的标准值(用户将使用的默认目标 URL)自动生成一个 URL。登录后发送至。

尽管自动生成的登录页面便于快速启动和运行,但是大多数应用程序都希望提供自己的登录页面。为此,我们可以如下所示更新配置:

protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.and()
		.formLogin()
			.loginPage("/login") (1)
			.permitAll();        (2)
}
  • (1) 更新后的配置指定登录页面的位置。
  • (2) 我们必须授予所有用户(即未经身份验证的用户)访问我们登录页面的权限。 formLogin().permitAll()方法允许向所有用户授予与基于表单的登录相关的所有 URL 的访问权限。

下面是我们当前配置中使用 JSP 实现的示例登录页面:

Note

下面的登录页面代表我们当前的配置。如果某些默认设置不能满足我们的需求,我们可以轻松地更新配置。

<c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post">       (1)
	<c:if test="${param.error != null}">        (2)
		<p>
			Invalid username and password.
		</p>
	</c:if>
	<c:if test="${param.logout != null}">       (3)
		<p>
			You have been logged out.
		</p>
	</c:if>
	<p>
		<label for="username">Username</label>
		<input type="text" id="username" name="username"/>	(4)
	</p>
	<p>
		<label for="password">Password</label>
		<input type="password" id="password" name="password"/>	(5)
	</p>
	<input type="hidden"                        (6)
		name="${_csrf.parameterName}"
		value="${_csrf.token}"/>
	<button type="submit" class="btn">Log in</button>
</form>
  • (1)/login URL 的 POST 将尝试验证用户身份
  • (2) 如果查询参数error存在,则尝试认证失败
  • (3) 如果查询参数logout存在,则表明用户已成功注销
  • (4) 用户名必须作为名为* username *的 HTTP 参数存在
  • (5) 密码必须作为名为* password *的 HTTP 参数存在
  • (6) 我们必须第 18.4.3 节“包括 CSRF 令牌”要了解更多信息,请阅读参考的第 18 章,跨站点请求伪造(CSRF)部分

5.4 授权请求

我们的示例仅要求对用户进行身份验证,并且对应用程序中的每个 URL 都进行了身份验证。我们可以通过将多个子级添加到http.authorizeRequests()方法中来为 URL 指定自定义要求。例如:

protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()                                                                (1)
			.antMatchers("/resources/**", "/signup", "/about").permitAll()                  (2)
			.antMatchers("/admin/**").hasRole("ADMIN")                                      (3)
			.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")            (4)
			.anyRequest().authenticated()                                                   (5)
			.and()
		// ...
		.formLogin();
}
  • (1) http.authorizeRequests()方法有多个子级,每个匹配器均按声明 Sequences 考虑。
  • (2) 我们指定了任何用户都可以访问的多个 URL 模式。具体来说,如果 URL 以“/resources /”开头,等于“/signup”或等于“/about”,则任何用户都可以访问请求。
  • (3) 任何以“/admin /”开头的 URL 都将限于角色为“ ROLE_ADMIN”的用户。您将注意到,由于我们正在调用hasRole方法,因此无需指定“ ROLE_”前缀。
  • (4) 任何以“/db /”开头的 URL 都要求用户同时具有“ ROLE_ADMIN”和“ ROLE_DBA”。您会注意到,由于我们使用的是hasRole表达式,因此无需指定“ ROLE_”前缀。
  • (5) 尚未匹配的任何 URL 仅要求对用户进行身份验证

5.5 处理注销

使用WebSecurityConfigurerAdapter时,将自动应用注销功能。默认设置是访问 URL /logout将通过以下方式注销用户:

  • 使 HTTP 会话无效

  • 清理配置的所有 RememberMe 身份验证

  • 清除SecurityContextHolder

  • 重定向到/login?logout

但是,与配置登录功能相似,您还可以使用各种选项来进一步自定义注销要求:

protected void configure(HttpSecurity http) throws Exception {
	http
		.logout()                                                                (1)
			.logoutUrl("/my/logout")                                                 (2)
			.logoutSuccessUrl("/my/index")                                           (3)
			.logoutSuccessHandler(logoutSuccessHandler)                              (4)
			.invalidateHttpSession(true)                                             (5)
			.addLogoutHandler(logoutHandler)                                         (6)
			.deleteCookies(cookieNamesToClear)                                       (7)
			.and()
		...
}
  • (1) 提供注销支持。使用WebSecurityConfigurerAdapter时将自动应用。
  • (2) 触发注销的 URL 发生(默认为/logout)。如果启用了 CSRF 保护(默认),则请求也必须是 POST。有关更多信息,请咨询JavaDoc
  • (3) 发生注销后重定向到的 URL。默认值为/login?logout。有关更多信息,请咨询JavaDoc
  • (4) 让我们指定一个自定义LogoutSuccessHandler。如果指定此选项,则logoutSuccessUrl()将被忽略。有关更多信息,请咨询JavaDoc
  • (5) 指定注销时是否使HttpSession无效。默认情况下,这是“ true”。在幕后配置SecurityContextLogoutHandler。有关更多信息,请咨询JavaDoc
  • (6) 添加LogoutHandler。默认情况下,将SecurityContextLogoutHandler添加为最后的LogoutHandler
  • (7) 允许指定成功注销后将删除的 cookie 名称。这是显式添加CookieClearingLogoutHandler的快捷方式。

Note

当然,也可以使用 XML 命名空间符号配置注销。请参阅 Spring Security XML 命名空间部分中logout element的文档以获取更多详细信息。

通常,为了自定义注销功能,您可以添加LogoutHandler和/或LogoutSuccessHandler实现。对于许多常见方案,使用流畅的 API 时会在后台应用这些处理程序。

5.5.1 LogoutHandler

通常,LogoutHandler实现表示能够参与注销处理的类。期望它们被调用以执行必要的清理。因此,它们不应引发异常。提供了各种实现:

有关详情,请参见第 17.4 节“记住我的接口和实现”

除了直接提供LogoutHandler实现之外,fluent API 还提供了快捷方式,这些快捷方式在幕后提供了各自的LogoutHandler实现。例如。 deleteCookies()允许指定注销成功后要删除的一个或多个 cookie 的名称。与添加CookieClearingLogoutHandler相比,这是捷径。

5.5.2 LogoutSuccessHandler

LogoutFilter成功注销后调用LogoutSuccessHandler,以处理例如重定向或转发到适当的目的地。请注意,该接口与LogoutHandler几乎相同,但可能会引发异常。

提供以下实现:

如上所述,您无需直接指定SimpleUrlLogoutSuccessHandler。相反,Fluent 的 API 通过设置logoutSuccessUrl()提供了快捷方式。这将在底下设置SimpleUrlLogoutSuccessHandler。提供的 URL 将在注销后重定向到。默认值为/login?logout

HttpStatusReturningLogoutSuccessHandler在 REST API 类型的场景中可能会很有趣。通过LogoutSuccessHandler,您可以提供要返回的纯 HTTP 状态代码,而不是在成功注销后重定向到 URL。如果未配置,默认情况下将返回状态码 200.

5.5.3 其他与注销有关的参考

5.6 Authentication

到目前为止,我们只看了最基本的身份验证配置。让我们看一下配置身份验证的一些高级选项。

5.6.1 内存中身份验证

我们已经看到了为单个用户配置内存中身份验证的示例。下面是配置多个用户的示例:

@Bean
public UserDetailsService userDetailsService() throws Exception {
	InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
	manager.createUser(User.withUsername("user").password("password").roles("USER").build());
	manager.createUser(User.withUsername("admin").password("password").roles("USER","ADMIN").build());
	return manager;
}

5.6.2 JDBC 身份验证

您可以找到更新以支持基于 JDBC 的身份验证。下面的示例假定您已经在应用程序中定义了DataSourcejdbc-javaconfig示例提供了使用基于 JDBC 的身份验证的完整示例。

@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	auth
		.jdbcAuthentication()
			.dataSource(dataSource)
			.withDefaultSchema()
			.withUser("user").password("password").roles("USER").and()
			.withUser("admin").password("password").roles("USER", "ADMIN");
}

5.6.3 LDAP 认证

您可以找到更新以支持基于 LDAP 的身份验证。 ldap-javaconfig示例提供了使用基于 LDAP 的身份验证的完整示例。

@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	auth
		.ldapAuthentication()
			.userDnPatterns("uid={0},ou=people")
			.groupSearchBase("ou=groups");
}

上面的示例使用以下 LDIF 和嵌入式 Apache DS LDAP 实例。

users.ldif.

dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password

dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password

dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org

dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org

5.6.4 AuthenticationProvider

您可以通过将自定义AuthenticationProvider公开为 bean 来定义自定义身份验证。例如,以下假设SpringAuthenticationProvider实现AuthenticationProvider,将自定义身份验证:

Note

仅在未填充AuthenticationManagerBuilder时使用

@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {
	return new SpringAuthenticationProvider();
}

5.6.5 UserDetailsService

您可以通过将自定义UserDetailsService公开为 bean 来定义自定义身份验证。例如,以下假设SpringDataUserDetailsService实现UserDetailsService,将自定义身份验证:

Note

仅当尚未填充AuthenticationManagerBuilder且未定义AuthenticationProviderBean时才使用此选项。

@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {
	return new SpringDataUserDetailsService();
}

您还可以通过将PasswordEncoder公开为 bean 来定制密码的 encodings。例如,如果使用 bcrypt,则可以添加如下所示的 bean 定义:

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

5.6.6 LDAP 验证

5.7 多个 HttpSecurity

我们可以配置多个 HttpSecurity 实例,就像我们可以拥有多个<http>块一样。关键是要多次扩展WebSecurityConfigurerAdapter。例如,以下是对以/api/开头的 URL 具有不同配置的示例。

@EnableWebSecurity
public class MultiHttpSecurityConfig {
	@Bean
	public UserDetailsService userDetailsService() throws Exception {
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(User.withUsername("user").password("password").roles("USER").build());
		manager.createUser(User.withUsername("admin").password("password").roles("USER","ADMIN").build());
		return manager;
	}

	@Configuration
	@Order(1)                                                        (1)
	public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
		protected void configure(HttpSecurity http) throws Exception {
			http
				.antMatcher("/api/**")                               (2)
				.authorizeRequests()
					.anyRequest().hasRole("ADMIN")
					.and()
				.httpBasic();
		}
	}

	@Configuration                                                   (3)
	public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.authorizeRequests()
					.anyRequest().authenticated()
					.and()
				.formLogin();
		}
	}
}
  • 正常配置身份验证
  • (1) 创建一个包含@OrderWebSecurityConfigurerAdapter实例,以指定应首先考虑哪个WebSecurityConfigurerAdapter
  • (2) http.antMatcher声明此HttpSecurity仅适用于以/api/开头的 URL
  • (3) 创建WebSecurityConfigurerAdapter的另一个实例。如果网址不是以/api/开头,则将使用此配置。在ApiWebSecurityConfigurationAdapter之后考虑此配置,因为它在1之后具有@Order值(没有@Order默认为最后)。

5.8 方法安全性

从 2.0 版开始,Spring Security 大大改善了对为服务层方法增加安全性的支持。它提供对 JSR-250Comments 安全性以及框架原始@SecuredComments 的支持。从 3.0 开始,您还可以使用新的expression-based annotations。您可以使用intercept-methods元素修饰 bean 声明,从而对单个 bean 应用安全性,或者可以使用 AspectJ 样式切入点在整个服务层中保护多个 bean。

5.8.1 EnableGlobalMethodSecurity

我们可以在任何@Configuration实例上使用@EnableGlobalMethodSecurityComments 启用基于 Comments 的安全性。例如,以下将启用 Spring Security 的@SecuredComments。

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}

向方法(在类或接口上)添加 Comments 将相应地限制对该方法的访问。 Spring Security 的本机 Comments 支持为该方法定义了一组属性。这些将被传递给 AccessDecisionManager 使其做出实际决定:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

可以使用以下命令启用对 JSR-250 注解的支持

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}

这些是基于标准的,并允许应用基于角色的简单约束,但是没有 Spring Security 的本机 Comments 的强大功能。要使用新的基于表达式的语法,您可以使用

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}

而等效的 Java 代码将是

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

5.8.2 GlobalMethodSecurityConfiguration

有时,您可能需要执行比@EnableGlobalMethodSecurityComments 允许所允许的操作更为复杂的操作。对于这些实例,您可以扩展GlobalMethodSecurityConfiguration,以确保@EnableGlobalMethodSecurityComments 出现在子类中。例如,如果要提供自定义MethodSecurityExpressionHandler,则可以使用以下配置:

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
	@Override
	protected MethodSecurityExpressionHandler createExpressionHandler() {
		// ... create and return custom MethodSecurityExpressionHandler ...
		return expressionHandler;
	}
}

有关可以覆盖的方法的其他信息,请参阅GlobalMethodSecurityConfiguration Javadoc。

5.9 后处理已配置的对象

Spring Security 的 Java 配置并未公开其配置的每个对象的每个属性。这简化了大多数用户的配置。毕竟,如果每个属性都公开,则用户可以使用标准 Bean 配置。

尽管有充分的理由不直接公开每个属性,但用户可能仍需要更多高级配置选项。为了解决这个问题,Spring Security 引入了ObjectPostProcessor的概念,该概念可用于修改或替换 Java 配置创建的许多 Object 实例。例如,如果要在FilterSecurityInterceptor上配置filterSecurityPublishAuthorizationSuccess属性,则可以使用以下命令:

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
				public <O extends FilterSecurityInterceptor> O postProcess(
						O fsi) {
					fsi.setPublishAuthorizationSuccess(true);
					return fsi;
				}
			});
}

5.10 自定义 DSL

您可以在 Spring Security 中提供自己的自定义 DSL。例如,您可能会有类似以下内容的内容:

public class MyCustomDsl extends AbstractHttpConfigurer<CorsConfigurerMyCustomDsl, HttpSecurity> {
	private boolean flag;

	@Override
	public void init(H http) throws Exception {
		// any method that adds another configurer
		// must be done in the init method
		http.csrf().disable();
	}

	@Override
	public void configure(H http) throws Exception {
		ApplicationContext context = http.getSharedObject(ApplicationContext.class);

		// here we lookup from the ApplicationContext. You can also just create a new instance.
		MyFilter myFilter = context.getBean(MyFilter.class);
		myFilter.setFlag(flag);
		http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
	}

	public MyCustomDsl flag(boolean value) {
		this.flag = value;
		return this;
	}

	public static MyCustomDsl customDsl() {
		return new MyCustomDsl();
	}
}

Note

实际上就是实现HttpSecurity.authorizeRequests()之类的方法的方式。

然后可以像下面这样使用自定义 DSL:

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.apply(customDsl())
				.flag(true)
				.and()
			...;
	}
}

该代码按以下 Sequences 调用:

  • 调用``的 configure 方法中的代码

  • MyCustomDsl 的 init 方法中的代码被调用

  • MyCustomDsl 的 configure 方法中的代码被调用

如果需要,可以使用SpringFactories默认情况下WebSecurityConfiguerAdapter添加MyCustomDsl。例如,您将在名为META-INF/spring.factories的 Classpath 上创建具有以下内容的资源:

META-INF/spring.factories.

org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl

希望禁用默认设置的用户可以明确地这样做。

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.apply(customDsl()).disable()
			...;
	}
}