Code ví dụ Spring MVC Security đăng nhập bằng LinkedIn
NỘI DUNG BÀI VIẾT
Các công nghệ sử dụng:
- Spring 5.0.2.RELEASE
- Spring Security 5.0.2.RELEASE
- Maven
- Tomcat
- JDK 1.8
- Eclipse + Spring Tool Suite
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 Maven Project
Thư viện sử dụng:
pom.xml
<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>
Code language: HTML, XML (xml)
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
spring-mvc-servlet.xml
<?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>
Code language: HTML, XML (xml)
File cấu hình Spring Security
spring-security.xml
<?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>
Code language: HTML, XML (xml)
File controller
BaseController.java
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";
}
}
Code language: JavaScript (javascript)
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:
LinkedInUtils.java
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;
}
}
Code language: JavaScript (javascript)
File LinkedInUser.java
Dùng để chứa thông tin tài khoản gửi về từ LinkedIn
LinkedInUser.java
package stackjava.com.springmvclinkedin.common;
public class LinkedInUser {
private String id;
private String name;
// getter - setter
}
Code language: PHP (php)
File application.properties
Chứa các thông tin về ứng dụng linkedin
application.properties
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
Code language: JavaScript (javascript)
Các file views
login.jsp
<%@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>
Code language: HTML, XML (xml)
Đườ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.
user.jsp
<%@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>
Code language: HTML, XML (xml)
admin.jsp
<%@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>
Code language: HTML, XML (xml)
403.jsp
<%@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>
Code language: HTML, XML (xml)
Demo
Đăng nhập bình thường với tài khoản kai/123456
Đăng nhập bằng tài khoản LinkedIn
Trường hợp từ chối cho phép ứng dụng truy cập tài khoản
Trường hợp đồng cho phép ứng dụng truy cập tài khoản
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 stackjava.com
Okay, Done!
Download code ví dụ trên tại đây.
Nguồn: https://stackjava.com/spring/code-vi-du-spring-mvc-security-dang-nhap-bang-linkedin.html
Leave a Reply