成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

如何設計一套單點登錄系統

開發 前端
昨天介紹了API接口設計token鑒權方案,其實token鑒權最佳的實踐場景就是在單點登錄系統上。在企業發展初期,使用的后臺管理系統還比較少,一個或者兩個。

[[401827]]

本文轉載自微信公眾號「Java極客技術」,作者鴨血粉絲 。轉載本文請聯系Java極客技術公眾號。

一、介紹

昨天介紹了API接口設計token鑒權方案,其實token鑒權最佳的實踐場景就是在單點登錄系統上。

在企業發展初期,使用的后臺管理系統還比較少,一個或者兩個。

以電商系統為例,在起步階段,可能只有一個商城下單系統和一個后端管理產品和庫存的系統。

隨著業務量越來越大,此時的業務系統會越來越復雜,項目會劃分成多個組,每個組負責各自的領域,例如:A組負責商城系統的開發,B組負責支付系統的開發,C組負責庫存系統的開發,D組負責物流跟蹤系統的開發,E組負責每日業績報表統計的開發...等等。

規模變大的同時,人員也會逐漸的增多,以研發部來說,大致的人員就有這么幾大類:研發人員、測試人員、運維人員、產品經理、技術支持等等。

他們會頻繁的登錄各自的后端業務系統,然后進行辦公。

此時,我們可以設想一下,如果每個組都自己開發一套后端管理系統的登錄,假如有10個這樣的系統,同時一個新入職的同事需要每個系統都給他開放一個權限,那么我們可能需要給他開通10個賬號。

隨著業務規模的擴大,大點的公司,可能高達一百多個業務系統,那豈不是要配置一百多個賬號,讓人去做這種操作,豈不傷天害理。

面對這種繁瑣而且又無效的工作,IT大佬們想到一個辦法,那就是開發一套登錄系統,所有的業務系統都認可這套登錄系統,那么就可以實現只需要登錄一次,就可以訪問其他相互信任的應用系統。

這個登錄系統,我們把它稱為:單點登錄系統。

好了,言歸正傳,下面我們從兩個方面來介紹單點登錄系統的實現。

  • 方案設計
  • 項目實踐

二、方案設計

2.1、單體后端系統登錄

在傳統的單體后端系統中,簡單點的操作,我們一般都會這么玩,用戶使用賬號、密碼登錄之后,服務器會給當前用戶創建一個session會話,同時也會生成一個cookie,最后返回給前端。

當用戶訪問其他后端的服務時,我們只需要檢查一下當前用戶的session是否有效,如果無效,就再次跳轉到登錄頁面;如果有效,就進入業務處理流程。

但是,如果訪問不同的域名系統時,這個cookie是無效的,因此不能跨系統訪問,同時也不支持集群環境的共享。

對于單點登錄的場景,我們需要重新設計一套新的方案。

2.2、單點登錄系統登錄

先來一張圖!

這個流程圖,就是單點登錄系統與應用系統之間的交互圖。

當用戶登錄某應用系統時,應用系統會把將客戶端傳入的token,調用單點登錄系統驗證token合法性接口,如果不合法就會跳轉到單點登錄系統的登錄頁面;如果合法,就直接進入首頁。

進入登錄頁面之后,會讓用戶輸入用戶名、密碼進行登錄驗證,如果驗證成功之后,會返回一個有效的token,然后客戶端會根據服務端返回的參數鏈接,跳轉回之前要訪問的應用系統。

接著,應用系統會再次驗證token的合法性,如果合法,就進入首頁,流程結束。

引入單點登錄系統后,接入的應用系統不需要關系用戶登錄這塊,只需要對客戶端的token做一下合法性鑒權操作就可以了。

而單點登錄系統,只需要做好用戶的登錄流程和鑒權并返回安全的token給客戶端。

有的項目,會將生成的token,存放在客戶端的cookie中,這樣做的目的,就是避免每次調用接口的時候都在url里面帶上token。

但是,瀏覽器只允許同域名下的cookies可以共享,對于不同的域名系統, cookie 是無法共享的。

對于這種情況,我們可以先將 token 放入到url鏈接中,類似上面流程圖中跳轉思路,對于同一個應用系統,我們可以將token放入到 cookie 中,不同的應用系統,我們可以通過 url 鏈接進行傳遞,實現token的傳輸。

三、項目實踐

在實踐上,token的存儲,有兩種方案:

  • 存放在服務器,如果是分布式環境,一般都會存儲在 redis 中
  • 存儲在客戶端,服務器做驗證,天然支持分布式

3.1、存放在redis

存放在redis中,是一種比較常見的處理辦法,最開始的時候也是這種處理辦法。

當用戶登錄成功之后,會將用戶的信息作為value,用uuid作為key,存儲到redis中,各個服務集群共享用戶信息。

代碼實踐也非常簡單。

用戶登錄之后,將用戶信息存在到redis,同時返回一個有效的token給客戶端。

  1. @RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) 
  2. public TokenVO login(@RequestBody LoginDTO loginDTO){ 
  3.     //...參數合法性驗證 
  4.     //從數據庫獲取用戶信息 
  5.     User dbUser = userService.selectByUserNo(loginDTO.getUserNo); 
  6.     //....用戶、密碼驗證 
  7.  
  8.     //創建token 
  9.     String token = UUID.randomUUID(); 
  10.     //將token和用戶信息存儲到redis,并設置有效期2個小時 
  11.     redisUtil.save(token, dbUser, 2*60*60); 
  12.     //定義返回結果 
  13.     TokenVO result = new TokenVO(); 
  14.     //封裝token 
  15.     result.setToken(token); 
  16.     //封裝應用系統訪問地址 
  17.     result.setRedirectURL(loginDTO.getRedirectURL()); 
  18.     return result; 

客戶端收到登錄成功之后,根據參數組合進行跳轉到對應的應用系統。

跳轉示例如下:http://xxx.com/page.html?token=xxxxxx

各個應用系統,只需要編寫一個過濾器TokenFilter對token參數進行驗證攔截,即可實現對接,代碼如下:

  1. @Override 
  2. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException, SecurityException { 
  3.     HttpServletRequest request = (HttpServletRequest) servletRequest; 
  4.     HttpServletResponse response = (HttpServletResponse) servletResponse; 
  5.  
  6.     String requestUri = request.getRequestURI(); 
  7.     String contextPath = request.getContextPath(); 
  8.     String serviceName = request.getServerName(); 
  9.  
  10.     //添加到白名單的URL放行 
  11.     String[] excludeUrls = { 
  12.             "(?:/images/|/css/|/js/|/template/|/static/|/web/|/constant/).+$"
  13.             "/user/login"
  14.             "/user/createImage" 
  15.     }; 
  16.     for (String url : excludeUrls) { 
  17.         if (requestUri.matches(contextPath + url) || (serviceName.matches(url))) { 
  18.             filterChain.doFilter(request, response); 
  19.             return
  20.         } 
  21.     } 
  22.     //運行跨域探測 
  23.     if(RequestMethod.OPTIONS.name().equals(request.getMethod())){ 
  24.         filterChain.doFilter(request, response); 
  25.         return
  26.     } 
  27.  
  28.     //檢查token是否有效 
  29.     final String token = request.getHeader("token"); 
  30.     if(StringUtils.isEmpty(token) || !redisUtil.exist(token)){ 
  31.         ResultMsg<Object> resultMsg = new ResultMsg<>(4000, "token已失效"); 
  32.         //封裝跳轉地址 
  33.         resultMsg.setRedirectURL("http://sso.xxx.com?redirectURL=" + request.getRequestURL()); 
  34.         WebUtil.buildPrintWriter(response, resultMsg); 
  35.         return
  36.     } 
  37.     //將用戶信息,存入request中,方便后續獲取 
  38.     User user =  redisUtil.get(token); 
  39.     request.setAttribute("user"user); 
  40.     filterChain.doFilter(request, response); 
  41.     return

上面返回的是json數據給前端,當然你還可以直接在服務器采用重定向進行跳轉,具體根據自己的情況進行選擇。

由于每個應用系統都可能需要進行對接,因此我們可以將上面的方法封裝成一個公共jar包,應用系統只需要依賴包即可完成對接!

3.2、token存放客戶端

還有一種方案,是將token存放客戶端,這種方案就是服務端根據規則對數據進行加密生成一個簽名串,這個簽名串就是我們所說的token,最后返回給前端。

因為加密的操作都是在服務端完成的,因此密鑰的管理非常重要,不能泄露出去,不然很容易被黑客解密出來。

最典型的應用就是JWT!

JWT 是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了JWT字符串。就像這樣:

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ 

如何實現呢?首先我們需要添加一個jwt依賴包。

  1. <!-- jwt支持 --> 
  2. <dependency> 
  3.     <groupId>com.auth0</groupId> 
  4.     <artifactId>java-jwt</artifactId> 
  5.     <version>3.4.0</version> 
  6. </dependency> 

然后,創建一個用戶信息類,將會通過加密存放在token中。

  1. @Data 
  2. @EqualsAndHashCode(callSuper = false
  3. @Accessors(chain = true
  4. public class UserToken implements Serializable { 
  5.  
  6.     private static final long serialVersionUID = 1L; 
  7.  
  8.     /** 
  9.      * 用戶ID 
  10.      */ 
  11.     private String userId; 
  12.  
  13.     /** 
  14.      * 用戶登錄賬戶 
  15.      */ 
  16.     private String userNo; 
  17.  
  18.     /** 
  19.      * 用戶中文名 
  20.      */ 
  21.     private String userName; 

接著,創建一個JwtTokenUtil工具類,用于創建token、驗證token。

  1. public class JwtTokenUtil { 
  2.  
  3.  //定義token返回頭部 
  4.     public static final String AUTH_HEADER_KEY = "Authorization"
  5.  
  6.  //token前綴 
  7.     public static final String TOKEN_PREFIX = "Bearer "
  8.  
  9.  //簽名密鑰 
  10.     public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x"
  11.   
  12.  //有效期默認為 2hour 
  13.     public static final Long EXPIRATION_TIME = 1000L*60*60*2; 
  14.  
  15.  
  16.     /** 
  17.      * 創建TOKEN 
  18.      * @param content 
  19.      * @return 
  20.      */ 
  21.     public static String createToken(String content){ 
  22.         return TOKEN_PREFIX + JWT.create() 
  23.                 .withSubject(content) 
  24.                 .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) 
  25.                 .sign(Algorithm.HMAC512(KEY)); 
  26.     } 
  27.  
  28.     /** 
  29.      * 驗證token 
  30.      * @param token 
  31.      */ 
  32.     public static String verifyToken(String token) throws Exception { 
  33.         try { 
  34.             return JWT.require(Algorithm.HMAC512(KEY)) 
  35.                     .build() 
  36.                     .verify(token.replace(TOKEN_PREFIX, "")) 
  37.                     .getSubject(); 
  38.         } catch (TokenExpiredException e){ 
  39.             throw new Exception("token已失效,請重新登錄",e); 
  40.         } catch (JWTVerificationException e) { 
  41.             throw new Exception("token驗證失敗!",e); 
  42.         } 
  43.     } 

同時編寫配置類,允許跨域,并且創建一個權限攔截器。

  1. @Slf4j 
  2. @Configuration 
  3. public class GlobalWebMvcConfig implements WebMvcConfigurer { 
  4.     /** 
  5.      * 重寫父類提供的跨域請求處理的接口 
  6.      * @param registry 
  7.      */ 
  8.     @Override 
  9.     public void addCorsMappings(CorsRegistry registry) { 
  10.         // 添加映射路徑 
  11.         registry.addMapping("/**"
  12.                 // 放行哪些原始域 
  13.                 .allowedOrigins("*"
  14.                 // 是否發送Cookie信息 
  15.                 .allowCredentials(true
  16.                 // 放行哪些原始域(請求方式) 
  17.                 .allowedMethods("GET""POST""DELETE""PUT""OPTIONS""HEAD"
  18.                 // 放行哪些原始域(頭部信息) 
  19.                 .allowedHeaders("*"
  20.                 // 暴露哪些頭部信息(因為跨域訪問默認不能獲取全部頭部信息) 
  21.                 .exposedHeaders("Server","Content-Length""Authorization""Access-Token""Access-Control-Allow-Origin","Access-Control-Allow-Credentials"); 
  22.     } 
  23.  
  24.     /** 
  25.      * 添加攔截器 
  26.      * @param registry 
  27.      */ 
  28.     @Override 
  29.     public void addInterceptors(InterceptorRegistry registry) { 
  30.         //添加權限攔截器 
  31.         registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**"); 
  32.     } 

使用AuthenticationInterceptor攔截器對接口參數進行驗證。

  1. @Slf4j 
  2. public class AuthenticationInterceptor implements HandlerInterceptor { 
  3.  
  4.     @Override 
  5.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
  6.         // 從http請求頭中取出token 
  7.         final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY); 
  8.         //如果不是映射到方法,直接通過 
  9.         if(!(handler instanceof HandlerMethod)){ 
  10.             return true
  11.         } 
  12.         //如果是方法探測,直接通過 
  13.         if (HttpMethod.OPTIONS.equals(request.getMethod())) { 
  14.             response.setStatus(HttpServletResponse.SC_OK); 
  15.             return true
  16.         } 
  17.         //如果方法有JwtIgnore注解,直接通過 
  18.         HandlerMethod handlerMethod = (HandlerMethod) handler; 
  19.         Method method=handlerMethod.getMethod(); 
  20.         if (method.isAnnotationPresent(JwtIgnore.class)) { 
  21.             JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class); 
  22.             if(jwtIgnore.value()){ 
  23.                 return true
  24.             } 
  25.         } 
  26.         LocalAssert.isStringEmpty(token, "token為空,鑒權失敗!"); 
  27.         //驗證,并獲取token內部信息 
  28.         String userToken = JwtTokenUtil.verifyToken(token); 
  29.    
  30.         //將token放入本地緩存 
  31.         WebContextUtil.setUserToken(userToken); 
  32.         return true
  33.     } 
  34.  
  35.     @Override 
  36.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 
  37.         //方法結束后,移除緩存的token 
  38.         WebContextUtil.removeUserToken(); 
  39.     } 

最后,在controller層用戶登錄之后,創建一個token,存放在頭部即可。

  1. /** 
  2.  * 登錄 
  3.  * @param userDto 
  4.  * @return 
  5.  */ 
  6. @JwtIgnore 
  7. @RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) 
  8. public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){ 
  9.     //...參數合法性驗證 
  10.  
  11.     //從數據庫獲取用戶信息 
  12.     User dbUser = userService.selectByUserNo(userDto.getUserNo); 
  13.  
  14.     //....用戶、密碼驗證 
  15.  
  16.     //創建token,并將token放在響應頭 
  17.     UserToken userToken = new UserToken(); 
  18.     BeanUtils.copyProperties(dbUser,userToken); 
  19.  
  20.     String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken)); 
  21.     response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token); 
  22.  
  23.  
  24.     //定義返回結果 
  25.     UserVo result = new UserVo(); 
  26.     BeanUtils.copyProperties(dbUser,result); 
  27.     return result; 

到這里基本就完成了!

其中AuthenticationInterceptor中用到的JwtIgnore是一個注解,用于不需要驗證token的方法上,例如驗證碼的獲取等等。

  1. @Target({ElementType.METHOD, ElementType.TYPE}) 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. public @interface JwtIgnore { 
  4.  
  5.     boolean value() default true

而WebContextUtil是一個線程緩存工具類,其他接口通過這個方法即可從token中獲取用戶信息。

  1. public class WebContextUtil { 
  2.  
  3.     //本地線程緩存token 
  4.     private static ThreadLocal<String> local = new ThreadLocal<>(); 
  5.  
  6.     /** 
  7.      * 設置token信息 
  8.      * @param content 
  9.      */ 
  10.     public static void setUserToken(String content){ 
  11.         removeUserToken(); 
  12.         local.set(content); 
  13.     } 
  14.  
  15.     /** 
  16.      * 獲取token信息 
  17.      * @return 
  18.      */ 
  19.     public static UserToken getUserToken(){ 
  20.         if(local.get() != null){ 
  21.             UserToken userToken = JSONObject.parseObject(local.get() , UserToken.class); 
  22.             return userToken; 
  23.         } 
  24.         return null
  25.     } 
  26.  
  27.     /** 
  28.      * 移除token信息 
  29.      * @return 
  30.      */ 
  31.     public static void removeUserToken(){ 
  32.         if(local.get() != null){ 
  33.             local.remove(); 
  34.         } 
  35.     } 

對應用系統而言,重點在于token的驗證,可以將攔截器方法封裝成一個公共的jar包,然后各個應用系統引用即可!

和上面介紹的token存儲到redis方案類似,不同點在于:一個將用戶數據存儲到redis,另一個是采用加密算法存儲到客戶端進行傳輸。

四、小結

在實際的使用過程中,我個人更加傾向于采用jwt方案,直接在服務端使用簽名加密算法生成一個token,然后在客戶端進行流轉,天然支持分布式,但是要注意加密時用的密鑰要安全管理。

而采用redis方案存儲的時候,你需要搭建高可用的集群環境,同時保證緩存數據不會失效等等,維護成本高! 

在實際的實現上,每個公司玩法不一樣,有的安全性要求高,后端還會加上密鑰環節進行安全驗證,基本思路大同小異。

 

責任編輯:武曉燕 來源: Java極客技術
相關推薦

2024-11-19 16:31:23

2025-02-21 08:17:13

2024-11-12 08:13:09

2024-09-23 04:00:00

java架構分布式系統

2021-05-06 11:06:52

人工智能語音識別聲聞檢索

2022-11-12 17:50:02

Web服務器微服務

2016-11-28 10:22:52

物聯網設備系統

2022-08-04 00:05:11

系統分布式流量

2025-04-27 10:10:04

2022-02-25 09:00:00

數據科學工具架構

2020-10-19 10:35:43

iOS設備尺寸

2019-10-11 15:58:25

戴爾

2020-05-12 14:20:47

GitHub 系統微軟

2009-03-03 13:00:00

虛擬化技術vmwarexen

2025-04-07 07:45:00

AI模型神經網絡

2016-10-12 17:42:04

云服務云計算云遷移

2009-06-23 18:01:45

Ajax框架源代碼

2018-08-31 08:42:48

LinuxUnix實用程序

2014-12-02 10:02:21

Android異步任務

2010-06-09 17:00:43

UML試題
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 91精品福利 | 日本精品网站 | 国产精品毛片无码 | 国产福利资源在线 | 欧美福利视频 | 欧美精品一区二区在线观看 | 美女天天操| 鲁一鲁资源影视 | 久久久久久久亚洲精品 | 日韩成人免费视频 | 91在线看片| 婷婷久久五月 | 视频一区二区三区中文字幕 | 国产日韩欧美在线 | 国产精品久久久久久久久久 | 99re| 黄色av观看 | 一区二区三区中文字幕 | 中文字幕一区二区三区乱码在线 | 欧美在线资源 | 天天天天操 | 成年女人免费v片 | 夜色www国产精品资源站 | 一区二区视频在线 | 秋霞a级毛片在线看 | 欧美毛片免费观看 | 国产色婷婷精品综合在线手机播放 | 香蕉91| 久久综合伊人一区二区三 | 国产精品海角社区在线观看 | 国产精品亚洲一区二区三区在线 | 色桃网| 涩色视频在线观看 | 天天色影视综合 | 日韩美女爱爱 | h片在线免费看 | 特黄特色大片免费视频观看 | 精品美女视频在免费观看 | 国产免费一区二区三区网站免费 | 久久久精品一区二区三区 | 日韩精品久久一区 |