Статьи

Интеграция Spring Security с базой данных

Вступление

Работа в корпоративной среде часто накладывает некоторые политики, которые могут ограничивать ваш выбор технологии и даже вашу свободу применять лучшие практики в вашем коде. В результате вы сталкиваетесь с многочисленными ограничениями, которые могут заставить вас использовать плохие методы. Одним из таких ограничений, с которым я недавно столкнулся, является запрещенное использование пула соединений с базой данных. Это было связано с тем, что пользователи должны использовать свои собственные учетные записи для доступа к базе данных в целях безопасности и аудита. Поэтому многие из систем, которые я видел, использовали прямое управление соединениями с базой данных, что является известной плохой практикой в ​​разработке программного обеспечения. Это ограничение создало ряд проблем при разработке собственных веб-приложений, некоторые из которых:

  • Хранение соединений с базой данных в объектах сеанса HTTP.

  • Некоторые разработчики продолжали открывать несколько соединений для одного и того же пользователя, даже если в объекте сеанса HTTP было доступно активное соединение с базой данных.

  • Соединения с базой данных остаются бездействующими в сеансе HTTP в течение длительного периода времени, особенно если пользователь не вышел из системы или разработчик неправильно внедрил аннулирование сеанса.

  • Большой объем памяти на сервере из-за увеличенного размера объектов сеанса http.

  • Требовалась дополнительная работа, чтобы сделать объекты сеанса HTTP масштабируемыми для других серверов из-за необходимости тщательно сериализовать / десериализовать соединения с базой данных.

Чтобы обойти это ограничение, избежать этих проблем и придерживаться четкой схемы, вы узнаете, как использовать среду Spring Security для облегчения управления подключением к базе данных с помощью служб аутентификации. Кроме того, вы узнаете, как использовать инфраструктуру MyBatis для использования учетных данных пользователей, управляемых Spring Security.

Эта статья состоит из следующих разделов

  • Аутентификация с помощью Spring Security. В этом разделе обсуждается подход к реализации аутентификации с использованием инфраструктуры Spring Security для веб-приложения, использующего аутентификацию на основе форм.

  • Интеграция с MyBatis: этот раздел дополняет предыдущий раздел, демонстрируя методику использования инфраструктуры Spring Security для упрощения управления соединениями.

Аутентификация с помощью Spring Security

В этой статье описывается простой сценарий, когда пользователи вводят свое имя пользователя и пароль, используя страницу входа для аутентификации в системе. Страница входа отправляет учетные данные пользователя на специальный URL-адрес, обрабатываемый Spring Security.

Страница входа (пример JSP)

<form method="POST" action="<c:url value="/j_spring_security_check" />">
	<label for="userName">User Name</label>
	<input type="text" id="userName" name="j_username" value=""/>
	<label for="userPassword">Password</label>
	<input type="password" id="userPassword" name="j_password"/>
	<div>
		<button type="submit" class="btn btn-primary">Login</button>
	</div>
</form>

Этот URL-адрес, имеющий значение / j_spring_security_check, внутренне сопоставляется с фильтром сервлета с идентификатором authenticationFilter, как определено в корневом контексте Spring Security, файле root-context.xml, следующим образом:

<beans:bean id="authenticationFilter"
class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
	<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>

AuthenticationFilter, который предоставляет механизм имени пользователя / пароля с помощью класса UsernamePasswordAuthenticationFilter, отвечает за обработку формы входа и создание объекта аутентификации, который содержит учетные данные пользователя. Этот объект передается менеджеру проверки подлинности, который может иметь одного или нескольких поставщиков проверки подлинности. Для простоты примера следующий код конфигурации определяет только одного поставщика:

<authentication-manager alias="authenticationManager" erase-credentials="false">
	<authentication-provider ref="databaseAuthenticationProvider" />
</authentication-manager>

Обратите внимание на использование атрибута erase-credential, который указывает Spring Security не очищать учетные данные пользователя после завершения процесса аутентификации. Это позволит вам использовать учетные данные позже, когда вам понадобится установить соединение с базой данных, как описано во втором разделе этой статьи. Менеджер аутентификации относится к следующему поставщику аутентификации базы данных, который имеет два свойства; один для класса базы данных и другой для URL, как показано ниже:

<beans:bean class="com.infoq.DatabaseAuthenticationProvider"
	id="databaseAuthenticationProvider">
	<beans:property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
	<beans:property name="url" value="${db.url}" />
</beans:bean>

Это метод authenticate () поставщика аутентификации, который выполняет фактическую логику аутентификации. В этом методе вы можете написать свою собственную логику аутентификации в зависимости от ваших бизнес-требований. Обратите внимание, что в примере используется имя пользователя и пароль, предоставленные пользователем, а не общая учетная запись для подключения к базе данных. Это соответствует политике корпоративной среды, как указано ранее. Чтобы проверить, что пользователь имеет доступ к базе данных, экземпляр JdbcTemplate используется для получения списка привилегий для пользователя и устанавливается как часть токена аутентификации. Если JdbcTemplate не может получить соединение с базой данных с использованием учетных данных пользователя, то будет выдано исключение, и Spring Security сообщит о неудачной попытке аутентификации. Логика аутентификации представлена ​​ниже:

public class DatabaseAuthenticationProvider implements AuthenticationProvider {

	private String url;
	private String driverClassName;

	@Override
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {

		ArrayList<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		String password = (String) authentication.getCredentials();

		try {
			OracleDataSource oracleDataSource = new OracleDataSource();
			oracleDataSource.setURL(this.getUrl());
			oracleDataSource.setUser(username);
			oracleDataSource.setPassword(password);

			JdbcTemplate jdbcTemplate = new JdbcTemplate(oracleDataSource);
			String query = "select * from user_privileges where username = ?";
			List<Map<String, Object>> list = jdbcTemplate.queryForList(query, username);

			Iterator<Map<String, Object>> iterator = list.iterator();
			while (iterator.hasNext()) {
				Map<String, Object> map = iterator.next();
				String role = (String) map.get("user_ privilege");
				authorities.add(new SimpleGrantedAuthority(role));
			}
		} catch (DataAccessException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return new UsernamePasswordAuthenticationToken(username, password, authorities);
	}
	@Override
	public boolean supports(Class<?> authentication) {
		return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
	}
	public String getUrl() {
		return url;
	}
	public void setUrl(String url) {
		this.url = url;
	}
	public String getDriverClassName() {
		return driverClassName;
	}
	public void setDriverClassName(String driverClassName) {
		this.driverClassName = driverClassName;
	}
}

После возврата из метода authenticate Spring Security сохранит маркер аутентификации в своем объекте SecurityContext. На этом этапе пользователь проходит проверку подлинности и имеет доступ к системе. Соединение с базой данных нигде не сохраняется. Преимущество этого заключается в уменьшении занимаемой памяти на сервере за счет отсутствия простоя соединений с базой данных. На следующем рисунке представлен процесс проверки подлинности на высоком уровне.

At this point, you have built the foundation for the next section, which will facilitate the proper management of database connections. To learn more about authentication services provided by Spring Security, please refer to http://docs.spring.io/spring-security/site/docs/3.2.5.RELEASE/reference/htmlsingle/#tech-intro-authentication.

Integration with MyBatis

Now that the user credentials and privileges are stored in the SecurityContext, it is time to configure MyBatis to make use of this information. MyBatis uses an instance of SqlSession, which relies on a DataSource to get a database connection. An instance of DataSource is often configured in advance with a generic username and password to create a pool of database connections. Moreover, once a DataSource is configured, it uses the same credentials to get additional database connections. This is problematic in a Web environment where individual users are expected to use their own credentials. To overcome the limitations of the DataSource class, an adapter will be used. This adapter will delegate the user credentials to every call of the getConnection() method of the target DataSource to ensure that the returned connection is for the right user, as shown below:

<beans:bean id="dataSource"
	class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<beans:property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
	<beans:property name="url" value="${db.url}" />
</beans:bean>
<beans:bean id="dataSourceAdapter"
	class="org.springframework.jdbc.datasource.UserCredentialsDataSourceAdapter">
	<beans:property name="targetDataSource" ref="dataSource" />
</beans:bean>

The next step is to configure MyBatis to use the adapter as part of its SQL session factory bean.

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dataSourceAdapter" />
</bean>

There is one piece still missing from the puzzle. How are we going to provide the DataSource adapter with user credentials? The trick here is to define a filter that would retrieve user credentials from the SecurityContext and pass them to the adapter.

<beans:bean id="credentialsFilter" class="com.infoq.CredentialsFilter">
	<beans:property name="dataSourceAdapter" ref="dataSourceAdapter" />
</beans:bean>

Once the CredentialsFilter retrieves the authentication token from the SecurityContext, the filter will invoke the setCredentialsForCurrentThread() method on the adapter to ensure that a call to getConnection on the DataSource will use the credentials of the intended user. Be careful not to use the setUsename() and setPassword() methods, which will set the credentials globally for the adapter, thereby, risking the use of the wrong credentials for some users. Correct use of the CredentialsFilter class is shown below:

public class CredentialsFilter implements Filter{
	private UserCredentialsDataSourceAdapter dataSourceAdapter;
	public UserCredentialsDataSourceAdapter getDataSourceAdapter() {
		return dataSourceAdapter;
	}

	public void setDataSourceAdapter(UserCredentialsDataSourceAdapter dataSourceAdapter) {
		this.dataSourceAdapter = dataSourceAdapter;
	}
	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		SecurityContext securityContext = SecurityContextHolder.getContext();
		Authentication authentication = securityContext.getAuthentication();
		
		if(authentication != null){
			Object principal = authentication.getPrincipal();
			Object credentials = authentication.getCredentials();
			dataSourceAdapter.setCredentialsForCurrentThread(
					principal.toString(), credentials.toString());
		}
		try{
			chain.doFilter(request, response);
		} finally{
			dataSourceAdapter.removeCredentialsFromCurrentThread();
		}
	}
}

Note the invocation of removeCredentialsFromCurrentThread() method at the end of doFilter(). This is needed to clear the user credentials from the current thread and void any possible memory leaks. The following figure summarized the integration process at a higher level:

The last step is to configure Spring Security to use the two filters. This can be easily achieved by using custom-filter tags as part of the http element in the Spring Security Root context as follows:

<http auto-config="false">
	<custom-filter position="FORM_LOGIN_FILTER" ref="authenticationFilter"/>
	<custom-filter after="FORM_LOGIN_FILTER" ref="credentialsFilter"/>
</http>

To learn more about Spring Security Filter Chain and the position attribute, please refer to http://docs.spring.io/spring-security/site/docs/3.2.5.RELEASE/reference/htmlsingle/#security-filter-chain.

Conclusion

By using Spring Security authentication services along with MyBatis you are able to delegate the management of database connection to MyBatis framework. Moreover, you no longer need to worry about opening, closing and storing database connections. Furthermore, you reduce the footprint on the application server and the database.