Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

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

(Xem lại: Code ví dụ Spring Boot Security đăng nhập bằng Linkedin)

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

Tạo ứng dụng/app trên linkedin

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

  • Client ID = 81xomg6on7p1gw
  • Client Secret = hjdWKlDvKAiJfM9y

(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

Tạo Maven Project

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Thư viện sử dụng:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>stackjava.com</groupId>
  <artifactId>SpringMvcLinkedIn</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

  <properties>
    <spring.version>5.0.2.RELEASE</spring.version>
    <spring.security.version>5.0.2.RELEASE</spring.security.version>
    <jstl.version>1.2</jstl.version>

  </properties>

  <dependencies>
    <!-- Spring MVC -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- Spring Security -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>${spring.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>${spring.security.version}</version>
    </dependency>

    <!-- JSP/Servlet -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.2</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
    </dependency>

    <!-- jstl for jsp page -->
    <dependency>
      <groupId>jstl</groupId>
      <artifactId>jstl</artifactId>
      <version>${jstl.version}</version>
    </dependency>

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

    <!-- Jackson -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.3</version>
    </dependency>

  </dependencies>
</project>

Mình sử dụng thêm thư viện httpcomponents để gửi request bên trong code Java và jackson  để xử lý dữ liệu JSON

File cấu hình Spring MVC

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

  <context:component-scan base-package="stackjava.com.springmvclinkedin" />

  <bean
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix">
      <value>/WEB-INF/views/jsp/</value>
    </property>
    <property name="suffix">
      <value>.jsp</value>
    </property>
  </bean>

</beans>

File cấu hình Spring Security

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

  <http auto-config="true">
    <intercept-url pattern="/admin**" access="hasRole('ROLE_ADMIN')" />
    <intercept-url pattern="/user**" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')" />
    <access-denied-handler error-page="/403"/>
    <form-login
        login-page="/login"
        login-processing-url="/j_spring_security_login"
        default-target-url="/user"
      authentication-failure-url="/login?message=error"
      username-parameter="username"
      password-parameter="password" />
    <logout logout-url="/j_spring_security_logout"
      logout-success-url="/login?message=logout" delete-cookies="JSESSIONID" />
  </http>

  <authentication-manager>
    <authentication-provider>
      <user-service>
        <user name="kai" password="{noop}123456" authorities="ROLE_ADMIN" />
        <user name="sena" password="{noop}123456" authorities="ROLE_USER" />
      </user-service>
    </authentication-provider>
  </authentication-manager>
</beans:beans>

File controller

package stackjava.com.springmvclinkedin.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.springmvclinkedin.common.LinkedInUser;
import stackjava.com.springmvclinkedin.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);
    
    LinkedInUser 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.springmvclinkedin.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.context.annotation.PropertySource;
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
@PropertySource("classpath:application.properties")
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 LinkedInUser 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 = "linkedin - " + firstName + " " + lastName;
    LinkedInUser user = new LinkedInUser(id, name);
    return user;

  }
  
  public UserDetails buildUser(LinkedInUser 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 LinkedInUser.java

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

package stackjava.com.springmvclinkedin.common;

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

  // getter - setter

}

 File application.properties

Chứa các thông tin về ứng dụng linkedin

linkedin.client.id=81xomg6on7p1gw
linkedin.client.secret=hjdWKlDvKAiJfM9y
linkedin.redirect.uri=http://localhost:8080/SpringMvcLinkedIn/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

Các file views

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>login</title>
</head>
<body>
  <h2>Spring MVC Security - Login with LinkedIn</h2>
  <span style="color: red;">${message}</span> <br/>
  
  
  <a href="https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=81xomg6on7p1gw
  &redirect_uri=http://localhost:8080/SpringMvcLinkedIn/login-linkedin&scope=r_basicprofile">Login With LinkedIn</a>	

  <form name='loginForm' action="<c:url value='j_spring_security_login' />" method='POST'>
    <table>
      <tr>
        <td>User:</td>
        <td><input type='text' name='username'></td>
      </tr>
      <tr>
        <td>Password:</td>
        <td><input type='password' name='password' /></td>
      </tr>
      <tr>
        <td colspan='2'><input name="submit" type="submit" value="login" /></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.

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>User Page</title>
</head>
<body>
  <h1>User Page</h1>
  <h2>Welcome: ${pageContext.request.userPrincipal.name}</h2>
  <a href="<c:url value="/admin" />">Admin Page</a>
  <br/><br/>

  <form action="<c:url value="/j_spring_security_logout" />" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    <input type="submit" value="Logout" />
  </form>

</body>
</html>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>Admin Page</title>
</head>
<body>
  <h1>Admin Page</h1>
  <h2>Welcome: ${pageContext.request.userPrincipal.name}</h2>
  <a href="<c:url value="/user" />">User Page</a>
  <br/><br/>
  <form action="<c:url value="/j_spring_security_logout" />" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    <input type="submit" value="Logout" />
  </form>

</body>
</html>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>403</title>
</head>
<body>
  <h1>403</h1>
  <span>Hi: ${pageContext.request.userPrincipal.name} you do not have permission to access this page</span>
  <a href="<c:url value="/user" />">User Page</a>
  <br/><br/>
  <form action="<c:url value="/j_spring_security_logout" />" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    <input type="submit" value="Logout" />
  </form>

</body>
</html>

Demo

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

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

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

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Trường hợp từ chối cho phép ứng dụng truy cập tài khoản

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Trường hợp đồng cho phép ứng dụng truy cập tài khoản

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

Tài khoản đăng nhập bằng LinkedIn chỉ có role User nên không thể truy cập trang admin.

Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn

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

Okay, Done!

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

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

Code ví dụ Spring MVC đăng nhập bằng google/gmail

References.

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

stackjava.com