Getting Started with Spring Social – Part 2
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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | < 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:
1 2 3 | < 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
FacebookPostsController is the business end of the application responsible for getting hold of the user’s Facebook data and pushing it into the model ready for display.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | @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:
1 2 3 4 5 6 7 | 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.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | 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.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | @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 application 2 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
1 | < 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
2 The code is available on Github at: https://github.com/roghughe/captaindebug.git
Reference: Getting Started with Spring Social – Part 2 from our JCG partner Roger Hughes at the Captain Debug’s Blog blog.
thanks for ur nice tutorial
btw hope it is not your real secret key :P
Good tutorial, but you dont explain anything about the view of this MVC arquitecture. The tutorial looks incomplete for me.