Code ví dụ Spring Boot Security đăng nhập bằng Linkedin

Code ví dụ Spring Boot Security đăng nhập bằng Linkedin

(Xem lại: Code ví dụ JSP Servlet đăng nhập (login) bằng Linkedin)

Xem thêm:

Code ví dụ Spring Boot Security login bằng Google (Gmail)

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

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

Tạo Spring Boot Project

Spring Boot Starter Project

Code ví dụ spring boot đăng nhập bằng linkedin

ví dụ đăng nhập linkedin với spring boot

Cấu trúc Project

project spring boot login linkedin

Thư viện sử dụng

Mình sử dụng thêm thư viện httpcomponents để gửi request bên trong code java.

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

Tạo ứng dụng/app trên linkedin để sử dụng chức năng login

Ở đây mình tạo ứng dụng “stackajava.com-SpringBoot” với:

  • Client ID = 81wqf7krhgqevu
  • Client Secret = kfgIdotQioQxG99f

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

Tạo ứng dụng/app trên linkedin để sử dụng chức năng login

File application.properties

linkedin.client.id=81wqf7krhgqevu
linkedin.client.secret=kfgIdotQioQxG99f
linkedin.redirect.uri=http://localhost:8080/login-linkedin
linkedin.link.get.token=https://www.linkedin.com/oauth/v2/accessToken
linkedin.link.get.user_info=https://api.linkedin.com/v1/people/~?format=json&oauth2_access_token=
linkedin.grant_type=authorization_code

File cấu hình Spring Security

package stackjava.com.sblinkedin.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.sblinkedin.controller;

import java.io.IOException;

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.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import stackjava.com.sblinkedin.common.LinkedInUtils;

@Controller
public class BaseController {
  
  @Autowired
  private LinkedInUtils linkedInUtils;

    @RequestMapping(value = { "/", "/login" })
    public String login(@RequestParam(required = false) String message, final Model model) {
      if (message != null && !message.isEmpty()) {
        if (message.equals("logout")) {
          model.addAttribute("message", "Logout!");
        }
        if (message.equals("error")) {
          model.addAttribute("message", "Login Failed!");
        }
        if (message.equals("linkedin_error")) {
          model.addAttribute("message", "Login by LinkedIn Failed!");
        }
      }
      return "login";
    }

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

    String accessToken = linkedInUtils.getToken(code);
    
    stackjava.com.sblinkedin.common.User user = linkedInUtils.getUserInfo(accessToken);
    UserDetails userDetail = linkedInUtils.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 loginLinkedIn xử lý kết quả trả về từ LinkedIn

  • Lấy code mà LinkedIn gửi về sau đó đổi code sang access token
  • Sử dụng access token lấy thông tin user (có thể thực hiện lưu lại thông tin vào database để quản lý)
  • Chuyển thông tin user sang đối tượng UserDetails để spring security quản lý
  • Sử dụng đối tượng UserDetails trên giống như thông tin authentication (tương đương với đăng nhập bằng username/password)

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

package stackjava.com.sblinkedin.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.Form;
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;

@Component
public class LinkedInUtils {

  @Autowired
  private Environment env;

  public String getToken(final String code) throws ClientProtocolException, IOException {
    String link = env.getProperty("linkedin.link.get.token");
    String response = Request.Post(link)
        .bodyForm(Form.form().add("client_id", env.getProperty("linkedin.client.id"))
            .add("client_secret", env.getProperty("linkedin.client.secret"))
            .add("redirect_uri", env.getProperty("linkedin.redirect.uri")).add("code", code)
            .add("grant_type", env.getProperty("linkedin.grant_type")).build())
        .execute().returnContent().asString();

    ObjectMapper mapper = new ObjectMapper();
    JsonNode node = mapper.readTree(response).get("access_token");
    return node.textValue();
  }

  public stackjava.com.sblinkedin.common.User getUserInfo(final String accessToken) throws ClientProtocolException, IOException {
    String link = env.getProperty("linkedin.link.get.user_info") + accessToken;
    String response = Request.Get(link).execute().returnContent().asString();
    ObjectMapper mapper = new ObjectMapper();
    JsonNode jsonNode = mapper.readTree(response);
    String id = jsonNode.get("id").textValue();
    String firstName = jsonNode.get("firstName").textValue();
    String lastName = jsonNode.get("lastName").textValue();
    String name = firstName + " " + lastName;
    stackjava.com.sblinkedin.common.User user = new stackjava.com.sblinkedin.common.User(id, name);
    return user;

  }
  
  public UserDetails buildUser(stackjava.com.sblinkedin.common.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.getName(),
        "", enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    return userDetail;
  }

}

 File User.java

Dùng để chứa thông tin tài khoản gửi về từ LinkedIn

package stackjava.com.sblinkedin.common;

public class User {
  private String id;
  private String name;

  // getter - setter

}

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 Login with LinkedIn</h2>
  <span th:text="${message}" style="color: red;"></span> <br/>
  <a href="https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=81wqf7krhgqevu
  &redirect_uri=http://localhost:8080/login-linkedin&scope=r_basicprofile">Login With LinkedIn</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.linkedin.com/oauth/v2/authorization?...scope=r_basicprofile dùng để gọi hộp thoại đăng nhập và cài đặt chuyển hướng URL.

<!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

Code ví dụ Spring Boot Security đăng nhập bằng Linkedin

Code ví dụ Spring Boot Security đăng nhập bằng Linkedin

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

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

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

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

Code ví dụ Spring Boot Security đăng nhập bằng Linkedin stackjava.com

Okay, Done!

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

 

References.

https://developer.linkedin.com/docs/signin-with-linkedin#

stackjava.com