9.6. Social Login

Вход через социальные сети, или social login, это разновидность single sign-on, которая позволяет использовать данные для входа в социальные сети, такие как Facebook, Twitter или Google+, для входа в приложения CUBA вместо того, чтобы создавать пользователя в приложении напрямую.

В этом примере мы рассмотрим, как можно войти в приложение, используя аккаунт на Facebook. В Facebook используется механизм авторизации OAuth2, более подробно о его использовании вы можете узнать из документации по Facebook API и Facebook Login Flow: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow.

Исходный код проекта из этого примера доступен на GitHub, ниже приведены ключевые моменты реализации social login.

  1. Чтобы подключить приложение к Facebook, необходимо создать для него App ID (уникальный идентификатор приложения) и App Secret (своего рода пароль для аутентификации запросов, поступающих от приложения на серверы Facebook). Следуя инструкции, создайте эти значения и затем зарегистрируйте их в файле app.properties в модуле core в свойствах приложения facebook.appId и facebook.appSecret соответственно, например:

    facebook.appId = 123456789101112
    facebook.appSecret = 123456789101112abcde131415fghi16

    Также необходимо зарегистрировать URL, который вы указали при регистрации приложения на Facebook, в свойстве приложения cuba.webAppUrl в модулях core и web, к примеру:

    cuba.webAppUrl = http://cuba-fb.test:8080/app
  2. Расширьте окно входа в систему и добавьте кнопку для входа через социальную сеть. По нажатию этой кнопки будет вызываться метод loginFacebook() - наша точка входа в процедуру social login.

  3. Чтобы использовать учётные записи пользователей Facebook в своём приложении, необходимо добавить новое поле к стандартной учётной записи пользователя CUBA. Расширьте сущность User и добавьте строковый атрибут facebookId:

    @Column(name = "FACEBOOK_ID")
    protected String facebookId;
  4. Создайте сервис, который будет искать пользователя приложения в базе данных по переданному facebookId, и если таковой не найден, то создавать его на лету:

    public interface SocialRegistrationService {
        String NAME = "demo_SocialRegistrationService";
    
        User findOrRegisterUser(String facebookId, String email, String name);
    }
    @Service(SocialRegistrationService.NAME)
    public class SocialRegistrationServiceBean implements SocialRegistrationService {
        @Inject
        private Metadata metadata;
    
        @Inject
        private Persistence persistence;
    
        @Inject
        private Configuration configuration;
    
        @Override
        @Transactional
        public User findOrRegisterUser(String facebookId, String email, String name) {
            EntityManager em = persistence.getEntityManager();
    
            TypedQuery<SocialUser> query = em.createQuery("select u from sec$User u where u.facebookId = :facebookId",
                    SocialUser.class);
            query.setParameter("facebookId", facebookId);
            query.setViewName(View.LOCAL);
    
            SocialUser existingUser = query.getFirstResult();
            if (existingUser != null) {
                return existingUser;
            }
    
            SocialRegistrationConfig config = configuration.getConfig(SocialRegistrationConfig.class);
    
            Group defaultGroup = em.find(Group.class, config.getDefaultGroupId(), View.MINIMAL);
    
            SocialUser user = metadata.create(SocialUser.class);
            user.setFacebookId(facebookId);
            user.setEmail(email);
            user.setName(name);
            user.setGroup(defaultGroup);
            user.setActive(true);
            user.setLogin(email);
    
            em.persist(user);
    
            return user;
        }
    }
  5. Создайте сервис для реализации логики входа. В данном примере это сервис FacebookService, содержащий два метода: getLoginUrl() и getUserData().

    • getLoginUrl() генерирует URL для входа на основании URL приложения и типа ответа OAuth2 (code, access token или оба; более подробно о параметре response_type см. в документации Facebook API). Исходный код этого метода можно посмотреть в файле FacebookServiceBean.java.

    • getUserData() будет искать пользователя Facebook по параметрам, переданным в URL и в коде, и вернёт данные существующего пользователя или создаст нового. В этом примере из пользовательских данных нам нужны id, name и email, id будет соответствовать атрибуту facebookId, который мы создали ранее.

  6. Определите свойства приложения facebook.fields и facebook.scope в файле app.properties модуля core:

    facebook.fields = id,name,email
    facebook.scope = email
  7. Вернёмся к методу loginFacebook() в контроллере расширенного окна входа. Код контроллера целиком вы можете найти в файле ExtAppLoginWindow.java.

    В этом методе мы добавим к текущей сессии обработчик запроса, затем сохраним текущий URL и перенаправим пользователя на экран авторизации Facebook в браузере:

    private RequestHandler facebookCallBackRequestHandler =
            this::handleFacebookCallBackRequest;
    
    private URI redirectUri;
    
    @Inject
    private FacebookService facebookService;
    
    @Inject
    private GlobalConfig globalConfig;
    
    public void loginFacebook() {
        VaadinSession.getCurrent()
            .addRequestHandler(facebookCallBackRequestHandler);
    
        this.redirectUri = Page.getCurrent().getLocation();
    
        String loginUrl = facebookService.getLoginUrl(globalConfig.getWebAppUrl(), OAuth2ResponseType.CODE);
        Page.getCurrent()
            .setLocation(loginUrl);
    }

    В методе handleFacebookCallBackRequest() будет обработан обратный вызов после формы авторизации Facebook. Во-первых, используем экземпляр UIAccessor, чтобы зафиксировать состояние UI, пока будет обрабатываться запрос на вход.

    Затем с помощью FacebookService получим email и id учётной записи Facebook. После этого найдём соответствующего пользователя CUBA по его facebookId или создадим нового на лету.

    Далее будет совершен вход в приложение, будет создана новая сессия от лица этого пользователя и обновлен UI. Теперь мы можем удалить обработчик обратного вызова Facebook, так как процедура аутентификации закончена.

    public boolean handleFacebookCallBackRequest(VaadinSession session, VaadinRequest request,
                                                VaadinResponse response) throws IOException {
        if (request.getParameter("code") != null) {
            uiAccessor.accessSynchronously(() -> {
                try {
                    String code = request.getParameter("code");
    
                    FacebookUserData userData = facebookService.getUserData(globalConfig.getWebAppUrl(), code);
    
                    User user = socialRegistrationService.findOrRegisterUser(
                            userData.getId(), userData.getEmail(), userData.getName());
    
                    App app = App.getInstance();
                    Connection connection = app.getConnection();
                    Locale defaultLocale = messages.getTools().getDefaultLocale();
    
                    connection.login(new ExternalUserCredentials(user.getLogin(), defaultLocale));
                } catch (Exception e) {
                    log.error("Unable to login using Facebook", e);
                } finally {
                     session.removeRequestHandler(facebookCallBackRequestHandler);
                }
            });
    
            ((VaadinServletResponse) response).getHttpServletResponse().
                sendRedirect(ControllerUtils.getLocationWithoutParams(redirectUri));
    
            return true;
        }
    
        return false;
    }

Теперь при нажатии кнопки Facebook на экране входа приложение запросит разрешение на использование учётных данных пользователя Facebook, и если разрешение будет получено, пользователь после логина будет перенаправлен на главную страницу приложения.

Вы можете реализовать свой механизм входа при помощи интерфейсов LoginProvider, HttpRequestFilter и обработчиков событий, как это описано в разделе Процесс входа в Web Client.