6.7. 社交网站登录

本章节提到的主要是使用 Facebook,Twitter 和 Google+这三个社交网络,依据网络情况,有些网址可能需要科学上网访问。

社交网站登录也是单点登录(SSO) 的一种形式,可以通过社交网站的账号(比如 Facebook,Twitter 或者 Google+)来登录 CUBA 系统,而不需要为 CUBA 应用程序创建特定的账号。

参考 匿名访问和社交网站登录 指南,了解如何为应用程序的某些界面设置公共访问,以及使用 Google、Facebook 或 GitHub 账号自定义登录的实现。

下面将使用 Facebook 来作为社交网络登录的示例。Facebook 使用 OAuth2 认证机制,想了解更多细节请参考 Facebook API 和 Facebook Login Flow: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow

示例项目代码在这里: GitHub,以下列出关键点的实现。

  1. 为了让项目连接到 Facebook,需要创建 App ID (唯一应用程序标识符)和 App Secret (为应用程序项目发送到 Facebook 的请求做认证的一种密码)。按照 介绍 申请,然后在 core 模块的 app.properties 文件中分别以 facebook.appIdfacebook.appSecret 这两个属性注册申请到的值。示例:

    facebook.appId = 123456789101112
    facebook.appSecret = 123456789101112abcde131415fghi16

    启用 email 权限,允许您的 app 查看用户的主邮箱地址。

    然后,在应用程序配置在 Facebook app 注册的 URL,填写在 coreweb 模块的应用程序属性文件的 cuba.webAppUrl 参数。示例:

    cuba.webAppUrl = http://cuba-fb.test:8080/app
  2. 扩展 登录界面 并添加社交登录按钮。订阅该按钮点击事件,作为社交登录流程的起点。

    <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 角色,允许用户查看 HelpSettingsAbout 界面:

    @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 返回类型(代码、访问令牌(access token)或者两者都有,参考 Facebook API 文档 了解更多返回类型)。这个方法的实现可以参考 FacebookServiceBean.java 文件。

    • getUserData() 使用提供的应用程序 URL 和代码来查找 Facebook 用户,并且返回已有用户的数据或者创建新用户。在这个例子中,希望获取用户的 idnameemailid 也就是上面创建的 facebookId

  7. core 模块的 app.properties 文件中定义 facebook.fields 应用程序属性:

    facebook.fields = id,name,email
  8. 返回扩展登录窗口控制器的 Facebook 登录按钮事件方法。这个控制器的所有代码在 ExtAppLoginWindow.java 文件。

    在这个方法中,有针对当前会话的请求处理(request handler),保存当前 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 会获取 facebook 用户账号的 emailid。在这之后,相应的 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 登录 章节提到的事件来实现定制化登录机制。