當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]單點(diǎn)登錄系統(tǒng)設(shè)計(jì)思路:采用Spring4 Java配置方式整合HttpClient,Redis ,MySql和SpringBoot的簡(jiǎn)易教程。

來(lái)源: urlify.cn/I3eyAz


單點(diǎn)登錄系統(tǒng)設(shè)計(jì)思路:采用Spring4 Java配置方式整合HttpClient,Redis ,MySql和SpringBoot的簡(jiǎn)易教程。
基于SpringBoot實(shí)現(xiàn)單點(diǎn)登錄系統(tǒng)
在傳統(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í)候需要通過(guò)單點(diǎn)登錄系統(tǒng)(Single Sign On)將用戶(hù)信息存在Redis數(shù)據(jù)庫(kù)中實(shí)現(xiàn)Session共享的效果。從而實(shí)現(xiàn)一次登錄就可以訪(fǎng)問(wèn)所有相互信任的應(yīng)用系統(tǒng)。

一、整合 HttpClient

HttpClient 是 Apache Jakarta Common 下的子項(xiàng)目,用來(lái)提供高效的、最新的、功能豐富的支持 HTTP 協(xié)議的客戶(hù)端編程工具包,并且它支持 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 : 作用于類(lèi)上,指明該類(lèi)就相當(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  作用于類(lèi)上,相當(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 // 定期清理無(wú)效連接
    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 幫助我們快速開(kāi)發(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ù)庫(kù)中的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() {
        ListjedisShardInfos = 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)證用戶(hù)信息是否正確,并將登錄成功的用戶(hù)信息保存到Redis數(shù)據(jù)庫(kù)中。
第二件事情:負(fù)責(zé)判斷用戶(hù)令牌是否過(guò)期,若沒(méi)有則刷新令牌存活時(shí)間。
第三件事情:負(fù)責(zé)從Redis數(shù)據(jù)庫(kù)中刪除用戶(hù)信息。

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) {
     // 檢查用戶(hù)名是否注冊(cè),一般在前端驗(yàn)證的時(shí)候處理,因?yàn)樽?cè)不存在高并發(fā)的情況,這里再加一層查詢(xún)是不影響性能的 if (null != userRepository.findByAccount(user.getAccount())) { return Result.build(400, "");
     }
     userRepository.save(user);
     // 注冊(cè)成功后選擇發(fā)送郵件激活?,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);
  // 把用戶(hù)信息寫(xiě)入 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語(yǔ)句,否則,如果兩個(gè)對(duì)象不同,則會(huì)發(fā)出update語(yǔ)句。
  user.setPassword(userPassword);
  user.setSalt(userSalt);
  // 設(shè)置 session 的過(guò)期時(shí)間
  jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE);
  // 添加寫(xiě) 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中查詢(xún)用戶(hù)信息
  String json = jedisClient.get(REDIS_USER_SESSION_KEY + ":" + token);
  // 判斷是否為空 if (StringUtils.isEmpty(json)) { return Result.build(400, "此session已經(jīng)過(guò)期,請(qǐng)重新登錄");
  }
  // 更新過(guò)期時(shí)間
  jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE);
  // 返回用戶(hù)信息 return Result.ok(JsonUtils.jsonToPojo(json, User.class));
 }
}

四、Controller 層

負(fù)責(zé)跳轉(zhuǎn)登錄頁(yè)面跳轉(zhuǎn),負(fù)責(zé)用戶(hù)的登錄,退出,獲取令牌的操作。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)單的登錄頁(yè)面和資源展示頁(yè)面。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" />"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)到登錄頁(yè)面,把用戶(hù)請(qǐng)求的url作為參數(shù)傳遞給登錄頁(yè)面。
   response.sendRedirect("http://localhost:8081/login?redirect=" + request.getRequestURL());
   // 返回false return false;
  }
  // 把用戶(hù)信息放入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è)用戶(hù),執(zhí)行sso 項(xiàng)目下SpringbootStudyApplicationTests.java 單元測(cè)試類(lèi)中的 registerUser() 方法添加用戶(hù)。
第二步:開(kāi)啟sso服務(wù)。
第三步:再開(kāi)啟兩個(gè)service-test-sso服務(wù)。
第四步:在service-test-sso服務(wù)頁(yè)面點(diǎn)擊“訪(fǎng)問(wèn)主頁(yè)”按鈕進(jìn)入權(quán)限頁(yè)面測(cè)試。

八、sso項(xiàng)目結(jié)構(gòu)

基于SpringBoot實(shí)現(xiàn)單點(diǎn)登錄系統(tǒng)

service-test-sso項(xiàng)目結(jié)構(gòu)
基于SpringBoot實(shí)現(xiàn)單點(diǎn)登錄系統(tǒng)

訪(fǎng)問(wèn)主頁(yè)
基于SpringBoot實(shí)現(xiàn)單點(diǎn)登錄系統(tǒng)

點(diǎn)擊登錄
基于SpringBoot實(shí)現(xiàn)單點(diǎn)登錄系統(tǒng)
用戶(hù)表存儲(chǔ)如下
基于SpringBoot實(shí)現(xiàn)單點(diǎn)登錄系統(tǒng)
依次通過(guò)訪(fǎng)問(wèn)如下鏈接:
http://localhost:8083/
http://localhost:8081/login?redirect=/indexHomePage
http://localhost:8082/
然后直接就可以不用登錄就可以訪(fǎng)問(wèn)資源了,實(shí)現(xiàn)SSO功能


免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀(guān)點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀(guān)點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專(zhuān)欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車(chē)的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車(chē)技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車(chē)工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車(chē)。 SODA V工具的開(kāi)發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車(chē) 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶(hù)希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開(kāi)幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱(chēng),數(shù)字世界的話(huà)語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱(chēng)"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉