Статьи

Начало работы с Spring Social — часть 2

Несколько недель назад я написал пост, демонстрирующий то, что я считаю самым простым приложением, которое вы можете написать с помощью Spring Social. Это приложение считывало и отображало общедоступные данные пользователя Twitter и было написано как введение в Spring Social и в область социального кодирования. Однако получить приложение для отображения общедоступных данных вашего пользователя — это только половина дела, и большую часть времени вам потребуется для отображения личных данных вашего пользователя.

В этом блоге я расскажу о сценарии, когда у вас есть требование отображать данные пользователя Facebook или другого поставщика программного обеспечения в качестве службы (SaaS) на одной или двух страницах вашего приложения. Идея здесь состоит в том, чтобы попытаться продемонстрировать самую маленькую и простую вещь, которую вы можете добавить, чтобы добавить Spring Social в приложение, которое требует от вашего пользователя входа в Facebook или другого поставщика SaaS.

Создание приложения

Для создания приложения первым шагом является создание базового проекта Spring MVC с использованием раздела шаблонов панели инструментов SpringSource Toolkit. Это обеспечивает веб-приложение, которое поможет вам начать.

Следующим шагом является настройка pom.xml путем добавления следующих зависимостей:

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>

… Очевидно, вам также необходимо добавить следующее в раздел % lt; properties /> файла:

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>

Вы заметите, что я добавил специальную запись pom для spring-security-crypto : это потому, что я использую Spring 3.0.6. Весной 3.1.x это стало частью основных библиотек.

Единственное, на что следует обратить внимание, это то, что существует также зависимость от spring-jdbc и h2 . Это потому, что реализация SpringConnectionRepository по умолчанию Spring: JdbcUsersConnectionRepository использует их, и, следовательно, они необходимы, даже если это приложение ничего не сохраняет в базе данных (насколько я могу судить).

Классы

Функциональность социального кодирования состоит из четырех классов (и одного из тех, которые я выделил из примера кода Spring Social Quick Start Start Кейта Дональда):

  • FacebookPostsController
  • Социальный контекст
  • FacebookConfig
  • UserCookieGenerator

FacebookPostsController — это бизнес-приложение, которое отвечает за получение данных Facebook пользователя и передачу их в модель, готовую для отображения.

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;
 
  }
 
}

Как видите, с точки зрения высокого уровня логика того, что мы пытаемся достичь, довольно проста:

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

FacebookPostsController делегирует задачу обработки логики входа классу SocialContext . Вы, вероятно, можете догадаться, что я получил идею для этого класса из действительно полезного SpringCon ApplicationContext . Идея в том, что есть один класс, который отвечает за привязку вашего приложения к 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 реализует интерфейсы ConnectionSignUp и SignInAdapter в Spring Social. Он содержит три метода isSignedIn () , signIn () , execute () . isSignedIn вызывается классом FacebookPostsController для реализации описанной выше логики, в то время как signIn () и execute () вызываются Spring Social.

Из моих предыдущих блогов вы помните, что OAuth требует много поездок между браузером, вашим приложением и поставщиком SaaS. При выполнении этих поездок приложение должно сохранять состояние нескольких аргументов OAuth, таких как: client_id, redirect_uri и другие. Spring Social скрывает всю эту сложность от вашего приложения, сопоставляя состояние диалога OAuth с одной переменной, которой управляет ваше веб-приложение. Это идентификатор пользователя ; тем не менее, не думайте, что это имя пользователя, потому что оно никогда не просматривается пользователем, это просто уникальный идентификатор, который связывает несколько HTTP-запросов с подключением SaaS-провайдера (например, Facebook) в ядре Spring Social.

Из-за его простоты я следовал идее Кейта Дональда об использовании файлов cookie для передачи идентификатора пользователя между браузером и сервером для поддержания состояния. Я также позаимствовал его класс UserCookieGenerator из Spring Social Quick Start, чтобы помочь мне в этом.

Метод isSignedIn (...) использует UserCookieGenerator, чтобы выяснить, содержит ли объект HttpServletRequest файл cookie, содержащий действительный идентификатор пользователя. Если это так, то также выясняется, содержит ли UsersConnectionRepository в Spring Social объект ConnectionRepository, связанный с тем же идентификатором пользователя. Если оба эти теста вернут true, тогда приложение запросит и отобразит данные пользователя Facebook. Если один из двух тестов возвращает false, пользователю будет предложено выполнить вход.

SocialContext был написан специально для этого примера и содержит достаточно функций, чтобы продемонстрировать, о чем я говорю в этом блоге. Это означает, что в настоящее время он немного груб и готов, хотя его можно улучшить, чтобы охватить подключения к любому / нескольким провайдерам, а затем повторно использовать в различных приложениях.

Последний класс, который нужно упомянуть, это FacebookConfig , который свободно основан на примере кода Spring Social. Существует два основных различия между этим кодом и примером кода, первое из которых заключается в том, что класс FacebookConfig реализует интерфейс InitializingBean . Это делается для того, чтобы переменная usersConnectionRepositiory могла быть вставлена в socialContext, и, в свою очередь, socialContext может быть вставлена в usersConnectionRepositiory как его реализация ConnectionSignUp . Второе отличие заключается в том, что я реализую метод providerSignInController (...), чтобы обеспечить правильно настроенный объект ProviderSignInController, который будет использоваться Spring Social для входа в Facebook. Единственное изменение по умолчанию, которое я здесь сделал, — это установить для свойства postSignInUrl в ProviderSignInController значение « / posts ». Это URL-адрес страницы, которая будет содержать данные пользователей Facebook и будет вызываться после завершения входа пользователя.

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;
 
  }
 
}

Поток приложений

Если вы запустите это приложение 2, вы впервые увидите домашний экран, содержащий простую ссылку, предлагающую вам просмотреть свои сообщения. Когда вы нажимаете эту ссылку в первый раз, вы перенаправляетесь на страницу / signin . Нажатие кнопки «Войти» сообщает ProviderSignInController для связи с Facebook. После завершения аутентификации ProviderSignInController перенаправляет приложение обратно на страницу / posts и на этот раз отображает данные Facebook.

конфигурация

Для полноты я подумал, что должен упомянуть конфигурацию XML, хотя ее немного, потому что я использую аннотацию Spring @Configuration в классе FacebookConfig . Я импортировал « data.xml » из Spring Social, чтобы JdbcUsersConnectionRepository работал и добавил

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

… Для автопроводки.

Резюме

Хотя этот пример приложения основан на подключении вашего приложения к данным Facebook вашего пользователя, его можно легко изменить, чтобы использовать любой из клиентских модулей Spring Social. Если вам нравится вызов, попробуйте реализовать Sina-Weibo, где все на китайском языке — это вызов, но Google Translate действительно полезен.

1 Spring Social и другие блоги OAuth:

  1. Начало работы с Spring Social
  2. Facebook и Twitter: за кулисами
  3. Шаги администрации OAuth
  4. Обзор потока веб-приложений OAuth 2.0

2 Код доступен на Github по адресу: https://github.com/roghughe/captaindebug.git.

Ссылка: Начало работы с Spring Social — часть 2 от нашего партнера по JCG Роджера Хьюза в блоге Captain Debug’s Blog .