Аутентификация через социальные сети для JavaServer Faces (JSF)

в 13:06, , рубрики: java, javaee, oauth 2.0, Программирование, метки: ,

Про вред от паролей при аутентификации пользователей сайта и вход через социальные сети написано уже много. Но в основном это касается либо теоретических рассуждений, либо каких-то аспектов интеграции модуля на PHP в одну из CMS.

Я предлагаю вашему вниманию пример организации аутентификации пользователей сетей ВКонтакте, Facebook, Мой Мир, Яндекс, Одноклассники, Google, Twitter, LinkedIn средствами JSF2. Это полностью авторская разработка, охватывающая наиболее популярных OAuth провайдеров рунета. Написана без использования сторонних библиотек, сервлетов, jsp и спрингов — только JSF2 и CDI.

Все это родилось при разработке платного сайта, предоставляющего услуги обучения. Естественно, чтобы заставить людей расстаться хоть с малой частью своих «кровных» нужно очень постараться. Сложная регистрация с паролями, е-мейлами, капчей и временными зонами этому не очень способствует. Поэтому было принято решение ограничиваться только логином и паролем (дважды), никаких дополнительных вопросов! На практике это вылилось во множественные регистрации, забывание пароля, зачастую уже после оплаты (как водится, своего лицевого счета никто не помнит) и тому подобные неприятности. Анкету тоже практически никто не заполнял, — службе поддержки пришлось не сладко.

Решение напрашивалось само собой: обеспечить регистрацию и последующую аутентификацию пользователей «одним кликом». Социальные сети с технологией OAuth подходили идеально. Авторизация у нас уже была, нужно было только получить некий уникальный идентификатор. Что и было сделано.

На главной странице нашего сайта есть две кнопки «Вход» и «Регистрация». Технически их можно было бы совместить, так как у пользователя нет возможности ошибиться — никаких данных вручную он не вводит. Если мы через OAuth получили идентификатор, которого еще нет в нашей базе — значит регистрируем нового пользователя, иначе просто пускаем в систему. Для нового пользователя можно еще показать приветствие.

Дотошный читатель спросит, а почему нужно было разрабатывать свое решение, ведь для каждой соцсети уже есть готовые библиотеки, реализующие весь функционал? Да, есть. Но за этот «весь», в данном случае избыточный, функционал приходится платить лишним объемом, многочисленными зависимостями и необходимостью интеграции и поддержки разных (для каждой библиотеки) подходов. Чужие баги и уязвимости тоже не нужно сбрасывать со счетов.

Итак, рассмотрим подробнее как все устроено

Для статьи на Хабре был разработан демо-проект — из рабочей системы выкушены части и убрано все лишнее, осталась только работающая модель. Внизу статьи даны ссылки на проект и демонстрацию.

Все начинается с login.xhtml, выглядит это так:
image
Иконки взяты из Хабрапака. За каждой иконкой закреплен вызов метода, который формирует запрос и посредством редиректа отправляет пользователя на сайт провайдера OAuth.

Пример кода для Яндекс:

	String plainUrl = "https://oauth.yandex.ru/authorize?" 
				+"client_id="+ yaId
				+"&response_type=code";
	FacesContext.getCurrentInstance().getExternalContext().redirect(plainUrl);

После того, как пользователь согласился с предложением дать доступ к его данным, он возвращается на наш сайт, принимающей стороной есть страница yaLogin.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"> 

<h:head></h:head>
<f:metadata>
	<f:viewParam id="code" name="code" value="#{yalogin.code}" />
	<f:viewParam id="error" name="error" value="#{yalogin.error}" />
	<f:viewParam id="state" name="state" value="#{yalogin.state}" />
	<f:event type="preRenderView" listener="#{yalogin.phase2()}" />
</f:metadata>
<body> 
</body> 
</html>

Как видим, полученные параметры code, error и state передаются в переменные класса и запускается метод phase2(), который собственно и проводит дальнейшую обработку.

Проверяем, не вернули ли нам ошибку:

if (error!=null) {
	logger.info("error="+error);
	try {
	FacesContext.getCurrentInstance().getExternalContext().redirect("error.jsf");
	} catch (Exception e) {
		logger.log(Level.SEVERE, "phase2() Error: redirect failed!", e);
	}
	return "";
}

Если все в порядке, формируем запрос на получение токена:

	Properties props = new Properties();
	props.put("grant_type", "authorization_code");
	props.put("code", code);
	props.put("client_id", yaId);
	props.put("client_secret", yaSecret);
	String ret1 = HttpURL.httpsPost("https://oauth.yandex.ru/token", props, "UTF-8");

И парсим результат:

	class YA{
		String access_token;
		String token_type;
	}

	YA ya = (YA) XStreamFactory.get(YA.class, ret1);

Имея токен доступа, можно запросить информацию о пользователе:

String ret2 = HttpURL.httpsGet("https://login.yandex.ru/info?format=json&oauth_token="+ya.access_token);

	class PersonData {
		public String birthday;
		public String display_name;
		public String sex;
		public String id;
		public String default_email;
		public String real_name;
	}
	PersonData personData = (PersonData) XStreamFactory.get(PersonData.class, ret2);

Теперь можно полученные данные передать в объект Core (диспетчер):

	core.getUserAutoReqProps().setEmail(personData.default_email);

	String[] fname = personData.real_name.split(" ");
	core.getUserAutoReqProps().setLastName(fname[0]);
	core.getUserAutoReqProps().setFirstName(fname[1]);
	core.getUserAutoReqProps().setSex("male".equalsIgnoreCase(personData.sex));

	DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
	core.getUserAutoReqProps().setBorn(dateFormat.parse(personData.birthday));

Запомнить уникальный идентификатор пользователя и перейти к защищенной части сайта:

	core.setValidatedId("http://my.ya.ru/"+personData.id);
	FacesContext.getCurrentInstance().getExternalContext().redirect("welcome.jsf");

Все, имеем аутентифицированного пользователя! Естественно, его нужно еще авторизовать, но это выходит за рамки статьи.

Как видим, для работы с одним провайдером OAuth достаточно одного java класса и одной xhtml страницы. Для Яндекса это будет YALogin.java и yaLogin.xhtml, для ВКонтакте соответственно VKLogin.java и vkLogin.xhtml, и так далее, по аналогии.

Проблема, где хранить Id и Secret получаемые при регистрации приложений, была решена следующим образом: во внешнем файле на файловой системе сервера приложений. Это дает следующие преимущества: у разработчиков в файле данные тестовых приложений а на сервере — реальных, при деплое ничего не нужно менять и секретность соблюдена. Пример файла oauth.properties есть в проекте.
Путь к файлу хранится в web.xml:

<context-param>
  <param-name>OAuthPropertiesPath</param-name>
  <param-value>/opt/oauth/oauth.properties</param-value>
</context-param>

При старте приложения Action bean открывает и парсит этот файл.

@Named
@ApplicationScoped
public class OAuthDAO implements Serializable {

	private static final long serialVersionUID = 1L;
	private Properties prop;
	private static Logger logger = Logger.getLogger(OAuthDAO.class.getName());

	public OAuthDAO() {
		load();
	}
	
	public boolean load(){
		try (FileInputStream fis = new FileInputStream(FacesContext.getCurrentInstance().getExternalContext().getInitParameter("OAuthPropertiesPath"))){
			prop = new Properties();
			prop.load(fis);
			if (prop.isEmpty()) {
				return false;
			}
		} catch (Exception ex) {
			return false;
		}
		return true;
	}

	public String getProperty(String key) {
		return prop.getProperty(key);
	}
}

В представленном тестовом приложении, после завершения аутентификации, отображается страница welcome.xhtml, которая символизирует защищенную часть приложения. Чтобы неавторизированный пользователь не попал на эту страницу, по событию preRenderView вызывается метод core.fiscal() который осуществляет проверку.

<f:metadata>
	<f:event listener="#{core.fiscal()}" type="preRenderView" />
</f:metadata>

Демо работает под управлением сервера приложений GlassFish. Для парсинга JSON и XML была использована библиотека XStream.

Посмотреть работу приложения можно здесь.
Скачать проект под Eclipse здесь.

Автор: vbabich

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js