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:
- Spring Boot 2.0.1 + Spring Security
- Maven
- JDK 1.8
- Eclipse + Spring Tool Suite
- Thymeleaf
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:
- Client ID = 81wqf7krhgqevu
- Client Secret = kfgIdotQioQxG99f
(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
- 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
Đă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.










