用于使用 Facebook 令牌进行身份验证的无状态 REST 端点的 Spring 社交身份验证过滤器

用于使用 Facebook 令牌进行身份验证的无状态 REST 端点的 Spring 社交身份验证过滤器

问题描述:

我想使用 Facebook 令牌 来使用 Spring Security 验证我的 REST 后端.您能否详细说明我如何将此安全性集成到我的 Spring 应用程序中.

I would like to use Facebook Tokens for authenticating my REST backend using Spring Security. Could you please elaborate how I can integrate this security to my Spring Application.

我想使用与 Spring Social Security 相同的用户管理.UserConnection 表和本地用户表.

I would like to use the same user management as Spring Social Security does. UserConnection table and local user table.

您可以从以下位置下载代码示例:

You can download the code sample from :

https://github.com/ozgengunay/FBSpringSocialRESTAuth

我们一直在寻找一种Spring"解决方案,该解决方案使用 REST 客户端已经拥有的 Facebook OAuth 令牌来保护我们的 REST 后端.例如:您有一个移动应用程序,在应用程序本身中实现了 Facebook Connect SDK,另一方面,您有一个提供 REST API 的后端.您想使用 Facebook OAuth 令牌对 REST API 调用进行身份验证.该解决方案实现了这种情况.

We have been looking for a "Spring" solution which secures our REST backends using Facebook OAuth Token that REST clients already have in hand. For example: You have a mobile app with Facebook Connect SDK implemented in the app itself and on the other hand , you have a backend which provides REST APIs. You want to authenticate REST API calls with Facebook OAuth Token. The solution realizes this scenario.

不幸的是,Spring Social Security Framework 只能保护您的有状态 HTTP 请求,而不是您的无状态 REST 后端.

Unfortunately, Spring Social Security Framework only secures your stateful HTTP requests, not your stateless REST backend.

这是 spring 社会保障框架的扩展,它由一个组件组成:FacebookTokenAuthenticationFilter.此过滤器拦截所有 REST 调用.客户端应在每个请求中将 Facebook OAuth 令牌作为input_token"参数在 url 中发送,因为 REST API 本质上是*的.过滤器查找此令牌并通过debug_token"Graph Api 调用对其进行验证.如果令牌经过验证,过滤器会尝试将用户与本地用户管理系统进行匹配.如果还没有这样的用户注册,过滤器会将该用户注册为新用户.

This is an extension of spring social security framework which consists of one component: FacebookTokenAuthenticationFilter. This filter intercepts all REST calls. The clients should send Facebook OAuth Token in the url as "input_token" parameter in every request as REST APIs are steteless in nature. The Filter looks for this token and validates it by "debug_token" Graph Api call. If the token is validated, the filter tries to match the user with the local user management system. If there is no such user registered yet, the filter registers the user as a new user.

如果您还有 REST API 以外的服务(如 Web 后端),则可以将此过滤器与 Spring Social Security 的标准 SocialAuthenticationFilter 一起使用.因此您可以使用相同的用户管理系统.

You can use this Filter together with standard SocialAuthenticationFilter of Spring Social Security if you have also services other than your REST API like a web backend. So you can use the same user management system.

1) 在 MYSQL 中按如下方式创建您的用户表:

1) Create your user table as follows in MYSQL :

CREATE TABLE IF NOT EXISTS `user` (
  `id` varchar(50) NOT NULL,
  `email` varchar(255) NOT NULL COMMENT 'unique',
  `first_name` varchar(255) NOT NULL,
  `last_name` varchar(255) NOT NULL,
  `password` varchar(255) DEFAULT NULL,
  `role` varchar(255) NOT NULL,
  `sign_in_provider` varchar(20) DEFAULT NULL,
  `creation_time` datetime NOT NULL,
  `modification_time` datetime NOT NULL,
  `status` varchar(20) NOT NULL COMMENT 'not used',
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`)
);

2) 在 context.xml 中配置您的数据源:

2) Configure your data source in context.xml :

tomcat 中的context.xml :

context.xml in tomcat :

<Resource auth="Container" driverClassName="com.mysql.jdbc.Driver" maxActive="100" maxIdle="30" maxWait="10000" 
name="jdbc/thingabled" password="..." type="javax.sql.DataSource" url="jdbc:mysql://localhost:3306/..." username="..."/>

3) Spring 配置:我们配置 spring 安全来拦截以受保护"开头的 URL 由 FacebookTokenAuthenticationFilter 进行身份验证.授权将由ROLE_USER_REST_MOBILE"角色完成.

3) Spring Configuration : We configure the spring security to intercept URLs beginning with "protected" by FacebookTokenAuthenticationFilter for authentication. Authorization will be done by "ROLE_USER_REST_MOBILE" role.

<security:http use-expressions="true" pattern="/protected/**"
create-session="never" entry-point-ref="forbiddenEntryPoint">
  <security:intercept-url pattern="/**"
  access="hasRole('ROLE_USER_REST_MOBILE')" />
<!-- Adds social authentication filter to the Spring Security filter chain. -->
  <security:custom-filter ref="facebookTokenAuthenticationFilter"
  before="FORM_LOGIN_FILTER" />
</security:http>


<bean id="facebookTokenAuthenticationFilter"
class="com.ozgen.server.security.oauth.FacebookTokenAuthenticationFilter">
  <constructor-arg index="0" ref="authenticationManager" />
  <constructor-arg index="1" ref="userIdSource" />
  <constructor-arg index="2" ref="usersConnectionRepository" />
  <constructor-arg index="3" ref="connectionFactoryLocator" />
</bean>

<security:authentication-manager alias="authenticationManager">
  <security:authentication-provider
  ref="socialAuthenticationProvider" />
</security:authentication-manager>

<!-- Configures the social authentication provider which processes authentication 
requests made by using social authentication service (FB). -->
<bean id="socialAuthenticationProvider"
class="org.springframework.social.security.SocialAuthenticationProvider">
  <constructor-arg index="0" ref="usersConnectionRepository" />
  <constructor-arg index="1" ref="simpleSocialUserDetailsService" />
</bean>

<bean id="forbiddenEntryPoint"
class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" />

<!-- This bean determines the account ID of the user.-->
<bean id="userIdSource"
class="org.springframework.social.security.AuthenticationNameUserIdSource" />

<!-- This is used to hash the password of the user. -->
<bean id="passwordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
  <constructor-arg index="0" value="10" />
</bean>
<!-- This bean encrypts the authorization details of the connection. In 
our example, the authorization details are stored as plain text. DO NOT USE 
THIS IN PRODUCTION. -->
<bean id="textEncryptor" class="org.springframework.security.crypto.encrypt.Encryptors"
factory-method="noOpText" />

4) FacebookTokenAuthenticationFilter 将拦截所有无状态 REST 请求,以使用有效的 Facebook 令牌对请求进行身份验证.检查 Facebook 令牌是否有效.如果 Facebook 令牌无效,则请求将被拒绝.如果 Facebook 令牌有效,则过滤器将尝试通过 SimpleSocialUserDetailsS​​ervice 验证请求.如果用户和用户连接数据不可用,则会创建一个新用户(通过 UserService)和 UserConnection.

4) All stateless REST requests will be intercepted by FacebookTokenAuthenticationFilter to authenticate requests using a valid Facebook Token. Checks whether Facebook token is valid . If Facebook token is not valid then request will be denied. If Facebook token isvalid then the filter will try to authenticate the request via SimpleSocialUserDetailsService. If user and userconnection data isn't available, a new user(via UserService) and UserConnection is created.

private Authentication attemptAuthService(...) {
  if (request.getParameter("input_token") == null) {
    throw new SocialAuthenticationException("No token in the request");
  }
  URIBuilder builder = URIBuilder.fromUri(String.format("%s/debug_token", "https://graph.facebook.com"));
  builder.queryParam("access_token", access_token);
  builder.queryParam("input_token", request.getParameter("input_token"));
  URI uri = builder.build();
  RestTemplate restTemplate = new RestTemplate();

  JsonNode resp = null;
  try {
    resp = restTemplate.getForObject(uri, JsonNode.class);
  } catch (HttpClientErrorException e) {
    throw new SocialAuthenticationException("Error validating token");
  }
  Boolean isValid = resp.path("data").findValue("is_valid").asBoolean();
  if (!isValid)
    throw new SocialAuthenticationException("Token is not valid");

  AccessGrant accessGrant = new AccessGrant(request.getParameter("input_token"), null, null,
    resp.path("data").findValue("expires_at").longValue());

  Connection<?> connection = ((OAuth2ConnectionFactory<?>) authService.getConnectionFactory())
    .createConnection(accessGrant);
  SocialAuthenticationToken token = new SocialAuthenticationToken(connection, null);
  Assert.notNull(token.getConnection());

  Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  if (auth == null || !auth.isAuthenticated()) {
    return doAuthentication(authService, request, token);
  } else {
    addConnection(authService, request, token);
    return null;
  }
 }

5) 项目中的其他重要部分:

5) Other important sections in the project :

用户:映射用户"表的实体.

User : Entity which maps 'user' table.

@Entity
@Table(name = "user")
public class User extends BaseEntity {

  @Column(name = "email", length = 255, nullable = false, unique = true)
  private String email;

  @Column(name = "first_name", length = 255, nullable = false)
  private String firstName;

  @Column(name = "last_name", length = 255, nullable = false)
  private String lastName;

  @Column(name = "password", length = 255)
  private String password;

  @Column(name = "role", length = 255, nullable = false)
  private String rolesString;

  @Enumerated(EnumType.STRING)
  @Column(name = "sign_in_provider", length = 20)
  private SocialMediaService signInProvider;

  ...
}

UserRepository :Spring Data JPA 存储库,它将使我们能够在用户"实体上运行 CRUD 操作.

UserRepository : Spring Data JPA repository which will enable us to run CRUD operations on 'User' entity.

public interface UserRepository extends JpaRepository<User, String> {
  public User findByEmailAndStatus(String email,Status status);
  public User findByIdAndStatus(String id,Status status);
}

UserService :这个 spring 服务将用于创建一个新的用户帐户,将数据插入到 'user' 表中.

UserService : This spring service will be used to create a new user account inserting data to 'user' table.

@Service
public class UserService {
  private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);

  @Autowired
  private UserRepository repository;

  @Transactional
  public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException {
      LOGGER.debug("Registering new user account with information: {}", userAccountData);

      if (emailExist(userAccountData.getEmail())) {
          LOGGER.debug("Email: {} exists. Throwing exception.", userAccountData.getEmail());
          throw new DuplicateEmailException("The email address: " + userAccountData.getEmail() + " is already in use.");
      }

      LOGGER.debug("Email: {} does not exist. Continuing registration.", userAccountData.getEmail());

      User registered =User.newEntity();
      registered.setEmail(userAccountData.getEmail());
      registered.setFirstName(userAccountData.getFirstName());
      registered.setLastName(userAccountData.getLastName());
      registered.setPassword(null);
      registered.addRole(User.Role.ROLE_USER_WEB);
      registered.addRole(User.Role.ROLE_USER_REST);
      registered.addRole(User.Role.ROLE_USER_REST_MOBILE);

      if (userAccountData.isSocialSignIn()) {
          registered.setSignInProvider(userAccountData.getSignInProvider());
      }

      LOGGER.debug("Persisting new user with information: {}", registered);

      return repository.save(registered);
  }
  .... 
}

SimpleSocialUserDetailsS​​ervice :SocialAuthenticationProvider 将使用此 Spring 服务来验证用户的 userId.

SimpleSocialUserDetailsService : This Spring service Will be used by SocialAuthenticationProvider to authenticate userId of the user.

@Service
public class SimpleSocialUserDetailsService implements SocialUserDetailsService {
  private static final Logger LOGGER = LoggerFactory.getLogger(SimpleSocialUserDetailsService.class);
  @Autowired
  private UserRepository repository;

  @Override
  public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException, DataAccessException {
      LOGGER.debug("Loading user by user id: {}", userId);

      User user = repository.findByEmailAndStatus(userId, Status.ENABLED);
      LOGGER.debug("Found user: {}", user);

      if (user == null) {
          throw new UsernameNotFoundException("No user found with username: " + userId);
      }

      ThingabledUserDetails principal = new ThingabledUserDetails(user.getEmail(),user.getPassword(),user.getAuthorities());
      principal.setFirstName(user.getFirstName());
      principal.setId(user.getId());
      principal.setLastName(user.getLastName());
      principal.setSocialSignInProvider(user.getSignInProvider());


      LOGGER.debug("Found user details: {}", principal);

      return principal;
  }
} 

您可以从以下位置下载代码示例:

You can download the code sample from :

https://github.com/ozgengunay/FBSpringSocialRESTAuth