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.