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:
- 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:
<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
Đă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.
Code ví dụ Spring MVC Security, login bằng Facebook
Code ví dụ Spring MVC đăng nhập bằng google/gmail
References.