6.7. Social Login

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

Руководство Anonymous Access & Social Login содержит пример настройки публичного доступа к некоторым экранам приложения, а также реализации пользовательского входа в приложение с помощью учетной записи Google, Facebook или GitHub.

В этом примере мы рассмотрим, как можно войти в приложение, используя аккаунт на 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

    Выдайте разрешение email, позволяющее вашему приложению просматривать основной электронный адрес пользователя.

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

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

    <linkButton id="facebookBtn"
                align="MIDDLE_CENTER"
                caption="Facebook"
                icon="font-icon:FACEBOOK_SQUARE"/>
  3. Чтобы использовать учётные записи пользователей Facebook в своём приложении, добавьте новое поле к стандартной учётной записи пользователя CUBA. Расширьте сущность User и добавьте строковый атрибут facebookId:

    @Column(name = "FACEBOOK_ID")
    protected String facebookId;
  4. Создайте роль FacebookAccessRole, дающую право пользователю просматривать экраны помощи, настроек и информации о приложении:

    @Role(name = "facebook-access")
    public class FacebookAccessRole extends AnnotatedRoleDefinition {
        @ScreenAccess(screenIds = {
                "help",
                "aboutWindow",
                "settings",
        })
        @Override
        public ScreenPermissionsContainer screenPermissions() {
            return super.screenPermissions();
        }
    }
  5. Создайте сервис, который будет искать пользователя приложения в базе данных по переданному 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 DataManager dataManager;
        @Inject
        private Configuration configuration;
    
        @Override
        public User findOrRegisterUser(String facebookId, String email, String name) {
            User existingUser = dataManager.load(User.class)
                    .query("select u from sec$User u where u.facebookId = :facebookId")
                    .parameter("facebookId", facebookId)
                    .optional()
                    .orElse(null);
            if (existingUser != null) {
                return existingUser;
            }
            SocialUser user = dataManager.create(SocialUser.class);
            user.setLogin(email);
            user.setName(name);
            user.setGroup(getDefaultGroup());
            user.setActive(true);
            user.setEmail(email);
            user.setFacebookId(facebookId);
            UserRole fbUserRole = dataManager.create(UserRole.class);
            fbUserRole.setRoleName("facebook-access");
            fbUserRole.setUser(user);
            EntitySet eSet = dataManager.commit(user, fbUserRole);
            return eSet.get(user);
        }
    
        private Group getDefaultGroup() {
            SocialRegistrationConfig config = configuration.getConfig(SocialRegistrationConfig.class);
    
            return dataManager.load(Group.class)
                    .query("select g from sec$Group g where g.id = :defaultGroupId")
                    .parameter("defaultGroupId", config.getDefaultGroupId())
                    .one();
        }
    }
  6. Создайте сервис для реализации логики входа. В данном примере это сервис FacebookService, содержащий два метода: getLoginUrl() и getUserData().

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

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

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

    facebook.fields = id,name,email
  8. Вернёмся к событию нажатия кнопки логина через Facebook в контроллере расширенного окна входа. Код контроллера целиком вы можете найти в файле ExtLoginScreen.java.

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

    private RequestHandler facebookCallBackRequestHandler =
            this::handleFacebookCallBackRequest;
    
    private URI redirectUri;
    
    @Inject
    private FacebookService facebookService;
    
    @Inject
    private GlobalConfig globalConfig;
    
    @Subscribe("facebookBtn")
    public void onFacebookBtnClick(Button.ClickEvent event) {
        VaadinSession.getCurrent()
            .addRequestHandler(facebookCallBackRequestHandler);
    
        this.redirectUri = Page.getCurrent().getLocation();
    
        String loginUrl = facebookService.getLoginUrl(globalConfig.getWebAppUrl(), FacebookService.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");
    
                    FacebookService.FacebookUserData userData = facebookService.getUserData(globalConfig.getWebAppUrl(), code);
    
                    User user = socialRegistrationService.findOrRegisterUser(
                            userData.getId(), userData.getEmail(), userData.getName());
    
                    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.