STACKJAVA

Code ví dụ Spring Boot Security login bằng Facebook

Code ví dụ Spring Boot Security login bằng Facebook.

(Xem lại bài Code ví dụ JSP Servlet login bằng Facebook (Java Web) để hiểu nguyên lý đăng nhập bằng tài khoản facebook)

Ở bài này mình cũng sẽ không dùng Facebook SDK hay Spring Social mà sẽ tạo thủ công quy trình đăng nhập ứng dụng bằng facebook để mọi người hiểu rõ nguyên lý và luồng chạy.

Các công nghệ sử dụng:

Tạo Spring Boot Project

Cấu trúc Project

Thư viện sử dụng:

Mình sử dụng thêm thư viện httpcomponents và restfb để gửi request bên trong controller tới facebook.

<!-- org.apache.httpcomponents -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
</dependency>

<!-- restfb -->
<dependency>
<groupId>com.restfb</groupId>
<artifactId>restfb</artifactId>
<version>2.3.0</version>
</dependency>

Thiết lập https

Tạo keystore để thiết lập https cho ứng dụng spring boot

Ở đây mình tạo keystore là springboot.keystore với alias là tomcat và password là admin1234

Tạo ứng dụng trên facebook

Ở đây mình tạo ứng dụng trên facebook để thực hiện chức năng login với app id = ‘180439422588509’ và key = ‘3e808bc29b78df7f46725782c206d9b0’

(Xem lại: Tạo ứng dụng facebook để đăng nhập thay tài khoản)

File application.properties

# config SSL/TLS
server.port: 8443
server.ssl.key-store: src/main/resources/springboot.keystore
server.ssl.key-store-password: admin1234
server.ssl.keyAlias: tomcat

# config facebook social
facebook.app.id=180439422588509
facebook.app.secret=3e808bc29b78df7f46725782c206d9b0
facebook.redirect.uri=https://localhost:8443/login-facebook
facebook.link.get.token=https://graph.facebook.com/oauth/access_token?client_id=%s&client_secret=%s&redirect_uri=%s&code=%s

File cấu hình security:

package stackjava.com.sbfacebook.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        return bCryptPasswordEncoder;
    }
  
  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().passwordEncoder(passwordEncoder()).
      withUser("kai").password("$2a$04$Q2Cq0k57zf2Vs/n3JXwzmerql9RzElr.J7aQd3/Sq0fw/BdDFPAj.").roles("ADMIN");
    auth.inMemoryAuthentication().passwordEncoder(passwordEncoder()).
      withUser("sena").password("$2a$04$Q2Cq0k57zf2Vs/n3JXwzmerql9RzElr.J7aQd3/Sq0fw/BdDFPAj.").roles("USER");
//		auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance()).withUser("sena").password("123456").roles("USER");
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    // Chỉ cho phép user có quyền ADMIN truy cập đường dẫn /admin/**
    http.authorizeRequests().antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')");

    // Chỉ cho phép user có quyền ADMIN hoặc USER truy cập đường dẫn /user/**
    http.authorizeRequests().antMatchers("/user/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')");

    // Khi người dùng đã login, với vai trò USER, Nhưng truy cập vào trang yêu cầu vai trò ADMIN, sẽ chuyển hướng tới trang /403
    http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/403");

    // Cấu hình cho Login Form.
    http.authorizeRequests().and().formLogin()//
        .loginProcessingUrl("/j_spring_security_login")//
        .loginPage("/login")//
        .defaultSuccessUrl("/user")//
        .failureUrl("/login?message=error")//
        .usernameParameter("username")//
        .passwordParameter("password")
        // Cấu hình cho Logout Page.
        .and().logout().logoutUrl("/j_spring_security_logout").logoutSuccessUrl("/login?message=logout");

  }
}

File controller:

package stackjava.com.sbfacebook.controller;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;

import org.apache.http.client.ClientProtocolException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import stackjava.com.sbfacebook.common.RestFB;

@Controller
public class BaseController {
  
  @Autowired
  private RestFB restFb;

  @RequestMapping(value = { "/", "/login" })
  public String login() {
    return "login";
  }

  @RequestMapping("/login-facebook")
  public String loginFacebook(HttpServletRequest request) throws ClientProtocolException, IOException {
    String code = request.getParameter("code");
    
    if (code == null || code.isEmpty()) {
      return "redirect:/login?facebook=error";
    }

    String accessToken = restFb.getToken(code);
    
    com.restfb.types.User user = restFb.getUserInfo(accessToken);
    UserDetails userDetail = restFb.buildUser(user);
    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetail, null,
        userDetail.getAuthorities());
    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    SecurityContextHolder.getContext().setAuthentication(authentication);
    return "redirect:/user";
  }

  @RequestMapping("/user")
  public String user() {
    return "user";
  }

  @RequestMapping("/admin")
  public String admin() {
    return "admin";
  }

  @RequestMapping("/403")
  public String accessDenied() {
    return "403";
  }
}

Method loginFacebook xử lý kết quả trả về từ facebook

File truy vấn, gửi request tới facebook:

package stackjava.com.sbfacebook.common;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.fluent.Request;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.restfb.DefaultFacebookClient;
import com.restfb.FacebookClient;
import com.restfb.Version;

@Component
public class RestFB {

  @Autowired
  private Environment env;

  public String getToken(final String code) throws ClientProtocolException, IOException {
    String link = String.format(env.getProperty("facebook.link.get.token"), env.getProperty("facebook.app.id"),
        env.getProperty("facebook.app.secret"), env.getProperty("facebook.redirect.uri"), code);
    String response = Request.Get(link).execute().returnContent().asString();
    ObjectMapper mapper = new ObjectMapper();
    JsonNode node = mapper.readTree(response).get("access_token");
    return node.textValue();
  }

  public com.restfb.types.User getUserInfo(final String accessToken) {
    FacebookClient facebookClient = new DefaultFacebookClient(accessToken, env.getProperty("facebook.app.secret"),
        Version.LATEST);
    return facebookClient.fetchObject("me", com.restfb.types.User.class);
  }
  
  public UserDetails buildUser(com.restfb.types.User user) {
    boolean enabled = true;
    boolean accountNonExpired = true;
    boolean credentialsNonExpired = true;
    boolean accountNonLocked = true;
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
    UserDetails userDetail = new User(user.getId() + user.getName(), "", enabled, accountNonExpired, credentialsNonExpired,
        accountNonLocked, authorities);
    return userDetail;
  }

}

Nếu không dùng thư viện RestFB thì bạn có thể dùng URL sau để lấy thông tin của user:
https://graph.facebook.com/me?access_token=... thông tin trả về sẽ gồm id và name.

(Bạn cũng có thể lấy thêm nhiều trường khác như email, comment, image… nhưng cần phải có thêm permission, ở đây mình chỉ thực hiện đăng nhập nên chỉ cần permission để lấy public profile)

 Các file views:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Boot Security Hello</title>
</head>
<body>
  <h2>Spring Boot Security Hello</h2>
  <a href="https://www.facebook.com/dialog/oauth?client_id=180439422588509&redirect_uri=https://localhost:8443/login-facebook">Login Facebook</a>
  <br/>
  <form name='login-form' th:action="@{/j_spring_security_login}" method='POST'>
    <table>
      <tr>
        <td>Username:</td>
        <td><input type='text' name='username' value=''></td>
      </tr>
      <tr>
        <td>Password:</td>
        <td><input type='password' name='password' /></td>
      </tr>
      <tr>
        <td><input name="submit" type="submit" value="submit" /></td>
      </tr>
    </table>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
  </form>
</body>
</html>

Đường link

https://www.facebook.com/dialog/oauth?client_id=180439422588509&redirect_uri=https://localhost:8443/login-facebook dùng để gọi hộp thoại Đăng nhập và cài đặt URL chuyển hướng.

<!DOCTYPE html>
<html>
<head>
<title>Spring Boot Security Hello</title>
</head>
<body>
  <h2>User Page</h2>
  <h3>
    Welcome : <span th:utext="${#request.userPrincipal.name}"></span>
  </h3>

  <a th:href="@{/admin}">Admin Page</a>

  <br/><br/>
  <form th:action="@{/j_spring_security_logout}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    <input type="submit" value="Logout" />
  </form>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Spring Boot Security Hello</title>
</head>
<body>
  <h2>Admin Page</h2>
  <h3>
    Hello : <span th:utext="${#request.userPrincipal.name}"></span>
  </h3>
  
  
  <a th:href="@{/user}">User Page</a>

  <br/><br/>
  <form th:action="@{/j_spring_security_logout}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    <input type="submit" value="Logout" />
  </form>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Spring Boot Security Hello</title>
</head>
<body>
  <h2>Access Denied Page - 403</h2>
  
  <h3>
    Hi : <span th:utext="${#request.userPrincipal.name} + ' - you do not have permission to access this page'"></span>
  </h3>

  <a th:href="@{/user}">User Page</a>

  <br/><br/>
  <form th:action="@{/j_spring_security_logout}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    <input type="submit" value="Logout" />
  </form>
</body>
</html>

Demo:

Đăng nhập bình thường bằng tài khoản kai/123456

  

Đăng nhập bằng tài khoản facebook

Trường hợp không cho phép đăng nhập bằng facebook

Trường hợp cho phép đăng nhập bằng facebook (sau khi cho phép, lần sau đăng nhập tiếp facebook sẽ không hỏi lại nữa)

Truy cập trang admin.html bị lỗi vì tài khoản đăng nhập bằng facebook chỉ có role user.

Code ví dụ Spring Boot Security login bằng Facebook stackjava.com

Okay, Done!

Download code ví dụ trên tại đây.

 

References:

https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow