In this blog, I’m going to cover the scenario where you have a requirement to display a user’s Facebook or other Software as a Service (SaaS) provider data on one or two pages of your application. The idea here is to try to demonstrate the smallest and simplest thing you can to to add Spring Social to an application that requires your user to log in to Facebook or other SaaS provider.
Creating the App
To create the application, the first step is to create a basic Spring MVC Project using the template section of the SpringSource Toolkit Dashboard. This provides a webapp that’ll get you started.The next step is to set up the pom.xml by adding the following dependencies:
<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>
...obviously you’ll also need to add the following to the %lt;properties/> section of the file:
<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>
You’ll notice that I’ve added a specific pom entry for spring-security-crypto: this is because I’m using Spring 3.0.6. In Spring 3.1.x, this has become part of the core libraries.
The only other point to note is that there is also a dependency on spring-jdbc and h2. This is because Spring’s UserConnectionRepository default implementation: JdbcUsersConnectionRepository uses them and hence they’re required even though this app doesn’t persist anything to a database (so far as I can tell).
The Classes
The social coding functionality consists of four classes (and one of those I’ve pinched from Keith Donald’s Spring Social Quick Start Sample code):- FacebookPostsController
- SocialContext
- FacebookConfig
- UserCookieGenerator
@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;
}
}
As you can see, from a high-level viewpoint the logic of what we’re trying to achieve is pretty simple:
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
The FacebookPostsController delegates the task of handling the sign in logic to the SocialContext class. You can probably guess that I got the idea for this class from Spring’s really useful ApplicationContext. The idea here is that there is one class that’s responsible for gluing your application to 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;
}
}
SocialContext implements Spring Social’s ConnectionSignUp and SignInAdapter interfaces. It contains three methods isSignedIn(), signIn(), execute(). isSignedIn is called by the FacebookPostsController class to implement the logic above, whilst signIn() and execute() are called by Spring Social.
From my previous blogs you'll remember that OAuth requires lots of trips between the browser, your app and the SaaS provider. In making these trips the application needs to save the state of several OAuth arguments such as: client_id, redirect_uri and others. Spring Social hides all this complexity from your application by mapping the state of the OAuth conversation to one variable that your webapp controls. This is the userId; however, don’t think of think of this as a user name because it’s never seen by the user, it’s just a unique identifier that links a number of HTTP requests to an SaaS provider connection (such as Facebook) in the Spring Social core.
Because of its simplicity, I’ve followed Keith Donald's idea of using cookies to pass the user id between the browser and the server in order to maintain state. I've also borrowed his UserCookieGenerator class from the Spring Social Quick Start to help me along.
The isSignedIn(...) method uses UserCookieGenerator to figure out if the HttpServletRequest object contains a cookie that contains a valid user id. If it does then it also figures out if Spring Social’s UsersConnectionRepository contains a ConnectionRepository linked to the same user id. If both of these tests return true then the application will request and display the user’s Facebook data. If one of the two tests returns false, then the user will be asked to sign in.
SocialContext has been written specifically for this sample and contains enough functionality to demonstrate what I’m talking about in this blog. This means that it’s currently a little rough and ready, though it could be improved to cover connections to any / many providers and then reused in different applications.
The final class to mention is FacebookConfig, which is loosely based upon the Spring Social sample code. There are two main differences between this code and the sample code with the first of these being that the FacebookConfig class implements the InitializingBean interface. This is so that the usersConnectionRepositiory variable can be injected into the socialContext and in turn the socialContext can be injected into the usersConnectionRepositiory as its ConnectionSignUp implementation. The second difference is that I’m implementing a providerSignInController(...) method to provide a correctly configured ProviderSignInController object to be used by Spring Social to sign in to Facebook. The only change to the default I’ve made here is to set the ProviderSignInController’s postSignInUrl property to “/posts”. This is the url of the page that will contain the users Facebook data and will be called once the user sign in is complete.
@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
If you run this application2 you’re first presented with the home screen containing a simple link inviting you to display your posts. The first time you click on this link, you’re re-directed to the /signin page. Pressing the ‘sign in’ button tells the ProviderSignInController to contact Facebook. Once authentication is complete, then the ProviderSignInController directs the app back to the /posts page and this time it displays the Facebook data.Configuration
For completeness, I thought that I should mention the XML config, although there’s not much of it because I’m using the Spring annotation @Configuration on the FacebookConfig class. I have imported “data.xml” from the Spring Social so that JdbcUsersConnectionRepository works and added<context:component-scan base-package="com.captaindebug.social" />
...for autowiring.
Summary
Although this sample app is based upon connecting your app to your user’s Facebook data, it can be easily modified to use any of the Spring Social client modules. If you like a challenge, try implementing Sina-Weibo where everything’s in Chinese - it’s a challenge, but Google Translate comes in really useful.1 Spring Social and Other OAuth Blogs:
- Getting Started with Spring Social
- Facebook and Twitter: Behind the Scenes
- The OAuth Administration Steps
- OAuth 2.0 Webapp Flow Overview
Facebook have made some changes entitled: "July 2013 Breaking Changes", which break this application. To ensure that everything works, use Spring social version 1.0.3.RELEASE or greater.
2 The code is available on Github at: https://github.com/roghughe/captaindebug.git
1 comment:
I just came across your blog and wanted to drop you a note telling you how impressed I was with the information you have posted here.
Thanks
Susanne Green
medical assistant
Post a comment