Single sign-on system implementation based on SpringBoot
Today’s dry goods are a little wet, with my tears in them. Maybe only code can give me temporary peace. In this chapter you will learn the difference between a single sign-on system and a traditional login system, the design of a single sign-on system, the Design of a Single sign-on system, the design of a single sign-on system, the design of a single sign-on system, the design of a single sign-on system, the design of a single sign-on system, the design of a single sign-on system. What are you waiting for? Roll up your sleeves and get to work!
Effect: Port 8081 is the SSO system, and the other two 8082 and 8083 ports simulate the two systems. After successful login, check whether there are values in the Redis database.
Technical: SpringBoot, SpringMVC, Spring, SpringData, Redis, HttpClient At the bottom of the source: see article SpringBoot introductory: www.cnblogs.com/itdrag…
Introduction to single sign-on system
In a traditional system, or a system with only one server. Session In a server, each module can be directly accessed. You only need to log in to each module once. In a server cluster or distributed system architecture where sessions are not shared between each server, it is possible to log in to each module. At this time, it is necessary to save user information in Redis database through Single sign-on system to achieve the effect of Session sharing. In this way, you can access all trusted application systems with one login.
Single sign-on system implementation
The Maven project core configuration file pom.xml requires the addition of httpClient and Jedis JAR packages
<dependency> <! - HTTP client version is 4.5.3 - > < groupId >. Org. Apache httpcomponents < / groupId > < artifactId > httpclient < / artifactId > </dependency> <dependency> <! Clients </groupId> <artifactId>jedis</artifactId> </dependency>Copy the code
Spring4 Java configuration mode
Here, we need to integrate HttpClient for communication between services (or okHTTP). You also need to integrate Redis to store user information (Session sharing). Before Spring3.x, it was common to use XML for basic configuration of applications, such as data sources, resource files, etc. Business development annotations such as Component, Service, Controller, etc. Java configuration is already available in Spring3.x. Both Spring4.x and SpringBoot are now recommending Java configuration for beans. It makes the bean structure clearer.
Integration of HttpClient
HttpClient is a subproject of Apache Jakarta Common that provides an efficient, up-to-date, feature-rich client programming toolkit that supports the latest versions and recommendations of the HTTP protocol. HttpClient4.5 series: blog.csdn.net/column/d…
Start by creating the httpClient.properties configuration file in the SRC /main/resources directory
HTTP. MaxTotal =300. HTTP. ConnectTimeout =1000 # set from the connection pool to obtain the maximum time to connect HTTP. ConnectionRequestTimeout = 500 # set the maximum time HTTP data transmission. SocketTimeout = 10000Copy the code
Then in the SRC/main/Java/com/itdragon/config directory to create HttpclientSpringConfig. Java file Here with the help of four important annotation @ the Configuration: Applied to a method, indicating that the method is equivalent to < Bean > in the XML configuration. Note the naming convention for the method name @propertysource: {” XXX: XXX “,” XXX: XXX “},ignoreResourceNotFound=true If the file does not exist, @value: gets the value of the configuration file
package com.itdragon.config; import java.util.concurrent.TimeUnit; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.IdleConnectionEvictor; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.Scope; /** * @configuration applies to classes, which is equivalent to an XML Configuration file * @bean for methods, Equivalent to <bean> * @propertysource in XML Configuration specifies the Configuration file to read * @value Gets the Value of the Configuration file */ @configuration@propertysource (Value = "classpath:httpclient.properties") public class HttpclientSpringConfig { @Value("${http.maxTotal}") private Integer httpMaxTotal; @Value("${http.defaultMaxPerRoute}") private Integer httpDefaultMaxPerRoute; @Value("${http.connectTimeout}") private Integer httpConnectTimeout; @Value("${http.connectionRequestTimeout}") private Integer httpConnectionRequestTimeout; @Value("${http.socketTimeout}") private Integer httpSocketTimeout; @Autowired private PoolingHttpClientConnectionManager manager; @Bean public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() { PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(); . / / maximum number of connections poolingHttpClientConnectionManager setMaxTotal (httpMaxTotal); The maximum number of concurrent / / each host poolingHttpClientConnectionManager. SetDefaultMaxPerRoute (httpDefaultMaxPerRoute); return poolingHttpClientConnectionManager; Public IdleConnectionEvictor IdleConnectionEvictor() {return new IdleConnectionEvictor(manager, 1L, TimeUnit.HOURS); Scope ="prototype": @scope ("prototype") public CloseableHttpClient closeableHttpClient() { return HttpClients.custom().setConnectionManager(this.manager).build(); } @bean public RequestConfig RequestConfig () {return Requestconfig.custom ().setConnectTimeout(httpConnectTimeout) // The maximum time to create a connection . SetConnectionRequestTimeout (httpConnectionRequestTimeout) / / from the connection pool to obtain the maximum time to connect the setSocketTimeout (httpSocketTimeout) / / The maximum time for data transfer.build(); }}Copy the code
Integrate Redis
SpringBoot actually provides a Spring-boot-starter-redis POM to help us develop quickly, but we can also customize the configuration so that we can control it more easily. Redis series: www.cnblogs.com/itdrag…
Create a redis. Properties configuration file in SRC /main/resources to set the IP address and port number of the redis host, as well as the key stored in the Redis database and the lifetime. In order to facilitate testing, the survival time is set to be small. The configuration here is singleton Redis.
Redis. Node. The host = 192.168.225.131 redis. Node. The port = 6379 REDIS_USER_SESSION_KEY = REDIS_USER_SESSION SSO_SESSION_EXPIRE = 30Copy the code
In SRC/main/Java/com/itdragon/config directory to create RedisSpringConfig. Java file
package com.itdragon.config; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.JedisShardInfo; import redis.clients.jedis.ShardedJedisPool; @Configuration @PropertySource(value = "classpath:redis.properties") public class RedisSpringConfig { @Value("${redis.maxTotal}") private Integer redisMaxTotal; @Value("${redis.node.host}") private String redisNodeHost; @Value("${redis.node.port}") private Integer redisNodePort; private JedisPoolConfig jedisPoolConfig() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(redisMaxTotal); return jedisPoolConfig; } @bean public JedisPool getJedisPool(){// omit the first argument to use Protocol.DEFAULT_DATABASE JedisPool JedisPool = new JedisPool(jedisPoolConfig(), redisNodeHost, redisNodePort); return jedisPool; } @Bean public ShardedJedisPool shardedJedisPool() { List<JedisShardInfo> jedisShardInfos = new ArrayList<JedisShardInfo>(); jedisShardInfos.add(new JedisShardInfo(redisNodeHost, redisNodePort)); return new ShardedJedisPool(jedisPoolConfig(), jedisShardInfos); }}Copy the code
The Service layer
In the SRC/main/Java/com/itdragon/service directory to create UserService. Java file, it is responsible for three things: the first thing to validate user information is correct, will be successful login and user information saved to Redis database. The second event is responsible for determining whether the user’s token is expired or not, and refreshing the token lifetime if not. The third event: responsible for deleting user information from Redis database. Here used some tool classes, does not affect learning, can be obtained directly from the source code.
package com.itdragon.service; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import com.itdragon.pojo.ItdragonResult; import com.itdragon.pojo.User; import com.itdragon.repository.JedisClient; import com.itdragon.repository.UserRepository; import com.itdragon.utils.CookieUtils; import com.itdragon.utils.ItdragonUtils; import com.itdragon.utils.JsonUtils; @Service @Transactional @PropertySource(value = "classpath:redis.properties") public class UserService { @Autowired private UserRepository userRepository; @Autowired private JedisClient jedisClient; @Value("${REDIS_USER_SESSION_KEY}") private String REDIS_USER_SESSION_KEY; @Value("${SSO_SESSION_EXPIRE}") private Integer SSO_SESSION_EXPIRE; public ItdragonResult userLogin(String account, String password, HttpServletRequest request, HttpServletResponse response) {/ / whether the account password correct User User. = userRepository findByAccount (account); if (! ItdragonUtils. DecryptPassword (user, password)) {return ItdragonResult. Build (400), "account name or password error"); } // Generate token String token = uuid.randomuuid ().toString(); String userPassword = user.getPassword(); String userPassword = user.getPassword(); String userSalt = user.getSalt(); user.setPassword(null); user.setSalt(null); Redis jedisclient. set(REDIS_USER_SESSION_KEY + ":" + token, jsonutils.objecttojson (user)); // The user is already a persistent object and stored in the session cache. If the user changes the attribute value again, the Hibernate object will compare the current user object with the user object stored in the session cache when the transaction is committed. If the two objects are the same, Otherwise, an UPDATE statement will be issued. user.setPassword(userPassword); user.setSalt(userSalt); Expire (REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE); // Add logic to write cookies. Cookies expire when the browser is closed. CookieUtils.setCookie(request, response, "USER_TOKEN", token); // Return token itdragonresult. ok(token); } public void logout(String token) { jedisClient.del(REDIS_USER_SESSION_KEY + ":" + token); } public ItdragonResult queryUserByToken(String token) {// Query user information from Redis based on the token String json = jedisClient.get(REDIS_USER_SESSION_KEY + ":" + token); If (stringutils.isempty (json)) {return itDragonresult. build(400, "this session has expired, please log in again "); } // Update expiration time jedisclient.expire (REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE); Return itdragonresult. ok(jsonutils.jsontopojo (json, user.class)); }}Copy the code
The Controller layer
Redirects the login page
package com.itdragon.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class PageController { @RequestMapping("/login") public String showLogin(String redirect, Model model) { model.addAttribute("redirect", redirect); return "login"; }}Copy the code
Responsible for user login, logout, get the token operation
package com.itdragon.controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import com.itdragon.pojo.ItdragonResult; import com.itdragon.service.UserService; @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @RequestMapping(value="/login", method=RequestMethod.POST) @ResponseBody public ItdragonResult userLogin(String username, String password, HttpServletRequest request, HttpServletResponse response) { try { ItdragonResult result = userService.userLogin(username, password, request, response); return result; } catch (Exception e) { e.printStackTrace(); return ItdragonResult.build(500, ""); } } @RequestMapping(value="/logout/{token}") public String logout(@PathVariable String token) { userService.logout(token); Return "index"; return "index"; } @RequestMapping("/token/{token}") @ResponseBody public Object getUserByToken(@PathVariable String token) { ItdragonResult result = null; try { result = userService.queryUserByToken(token); } catch (Exception e) { e.printStackTrace(); result = ItdragonResult.build(500, ""); } return result; }}Copy the code
The view layer
A simple login page
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <! Doctype HTML > < HTML lang=" en "> <head> <meta name="viewport" content="initial-scale=1.0, width=device-width, user-scalable=no" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,Chrome=1" /> <meta http-equiv="X-UA-Compatible" <link type="image/x-icon" href="images/favicon.ico" rel="shortcut icon"> <link rel="stylesheet" href="static/css/main.css" /> </head> <body> <div class="wrapper"> <div class="container"> <h1>Welcome</h1> <form method="post" onsubmit="return false;" class="form"> <input type="text" value="itdragon" name="username" placeholder="Account"/> <input type="password" value="123456789" name="password" placeholder="Password"/> <button type="button" id="login-button">Login</button> </form> </div> <ul class="bg-bubbles"> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> < li > < / li > < li > < / li > < / ul > < / div > < script type = "text/javascript" SRC = "static/js/jquery - 1.10.1. Min. Js" > < / script > < script type="text/javascript"> var redirectUrl = "${redirect}"; Function doLogin() {$.post("/user/login", $(".form").serialize(),function(data){ if (data.status == 200) { if (redirectUrl == "") { location.href = "http://localhost:8082"; } else { location.href = redirectUrl; }} else {alert(" login failed because: "+ data.msg); }}); } $(function(){ $("#login-button").click(function(){ doLogin(); }); }); </script> </body> </html>Copy the code
HttpClient basic syntax
This encapsulates the methods of get and POST requests
package com.itdragon.utils; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; Public class HttpClientUtil {public static String doGet(String URL) {// No arguments get request return doGet(url, null); } public static String doGet(String url, Map<String, String > param) {/ / arguments get request CloseableHttpClient httpClient. = HttpClients createDefault (); // create a default closed Httpclient object. String resultMsg = ""; // Set the return value CloseableHttpResponse Response = null; URIBuilder = new URIBuilder(url); // Define HttpResponse object try {URIBuilder builder = new URIBuilder(url); If (param! = null) { for (String key : param.keySet()) { builder.addParameter(key, param.get(key)); } } URI uri = builder.build(); HttpGet httpGet = new HttpGet(uri); // Create an HTTP GET request. Response = httpClient.execute(httpGet); If (response.getStatusLine().getStatusCode() == 200) {response.getStatusLine().getStatusCode() == 200) {resultMsg = EntityUtils.toString(response.getEntity(), "UTF-8"); } } catch (Exception e) { e.printStackTrace(); } finally {// Don't forget to close try {if (response! = null) { response.close(); } httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } return resultMsg; } public static String doPost(String url) {// No parameter post request return doPost(url, null); } public static String doPost(String url, Map<String, String > param) {/ / take a post request CloseableHttpClient httpClient. = HttpClients createDefault (); // Create a default closeable Httpclient object CloseableHttpResponse Response = null; String resultMsg = ""; try { HttpPost httpPost = new HttpPost(url); // Create an Http Post request if (param! ArrayList<NameValuePair> paramList = new ArrayList<NameValuePair>(); for (String key : param.keySet()) { paramList.add(new BasicNameValuePair(key, param.get(key))); } UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList); // mock form httpPost.setentity (entity); } response = httpClient.execute(httpPost); If (response.getStatusLine().getStatusCode() == 200) {resultMsg = EntityUtils.toString(response.getEntity(), "utf-8"); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (response ! = null) { response.close(); } httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } return resultMsg; } public static String doPostJson(String url, String json) { CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { HttpPost httpPost = new HttpPost(url); StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON); httpPost.setEntity(entity); response = httpClient.execute(httpPost); if (response.getStatusLine().getStatusCode() == 200) { resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (response ! = null) { response.close(); } httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; }}Copy the code
Spring custom interceptors
Here is another project itdragon – service – test – sso in the code, first in the SRC/main/resources/spring/for springmvc XML configuration interceptors, set the request to intercept
<! < MVC :interceptors> < MVC :interceptor> < MVC :mapping path="/github/**"/> <bean class="com.itdragon.interceptors.UserLoginHandlerInterceptor"/> </mvc:interceptor> </mvc:interceptors>Copy the code
Then in SRC/main/Java/com/itdragon interceptors create UserLoginHandlerInterceptor directory. The Java file
package com.itdragon.interceptors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import com.itdragon.pojo.User; import com.itdragon.service.UserService; import com.itdragon.utils.CookieUtils; public class UserLoginHandlerInterceptor implements HandlerInterceptor { public static final String COOKIE_NAME = "USER_TOKEN"; @Autowired private UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = CookieUtils.getCookieValue(request, COOKIE_NAME); User user = this.userService.getUserByToken(token); If (StringUtils. IsEmpty (token) | | null = = user) {/ / jump to the login page, the user requested url passed as a parameter to the login page. response.sendRedirect("http://localhost:8081/login? redirect=" + request.getRequestURL()); // return false return false; } // Put user information into Request request.setAttribute("user", user); // The return value determines whether the handler executes. True: execute the command. False: disable the command. return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }Copy the code
Possible problems
Problems with automatic SpringData updates
SpringData is based on Hibernate. When a User is already a persistent object, it is stored in the session cache. If the User changes the attribute value again, the Hibernate object will compare the current User object to the User object stored in the session cache when the transaction is committed. If the two objects are the same, no update statement will be sent; otherwise, an Update statement will be issued. I took the silly approach of restoring the data before committing the transaction. If you have a better way, please let me know, thank you! Refer to the blog: www.cnblogs.com/xiaolu…
Check whether the user information is saved
After the login is successful, access the Redis client to check whether the user information is saved successfully. You can also delete this key for testing purposes.
[root@localhost bin]#./redis-cli -h 192.168.225.131 -p 6379 192.168.225.131:6379> 192.168.225.131:6379> keys * 1) "D869ac0 REDIS_USER_SESSION: 1-3 e22 d22-4 - bca0 37 c8dfade9ad" 192.168.225.131:6379 > get REDIS_USER_SESSION:1d869ac0-3d22-4e22-bca0-37c8dfade9ad "{\"id\":3,\"account\":\"itdragon\",\"userName\":\"ITDragonGit\",\"plainPassword\":null,\"password\":null,\"salt\":null, \"iphone\":\"12349857999\",\"email\":\"[email protected]\",\"platform\":\"github\",\"createdDate\":\"2017-12-22 21:11:19\",\"updatedDate\":\"2017-12-22 21:11:19\"}"Copy the code
conclusion
1 Single sign-on system realizes shared Session effect by putting user information in Redis database. 2 Java Configuration uses four annotations @configuration@bean@propertysource@value. 3 Spring interceptor setup. Merry Christmas to you all
Source: github.com/ITDragonBl….
At this point, the single sign-on system based on SpringBoot is over. Please point out any errors.