STACKJAVA

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

Cấu trúc Project

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:

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

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

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

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

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#