基于SpringBoot實(shí)現(xiàn)單點(diǎn)登錄系統(tǒng)
來源 |? urlify.cn/I3eyAz
單點(diǎn)登錄系統(tǒng)設(shè)計(jì)思路:采用Spring4 Java配置方式整合HttpClient,Redis ,MySql和SpringBoot的簡(jiǎn)易教程。
在傳統(tǒng)的系統(tǒng),或者是只有一個(gè)服務(wù)器的系統(tǒng)中。Session在一個(gè)服務(wù)器中,各個(gè)模塊都可以直接獲取,只需登錄一次就進(jìn)入各個(gè)模塊。若在服務(wù)器集群或者是分布式系統(tǒng)架構(gòu)中,每個(gè)服務(wù)器之間的Session并不是共享的,這會(huì)出現(xiàn)每個(gè)模塊都要登錄的情況。這時(shí)候需要通過單點(diǎn)登錄系統(tǒng)(Single Sign On)將用戶信息存在Redis數(shù)據(jù)庫中實(shí)現(xiàn)Session共享的效果。從而實(shí)現(xiàn)一次登錄就可以訪問所有相互信任的應(yīng)用系統(tǒng)。
一、整合 HttpClient
HttpClient 是 Apache Jakarta Common 下的子項(xiàng)目,用來提供高效的、最新的、功能豐富的支持 HTTP 協(xié)議的客戶端編程工具包,并且它支持 HTTP 協(xié)議最新的版本和建議。
首先在src/main/resources 目錄下創(chuàng)建?httpclient.properties?配置文件。
#設(shè)置整個(gè)連接池默認(rèn)最大連接數(shù)
http.defaultMaxPerRoute=100
#設(shè)置整個(gè)連接池最大連接數(shù)
http.maxTotal=300
#設(shè)置請(qǐng)求超時(shí)
http.connectTimeout=1000
#設(shè)置從連接池中獲取到連接的最長(zhǎng)時(shí)間
http.connectionRequestTimeout=500
#設(shè)置數(shù)據(jù)傳輸?shù)淖铋L(zhǎng)時(shí)間
http.socketTimeout=10000
然后在 src/main/java/com/itdragon/config 目錄下創(chuàng)建?HttpclientSpringConfig.java?文件
這里用到了四個(gè)很重要的注解
@Configuration?: 作用于類上,指明該類就相當(dāng)于一個(gè)xml配置文件
@Bean?: 作用于方法上,指明該方法相當(dāng)于xml配置中的bean,注意方法名的命名規(guī)范
@PropertySource?: 指定讀取的配置文件,引入多個(gè)value={“xxx:xxx”,“xxx:xxx”},ignoreResourceNotFound=true 文件不存在時(shí)忽略
@Value?: 獲取配置文件的值
package?com.itdragon.config;
/**
?*?@Configuration??作用于類上,相當(dāng)于一個(gè)xml配置文件
?*?@Bean????作用于方法上,相當(dāng)于xml配置中的
?*?@PropertySource?指定讀取的配置文件,ignoreResourceNotFound=true?文件不存在是忽略
?*?@Value???獲取配置文件的值
?*/
@Configuration
@PropertySource(value?=?"classpath:httpclient.properties",?ignoreResourceNotFound=true)
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();
????????//?最大連接數(shù)
????????poolingHttpClientConnectionManager.setMaxTotal(httpMaxTotal);
????????//?每個(gè)主機(jī)的最大并發(fā)數(shù)
????????poolingHttpClientConnectionManager.setDefaultMaxPerRoute(httpDefaultMaxPerRoute);
????????return?poolingHttpClientConnectionManager;
????}
????@Bean?//?定期清理無效連接
????public?IdleConnectionEvictor?idleConnectionEvictor()?{
????????return?new?IdleConnectionEvictor(manager,?1L,?TimeUnit.HOURS);
????}
????@Bean?//?定義HttpClient對(duì)象?注意該對(duì)象需要設(shè)置scope="prototype":多例對(duì)象
????@Scope("prototype")
????public?CloseableHttpClient?closeableHttpClient()?{
????????return?HttpClients.custom().setConnectionManager(this.manager).build();
????}
????@Bean?//?請(qǐng)求配置
????public?RequestConfig?requestConfig()?{
????????return?RequestConfig.custom().setConnectTimeout(httpConnectTimeout)?//?創(chuàng)建連接的最長(zhǎng)時(shí)間
????????????????.setConnectionRequestTimeout(httpConnectionRequestTimeout)?//?從連接池中獲取到連接的最長(zhǎng)時(shí)間
????????????????.setSocketTimeout(httpSocketTimeout)?//?數(shù)據(jù)傳輸?shù)淖铋L(zhǎng)時(shí)間
????????????????.build();
????}
}
二、整合 Redis
SpringBoot官方其實(shí)提供了spring-boot-starter-redis pom 幫助我們快速開發(fā),但我們也可以自定義配置,這樣可以更方便地掌控。
首先在src/main/resources 目錄下創(chuàng)建 redis.properties 配置文件
redis.maxTotal=200
redis.node.host=10.128.15.21
redis.node.port=6379
REDIS_USER_SESSION_KEY=REDIS_USER_SESSION
SSO_SESSION_EXPIRE=30
設(shè)置Redis主機(jī)的ip地址和端口號(hào),和存入Redis數(shù)據(jù)庫中的key以及存活時(shí)間。這里為了方便測(cè)試,存活時(shí)間設(shè)置的比較小。這里的配置是單例Redis。
在src/main/java/com/itdragon/config 目錄下創(chuàng)建?RedisSpringConfig.java?文件。
@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(){?//?省略第一個(gè)參數(shù)則是采用?Protocol.DEFAULT_DATABASE
?????JedisPool?jedisPool?=?new?JedisPool(jedisPoolConfig(),?redisNodeHost,?redisNodePort);
????????return?jedisPool;
????}
????@Bean
????public?ShardedJedisPool?shardedJedisPool()?{
????????List?jedisShardInfos?=?new?ArrayList();
????????jedisShardInfos.add(new?JedisShardInfo(redisNodeHost,?redisNodePort));
????????return?new?ShardedJedisPool(jedisPoolConfig(),?jedisShardInfos);
????}
}
三、Service 層
在src/main/java/com/itdragon/service 目錄下創(chuàng)建 UserService.java 文件,它負(fù)責(zé)三件事情
第一件事情:驗(yàn)證用戶信息是否正確,并將登錄成功的用戶信息保存到Redis數(shù)據(jù)庫中。
第二件事情:負(fù)責(zé)判斷用戶令牌是否過期,若沒有則刷新令牌存活時(shí)間。
第三件事情:負(fù)責(zé)從Redis數(shù)據(jù)庫中刪除用戶信息。
package?com.itdragon.service;
@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?Result?registerUser(User?user)?{
?????//?檢查用戶名是否注冊(cè),一般在前端驗(yàn)證的時(shí)候處理,因?yàn)樽?cè)不存在高并發(fā)的情況,這里再加一層查詢是不影響性能的
?????if?(null?!=?userRepository.findByAccount(user.getAccount()))?{
??????return?Result.build(400,?"");
?????}
?????userRepository.save(user);
?????//?注冊(cè)成功后選擇發(fā)送郵件激活。現(xiàn)在一般都是短信驗(yàn)證碼
?????return?Result.build(200,?"");
????}
????
????public?Result?userLogin(String?account,?String?password,
???????HttpServletRequest?request,?HttpServletResponse?response)?{
?????//?判斷賬號(hào)密碼是否正確
??User?user?=?userRepository.findByAccount(account);
??if(user?==?null){
???return?Result.build(400,?"賬號(hào)名或密碼錯(cuò)誤");
??}
??if?(!CheckUtils.decryptPassword(user,?password))?{
???return?Result.build(400,?"賬號(hào)名或密碼錯(cuò)誤");
??}
??//?生成token
??String?token?=?UUID.randomUUID().toString();
??//?清空密碼和鹽避免泄漏
??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));
??// user 已經(jīng)是持久化對(duì)象了,被保存在了session緩存當(dāng)中,若user又重新修改了屬性值,那么在提交事務(wù)時(shí),此時(shí) hibernate對(duì)象就會(huì)拿當(dāng)前這個(gè)user對(duì)象和保存在session緩存中的user對(duì)象進(jìn)行比較,如果兩個(gè)對(duì)象相同,則不會(huì)發(fā)送update語句,否則,如果兩個(gè)對(duì)象不同,則會(huì)發(fā)出update語句。
??user.setPassword(userPassword);
??user.setSalt(userSalt);
??//?設(shè)置?session?的過期時(shí)間
??jedisClient.expire(REDIS_USER_SESSION_KEY?+?":"?+?token,?SSO_SESSION_EXPIRE);
??//?添加寫 cookie 的邏輯,cookie 的有效期是關(guān)閉瀏覽器就失效。
??CookieUtils.setCookie(request,?response,?"USER_TOKEN",?token);
??//?返回token
??return?Result.ok(token);
?}
????
????public?void?logout(String?token)?{
?????jedisClient.del(REDIS_USER_SESSION_KEY?+?":"?+?token);
????}
?public?Result?queryUserByToken(String?token)?{
??//?根據(jù)token從redis中查詢用戶信息
??String?json?=?jedisClient.get(REDIS_USER_SESSION_KEY?+?":"?+?token);
??//?判斷是否為空
??if?(StringUtils.isEmpty(json))?{
???return?Result.build(400,?"此session已經(jīng)過期,請(qǐng)重新登錄");
??}
??//?更新過期時(shí)間
??jedisClient.expire(REDIS_USER_SESSION_KEY?+?":"?+?token,?SSO_SESSION_EXPIRE);
??//?返回用戶信息
??return?Result.ok(JsonUtils.jsonToPojo(json,?User.class));
?}
}
四、Controller 層
負(fù)責(zé)跳轉(zhuǎn)登錄頁面跳轉(zhuǎn),負(fù)責(zé)用戶的登錄,退出,獲取令牌的操作。UserController.java和PageController.java
package?com.itdragon.controller;
@Controller
@RequestMapping("/user")
public?class?UserController?{
?@Autowired
?private?UserService?userService;
?@RequestMapping(value="/login",?method=RequestMethod.POST)
?@ResponseBody
?public?Result?userLogin(String?username,?String?password,
????????????????????????????HttpServletRequest?request,?HttpServletResponse?response)?{
??try?{
???Result?result?=?userService.userLogin(username,?password,?request,?response);
???return?result;
??}?catch?(Exception?e)?{
???e.printStackTrace();
???return?Result.build(500,?"");
??}
?}
?
?@RequestMapping(value="/logout/{token}")
?public?String?logout(@PathVariable?String?token)?{
??userService.logout(token);?//?思路是從Redis中刪除key,實(shí)際情況請(qǐng)和業(yè)務(wù)邏輯結(jié)合
??return?"back";
?}
?
?@RequestMapping("/token/{token}")
?@ResponseBody
?public?Object?getUserByToken(@PathVariable?String?token)?{
??Result?result?=?null;
??try?{
???result?=?userService.queryUserByToken(token);
??}?catch?(Exception?e)?{
???e.printStackTrace();
???result?=?Result.build(500,?"");
??}
??return?result;
?}
}
package?com.itdragon.controller;
@Controller
public?class?PageController?{
?@RequestMapping("/login")
?public?String?showLogin(String?redirect,?Model?model)?{
??model.addAttribute("redirect",?redirect);
??return?"login";
?}?
}
五、視圖層
一個(gè)簡(jiǎn)單的登錄頁面和資源展示頁面。login.jsp、index.jsp和indexHomePage.jsp
六、Spring 自定義攔截器
這里是另外一個(gè)項(xiàng)目 service-test-sso 中的代碼,首先在src/main/resources/spring/springmvc.xml 中配置攔截器,設(shè)置哪些請(qǐng)求需要攔截
?"com.it.controller"?/>
?
???class="org.springframework.web.servlet.view.InternalResourceViewResolver">
??"prefix"?value="/WEB-INF/views/"?/>
??"suffix"?value=".jsp"?/>
?
?
?"/WEB-INF/static/"?mapping="/static/**"/>
?
?
??
???"/indexHomePage/**"/>
???"com.it.interceptors.UserLoginHandlerInterceptor"/>
??
?
UserLoginHandlerInterceptor.java
package?com.it.interceptors;
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)?{
???//?跳轉(zhuǎn)到登錄頁面,把用戶請(qǐng)求的url作為參數(shù)傳遞給登錄頁面。
???response.sendRedirect("http://localhost:8081/login?redirect="?+?request.getRequestURL());
???//?返回false
???return?false;
??}
??//?把用戶信息放入Request
??request.setAttribute("user",?user);
??//?返回值決定handler是否執(zhí)行。true:執(zhí)行,false:不執(zhí)行。
??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?{
????}
}
七、操作步驟
測(cè)試思路:
第一步:注冊(cè)用戶,執(zhí)行sso 項(xiàng)目下SpringbootStudyApplicationTests.java 單元測(cè)試類中的 registerUser() 方法添加用戶。
第二步:開啟sso服務(wù)。
第三步:再開啟兩個(gè)service-test-sso服務(wù)。
第四步:在service-test-sso服務(wù)頁面點(diǎn)擊“訪問主頁”按鈕進(jìn)入權(quán)限頁面測(cè)試。
八、sso項(xiàng)目結(jié)構(gòu)

service-test-sso項(xiàng)目結(jié)構(gòu)? ?
訪問主頁
點(diǎn)擊登錄
用戶表存儲(chǔ)如下
依次通過訪問如下鏈接:
http://localhost:8083/
http://localhost:8081/login?redirect=/indexHomePage
http://localhost:8082/
然后直接就可以不用登錄就可以訪問資源了,實(shí)現(xiàn)SSO功能
推薦閱讀
