Back-End

11 jan, 2013

Primeiros passos com Spring Social – Parte 02

Publicidade

Algum tempo atrás, eu escrevi um artigo demonstrando o que achei que fosse o aplicativo mais simples que você pode escrever usando o Spring Social. Esse aplicativo leu e apresentou os dados públicos de um usuário do Twitter e foi escrito como uma introdução ao Spring Social e à arena social de codificação. No entanto, fazer com que o seu aplicativo exiba dados públicos do seu usuário é apenas metade da história e, na maioria das vezes, você vai precisar exibir dados privados de seus usuários.

Neste artigo, vou abordar o cenário no qual você tem uma exigência para exibir o Software de um usuário do Facebook ou outro dados de provedor de dados de Software como Serviço (SaaS) em uma ou duas páginas de seu aplicativo. A ideia aqui é tentar demonstrar a coisa menor e mais simples que puder para adicionar Spring Social a um aplicativo que requer que o usuário faça login no Facebook ou em outro provedor de SaaS.

Criando o aplicativo

Para criar o aplicativo, o primeiro passo é criar um Spring MVC Project básico usando a seção template do SpringSource Toolkit Dashboard. Isso fornece um webapp que vai te ajudar a começar.

O próximo passo é configurar o pom.xml, adicionando as seguintes dependências:

<dependency>
 <groupId>org.springframework.security</groupId>
 <artifactId>spring-security-crypto</artifactId>
 <version>${org.springframework.security.crypto-version}</version>
</dependency>

<!-- Spring Social -->
<dependency>
 <groupId>org.springframework.social</groupId>
 <artifactId>spring-social-core</artifactId>
 <version>${spring-social.version}</version>
</dependency>  
<dependency>
 <groupId>org.springframework.social</groupId>
 <artifactId>spring-social-web</artifactId>
 <version>${spring-social.version}</version>
</dependency>

<!-- Facebook API -->
<dependency>
  <groupId>org.springframework.social</groupId>
  <artifactId>spring-social-facebook</artifactId>
  <version>${org.springframework.social-facebook-version}</version>
</dependency>

<!-- JdbcUserConfiguration -->
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-jdbc</artifactId>
 <version>${org.springframework-version}</version>
</dependency> 
<dependency>
 <groupId>com.h2database</groupId>
 <artifactId>h2</artifactId>
 <version>1.3.159</version>
</dependency>

<!-- CGLIB, only required and used for @Configuration usage: could be removed in future release of Spring -->
<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib-nodep</artifactId>
 <version>2.2</version>
</dependency>

… obviamente você também vai precisar adicionar o seguinte para a seção %lt;properties/> do arquivo:

<spring-social.version>1.0.2.RELEASE</spring-social.version>
<org.springframework.social-facebook-version>1.0.1.RELEASE</org.springframework.social-facebook-version>
<org.springframework.security.crypto-version>3.1.0.RELEASE</org.springframework.security.crypto-version>

Você vai perceber que eu adicionei uma entrada pom específica para o spring-security-crypto: isso é porque eu estou usando o Spring 3.0.6. Na versão 3.1.x, isso se tornou parte das bibliotecas do núcleo.

O único ponto que vale a pena salientar é que existe também uma dependência no spring-jdbc e no h2. Isso é por causa da implementação padrão UserConnectionRepository do Spring: JdbcUsersConnectionRepository os usa e, portanto, eles são necessários mesmo que esse aplicativo não persista qualquer coisa para um banco de dados (até onde eu posso dizer).

As classes

A funcionalidade de codificação social consiste em quatro classes (e uma das quais eu catei do código exemplo do Spring Social Quick Start de Donald Keith):

  • FacebookPostsController
  • SocialContext
  • FacebookConfig
  • UserCookieGenerator

O FacebookPostsController é o final do aplicativo responsável por pegar os dados do usuário no Facebook e levá-los para o modelo pronto para exibição.

@Controller
public class FacebookPostsController {

private static final Logger logger = LoggerFactory.getLogger(FacebookPostsController.class);

private final SocialContext socialContext;

@Autowired
public FacebookPostsController(SocialContext socialContext) {
this.socialContext = socialContext;
}

@RequestMapping(value = "posts", method = RequestMethod.GET)
public String showPostsForUser(HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {

String nextView;

if (socialContext.isSignedIn(request, response)) {

List<Post> posts = retrievePosts();
model.addAttribute("posts", posts);
nextView = "show-posts";
} else {
nextView = "signin";
}

return nextView;
}

private List<Post> retrievePosts() {

Facebook facebook = socialContext.getFacebook();
FeedOperations feedOps = facebook.feedOperations();

List<Post> posts = feedOps.getHomeFeed();
logger.info("Retrieved " + posts.size() + " posts from the Facebook authenticated user");
return posts;
}
}

Como você pode ver, de um ponto de vista mais alto nível, a lógica do que estamos tentando conseguir é muito simples:

IF user is signed in THEN 
     read Facebook data, 
     display Facebook data 
ELSE 
    ask user to sign in 
    when user has signed in, go back to the beginning
END IF

O FacebookPostsController delega a tarefa de lidar com o sinal em lógica para a classe SocialContext. Você pode adivinhar que eu tive a ideia para essa classe a partir do ApplicationContext do Spring, que é realmente muito útil. A ideia aqui é que não há uma classe que seja responsável pela colagem do seu aplicativo no Spring Social.

public class SocialContext implements ConnectionSignUp, SignInAdapter
* Use a random number generator to generate IDs to avoid cookie clashes
* between server restarts
*/
private static Random rand;

/**
* Manage cookies - Use cookies to remember state between calls to the
* server(s)
*/
private final UserCookieGenerator userCookieGenerator;

/** Store the user id between calls to the server */
private static final ThreadLocal<String> currentUser = new ThreadLocal<String>();

private final UsersConnectionRepository connectionRepository;

private final Facebook facebook;

public SocialContext(UsersConnectionRepository connectionRepository, UserCookieGenerator userCookieGenerator,
Facebook facebook) {
this.connectionRepository = connectionRepository;
this.userCookieGenerator = userCookieGenerator;
this.facebook = facebook;

rand = new Random(Calendar.getInstance().getTimeInMillis());
}

@Override
public String signIn(String userId, Connection<?> connection, NativeWebRequest request) {
userCookieGenerator.addCookie(userId, request.getNativeResponse(HttpServletResponse.class));
return null;
}

@Override
public String execute(Connection<?> connection) {
return Long.toString(rand.nextLong());
}

public boolean isSignedIn(HttpServletRequest request, HttpServletResponse response) {

boolean retVal = false;
String userId = userCookieGenerator.readCookieValue(request);
if (isValidId(userId)) {

if (isConnectedFacebookUser(userId)) {
retVal = true;
} else {
userCookieGenerator.removeCookie(response);
}
}

currentUser.set(userId);
return retVal;
}

private boolean isValidId(String id) {
return isNotNull(id) && (id.length() > 0);
}

private boolean isNotNull(Object obj) {
return obj != null;
}

private boolean isConnectedFacebookUser(String userId) {

ConnectionRepository connectionRepo = connectionRepository.createConnectionRepository(userId);
Connection<Facebook> facebookConnection = connectionRepo.findPrimaryConnection(Facebook.class);
return facebookConnection != null;
}

public String getUserId() {

return currentUser.get();
}

public Facebook getFacebook() {
return facebook;
}

}

O SocialContext implementa o ConnectionSignUp do Spring Social e as interfaces SignInAdapter. Ele contém três métodos: isSignedIn (), signIn (), execute (). isSignedIn é chamado pela classe FacebookPostsController para implementar a lógica acima, enquanto signIn () e execute () são chamados pelo Spring Social.

Você vai se lembrar dos meus artigos anteriores que OAuth requer muitas viagens entre o navegador, seu aplicativo e o provedor de SaaS. Ao fazer essas viagens, o aplicativo precisa salvar o estado de diversos argumentos do OAuth, tais como: client_id, redirect_uri, entre outros. O Spring Social esconde toda essa complexidade do seu aplicativo, mapeando o estado da conversação do OAuth para uma variável que controla seu aplicativo web. Este é o userId; no entanto, não pense nisso como um nome de usuário porque ele nunca é visto pelo usuário, é apenas um identificador único que liga um número de solicitações HTTP a uma conexão de provedor de SaaS (como o Facebook) no núcleo do Spring Social.

Por conta da sua simplicidade, eu segui a ideia do Keith Donald de usar os cookies para passar a ID do usuário entre o navegador e o servidor a fim de manter o estado. Eu também peguei emprestado a sua classe UserCookieGenerator do Spring Social Quick Start para me dar uma mão.

O método isSignedIn(…) usa o UserCookieGenerator para descobrir se o objeto HttpServletRequest tem um cookie que contém uma ID válida de usuário. Se isso acontecer, então ele também descobre se o UsersConnectionRepository do Spring Social contém um ConnectionRepository ligado à mesma ID do usuário. Se ambos os testes retornam true, então o aplicativo irá solicitar e exibir os dados do usuário do Facebook. Se um dos dois testes volta false, então será solicitado ao usuário que ele faça o seu login.

O SocialContext foi escrito especificamente para esse exemplo e contém funcionalidade suficiente para demonstrar o que eu estou falando neste artigo. Isso significa que ele está atualmente um pouco bruto e pronto, apesar de que poderia ser melhorado para abordar as conexões de todos/muitos fornecedores e depois ser reutilizado em diferentes aplicativos.

A última classe pra se falar é a FacebookConfig, que é livremente baseada no código de exemplo do Spring Social. Existem duas diferenças principais entre esse código e o exemplo de código, com a primeira delas sendo que a classe FacebookConfig implementa a interface InitializingBean. Isso acontece de modo que a variável usersConnectionRepositiory possa ser injetada no socialContext e, por sua vez, o socialContext possa ser injetado no usersConnectionRepositiory como a sua implementação ConnectionSignUp. A segunda diferença é que eu estou implementando um método providerSignInController (…) para fornecer um objeto ProviderSignInController corretamente configurado para ser usado pelo Spring Social para entrar no Facebook. A única mudança para o padrão que eu fiz aqui foi definir a propriedade postSignInUrl do ProviderSignInController para “/posts”. Essa é a URL da página que irá conter os dados de usuários do Facebook e será chamada uma vez o que o sign in do usuário estiver completo.

@Configuration
public class FacebookConfig implements InitializingBean {

private static final Logger logger = LoggerFactory.getLogger(FacebookConfig.class);

private static final String appId = "439291719425239";
private static final String appSecret = "65646c3846ab46f0b44d73bb26087f06";

private SocialContext socialContext;

private UsersConnectionRepository usersConnectionRepositiory;

@Inject
private DataSource dataSource;

/**
* Point to note: the name of the bean is either the name of the method
* "socialContext" or can be set by an attribute
*
* @Bean(name="myBean")
*/
@Bean
public SocialContext socialContext() {

return socialContext;
}

@Bean
public ConnectionFactoryLocator connectionFactoryLocator() {

logger.info("getting connectionFactoryLocator");
ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry();
registry.addConnectionFactory(new FacebookConnectionFactory(appId, appSecret));
return registry;
}

/**
* Singleton data access object providing access to connections across all
* users.
*/
@Bean
public UsersConnectionRepository usersConnectionRepository() {

return usersConnectionRepositiory;
}

/**
* Request-scoped data access object providing access to the current user's
* connections.
*/
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public ConnectionRepository connectionRepository() {
String userId = socialContext.getUserId();
logger.info("Createung ConnectionRepository for user: " + userId);
return usersConnectionRepository().createConnectionRepository(userId);
}

/**
* A proxy to a request-scoped object representing the current user's
* primary Facebook account.
*
* @throws NotConnectedException
*             if the user is not connected to facebook.
*/
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public Facebook facebook() {
return connectionRepository().getPrimaryConnection(Facebook.class).getApi();
}

/**
* Create the ProviderSignInController that handles the OAuth2 stuff and
* tell it to redirect back to /posts once sign in has completed
*/
@Bean
public ProviderSignInController providerSignInController() {
ProviderSignInController providerSigninController = new ProviderSignInController(connectionFactoryLocator(),
usersConnectionRepository(), socialContext);
providerSigninController.setPostSignInUrl("/posts");
return providerSigninController;
}

@Override
public void afterPropertiesSet() throws Exception {

JdbcUsersConnectionRepository usersConnectionRepositiory = new JdbcUsersConnectionRepository(dataSource,
connectionFactoryLocator(), Encryptors.noOpText());

socialContext = new SocialContext(usersConnectionRepositiory, new UserCookieGenerator(), facebook());

usersConnectionRepositiory.setConnectionSignUp(socialContext);
this.usersConnectionRepositiory = usersConnectionRepositiory;
}
}

Application Flow

Se executar esse aplicativo (2), você será apresentado primeiro com a tela inicial contendo um simples link convidando-o para exibir o seus posts. A primeira vez em que clicar nesse link, você será re-direcionado para a página /signin. Pressionar o botão “sign in” diz para ProviderSignInController entrar em contato com o Facebook. Quando a autenticação for concluída, o ProviderSignInController direciona o aplicativo de volta para a página/posts e dessa vez ele exibe os dados do Facebook.

Configuração

Para uma abrangência mais completa, acho que devo mencionar o XML config, embora não haja muito sobre isso, já que estou usando a notação @Configuration do Spring na classe FacebookConfig. Eu importei “data.xml” do Spring Social, de modo que JdbcUsersConnectionRepository funcione e tenha adicionado

<context:component-scan base-package="com.captaindebug.social" />

… para autowiring.

Resumo

Embora este exemplo de aplicativo se baseie na conexão de seu aplicativo aos dados do seu usuário do Facebook, ele pode ser facilmente modificado para usar qualquer um dos módulos de cliente do Spring Social. Se você gosta de um desafio, tente implementar o Sina-Weibo onde tudo está em chinês! É um desafio, mas o Google Tradutor se mostra muito útil.

(2) O código está disponível no Github em: https://github.com/roghughe/captaindebug.git

***

Texto original disponível em http://www.captaindebug.com/2012/07/getting-started-with-spring-social-part.html#.UMpEpW_7KSo