很久沒寫博客了,由於最近公司要求我學spring cloud
,早點將之前軟件遷移到新的架構上。因此我那個拼命的學吶,老是圖快,不少關鍵的筆記沒有作好記錄,如今又遺忘了不少關鍵的技術點,極其罪惡!javascript
如今想想,仍是踏踏實實的走比較好。這不,今天我冒了個泡,來補一補前面我所學所忘的知識點。java
想要解鎖更多新姿式?請訪問個人博客。git
今天我麼聊一聊JWT。github
關於JWT,相信不少人都已經看過用過,他是基於json
數據結構的認證規範,簡單的說就是驗證用戶登沒登錄的玩意。這時候你可能回想,哎喲,不是又那個session麼,分佈式系統用redis
作分佈式session,那這個jwt有什麼好處呢?web
請聽我慢慢訴說這歷史!redis
HTTP BASIC auth,別看它名字那麼長那麼生,你就認爲這個玩意很高大上。其實原理很簡單,簡單的說就是每次請求API的時候,都會把用戶名和密碼經過restful API
傳給服務端。這樣就能夠實現一個無狀態思想,即每次HTTP請求和之前都沒有啥關係,只是獲取目標URI,獲得目標內容以後,此次鏈接就被殺死,沒有任何痕跡。你可別一聽無狀態,正是如今的熱門思想,就以爲很厲害。其實他的缺點仍是又的,咱們經過http請求發送給服務端的時候,頗有可能將咱們的用戶名密碼直接暴漏給第三方客戶端,風險特別大,所以生產環境下用這個方法不多。算法
session和cookie老生常談了。開始時,都會在服務端全局建立session對象,session對象保存着各類關鍵信息,同時向客戶端發送一組sessionId
,成爲一個cookie對象保存在瀏覽器中。spring
當認證時,cookie的數據會傳入服務端與session進行匹配,進而進行數據認證。sql
此時,實現的是一個有狀態的思想,即該服務的實例能夠將一部分數據隨時進行備份,而且在建立一個新的有狀態服務時,能夠經過備份恢復這些數據,以達到數據持久化的目的。數據庫
這種認證方法基本是如今軟件最經常使用的方法了,它有一些本身的缺點:
token 即便是在計算機領域中也有不一樣的定義,這裏咱們說的token,是指 訪問資源的憑據 。使用基於 Token 的身份驗證方法,在服務端不須要存儲用戶的登陸記錄。大概的流程是 這樣的:
Token機制,我認爲其本質思想就是將session中的信息簡化不少,看成cookie用,也就是客戶端的「session」。
那Token機制相對於Cookie機制又有什麼好處呢?
說了那麼多token認證的好處,但他其實並無想象的那麼神,token 也並非沒有問題。
正常狀況下要比 session_id 更大,須要消耗更多流量,擠佔更多帶寬,假如你的網站每個月有 10 萬次的瀏覽器,就意味着要多開銷幾十兆的流量。聽起來並很少,但日積月累也是不小一筆開銷。實際上,許多人會在 JWT 中存儲的信息會更多。
在網站上使用 JWT,對於用戶加載的幾乎全部頁面,都須要從緩存/數據庫中加載用戶信息,若是對於高流量的服務,你肯定這個操做合適麼?若是使用redis進行緩存,那麼效率上也並不能比 session 更高效
JWT 的賣點之一就是加密簽名,因爲這個特性,接收方得以驗證 JWT 是否有效且被信任。可是大多數 Web 身份認證應用中,JWT 都會被存儲到 Cookie 中,這就是說你有了兩個層面的簽名。聽着彷佛很牛逼,可是沒有任何優點,爲此,你須要花費兩倍的 CPU 開銷來驗證簽名。對於有着嚴格性能要求的 Web 應用,這並不理想,尤爲對於單線程環境。
如今咱們來講說今天的主角,JWT
JSON Web Token(JWT)是一個很是輕巧的規範。這個規範容許咱們使用JWT在用 戶和服務器之間傳遞安全可靠的信息
一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。
頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也能夠 被表示成一個JSON對象。
{ "typ":"JWT", "alg":"HS256" }
這就是頭部的明文內容,第一部分說明他是一個jwt,第二部分則指出簽名算法用的是HS256算法。
而後將這個頭部進行BASE64編碼,編碼後造成頭部:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
載荷就是存放有效信息的地方,有效信息包含三個部分:
(1)標準中註冊的聲明(建議但不強制使用)
(2)公共的聲明
公共的聲明能夠添加任何的信息,通常添加用戶的相關信息或其餘業務須要的必要信息. 但不建議添加敏感信息,由於該部分在客戶端可解密.
(3)私有的聲明
私有聲明是提供者和消費者所共同定義的聲明,通常不建議存放敏感信息,由於base64 是對稱解密的,意味着該部分信息能夠歸類爲明文信息。
{ "sub":"1234567890", "name":"tengshe789", "admin": true }
上面就是一個簡單的載荷的明文,接下來使用base64加密:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:
這個部分須要base64加密後的header和base64加密後的payload使用.鏈接組成的字符串,而後經過header中聲明的加密方式進行加鹽secret組合加密,而後就構成了jwt的第 三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q
如今通常實現jwt,都使用Apache 的開源項目JJWT(一個提供端到端的JWT建立和驗證的Java庫)。
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
public class CreateJWT { public static void main(String[] args) throws Exception{ JwtBuilder builder = Jwts.builder().setId("123") .setSubject("jwt所面向的用戶") .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256,"tengshe789"); String s = builder.compact(); System.out.println(s); //eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA } }
結果如圖:
(注意,jjwt不支持jdk11,0.9.1之後的jjwt必須實現signWith()方法才能實現)
public class ParseJWT { public static void main(String[] args) { String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA"; Claims claims = Jwts.parser().setSigningKey("tengshe789").parseClaimsJws(token).getBody(); System.out.println("id"+claims.getId()); System.out.println("Subject"+claims.getSubject()); System.out.println("IssuedAt"+claims.getIssuedAt()); } }
結果如圖:
在企業級系統中,一般內部會有很是多的工具平臺供你們使用,好比人力資源,代碼管理,日誌監控,預算申請等等。若是每個平臺都實現本身的用戶體系的話無疑是巨大的浪費,因此公司內部會有一套公用的用戶體系,用戶只要登錄以後,就可以訪問全部的系統。
這就是 單點登陸(SSO: Single Sign-On)
SSO 是一類解決方案的統稱,而在具體的實施方面,通常有兩種策略可供選擇:
欲揚先抑,先說說幾個重要的知識點。
認證 的做用在於承認你有權限訪問系統,用於鑑別訪問者是不是合法用戶。負責認證的服務一般稱爲 Authorization Server 或者 Identity Provider,如下簡稱 IdP
受權 用於決定你有訪問哪些資源的權限。大多數人不會區分這二者的區別,由於站在用戶的立場上。而做爲系統的設計者來講,這二者是有差異的,這是不一樣的兩個工做職責,咱們能夠只須要認證功能,而不須要受權功能,甚至不須要本身實現認證功能,而藉助 Google 的認證系統,即用戶能夠用 Google 的帳號進行登錄。負責提供資源(API調用)的服務稱爲 Resource Server 或者 Service Provider,如下簡稱 SP
OAuth(開放受權)是一個開放的受權標準,容許用戶讓第三方應用訪問該用戶在 某一web服務上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。
流程能夠參考以下:
簡單的來講,就是你要訪問一個應用服務,先找它要一個request token
(請求令牌),再把這個request token
發到第三方認證服務器,此時第三方認證服務器會給你一個aceess token
(通行令牌), 有了aceess token
你就可使用你的應用服務了。
注意圖中第4步兌換 access token
的過程當中,不少第三方系統,如Google ,並不會僅僅返回 access token
,還會返回額外的信息,這其中和以後更新相關的就是 refresh token
。一旦 access token
過時,你就能夠經過 refresh token
再次請求 access token
。
固然了,流程是根據你的請求方式和訪問的資源類型而定的,業務不少也是不同的,我這是簡單的聊聊。
如今這種方法比較常見,常見的譬如使用QQ快速登錄,用的基本的都是這種方法。
咱們用一個很火的開源項目Cloud-Admin爲栗子,來分析一下jwt的應用。
Cloud-Admin是基於Spring Cloud微服務化開發平臺,具備統一受權、認證後臺管理系統,其中包含具有用戶管理、資源權限管理、網關API管理等多個模塊,支持多業務系統並行開發。
鑑權中心功能在ace-auth
與ace-gate
下。
下面是官方提供的架構模型。
能夠看到,AuthServer
在架構的中心環節,要訪問服務,必須須要鑑權中心的JWT鑑權。
先看實體類,這裏鑑權中心定義了一組客戶端實體,以下:
@Table(name = "auth_client") @Getter @Setter public class Client { @Id private Integer id; private String code; private String secret; private String name; private String locked = "0"; private String description; @Column(name = "crt_time") private Date crtTime; @Column(name = "crt_user") private String crtUser; @Column(name = "crt_name") private String crtName; @Column(name = "crt_host") private String crtHost; @Column(name = "upd_time") private Date updTime; @Column(name = "upd_user") private String updUser; @Column(name = "upd_name") private String updName; @Column(name = "upd_host") private String updHost; private String attr1; private String attr2; private String attr3; private String attr4; private String attr5; private String attr6; private String attr7; private String attr8;
對應數據庫:
CREATE TABLE `auth_client` ( `id` int(11) NOT NULL AUTO_INCREMENT, `code` varchar(255) DEFAULT NULL COMMENT '服務編碼', `secret` varchar(255) DEFAULT NULL COMMENT '服務密鑰', `name` varchar(255) DEFAULT NULL COMMENT '服務名', `locked` char(1) DEFAULT NULL COMMENT '是否鎖定', `description` varchar(255) DEFAULT NULL COMMENT '描述', `crt_time` datetime DEFAULT NULL COMMENT '建立時間', `crt_user` varchar(255) DEFAULT NULL COMMENT '建立人', `crt_name` varchar(255) DEFAULT NULL COMMENT '建立人姓名', `crt_host` varchar(255) DEFAULT NULL COMMENT '建立主機', `upd_time` datetime DEFAULT NULL COMMENT '更新時間', `upd_user` varchar(255) DEFAULT NULL COMMENT '更新人', `upd_name` varchar(255) DEFAULT NULL COMMENT '更新姓名', `upd_host` varchar(255) DEFAULT NULL COMMENT '更新主機', `attr1` varchar(255) DEFAULT NULL, `attr2` varchar(255) DEFAULT NULL, `attr3` varchar(255) DEFAULT NULL, `attr4` varchar(255) DEFAULT NULL, `attr5` varchar(255) DEFAULT NULL, `attr6` varchar(255) DEFAULT NULL, `attr7` varchar(255) DEFAULT NULL, `attr8` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;
這些是每組微服務客戶端的信息
第二個實體類,就是客戶端_服務的實體,也就是對應着那些微服務客戶端能調用哪些微服務客戶端:
大概對應的就是微服務間調用權限關係。
@Table(name = "auth_client_service") public class ClientService { @Id private Integer id; @Column(name = "service_id") private String serviceId; @Column(name = "client_id") private String clientId; private String description; @Column(name = "crt_time") private Date crtTime; @Column(name = "crt_user") private String crtUser; @Column(name = "crt_name") private String crtName; @Column(name = "crt_host") private String crtHost;}
咱們跳着看,先看接口層
@RestController @RequestMapping("jwt") @Slf4j public class AuthController { @Value("${jwt.token-header}") private String tokenHeader; @Autowired private AuthService authService; @RequestMapping(value = "token", method = RequestMethod.POST) public ObjectRestResponse<String> createAuthenticationToken( @RequestBody JwtAuthenticationRequest authenticationRequest) throws Exception { log.info(authenticationRequest.getUsername()+" require logging..."); final String token = authService.login(authenticationRequest); return new ObjectRestResponse<>().data(token); } @RequestMapping(value = "refresh", method = RequestMethod.GET) public ObjectRestResponse<String> refreshAndGetAuthenticationToken( HttpServletRequest request) throws Exception { String token = request.getHeader(tokenHeader); String refreshedToken = authService.refresh(token); return new ObjectRestResponse<>().data(refreshedToken); } @RequestMapping(value = "verify", method = RequestMethod.GET) public ObjectRestResponse<?> verify(String token) throws Exception { authService.validate(token); return new ObjectRestResponse<>(); } }
這裏放出了三個接口
先說第一個接口,建立token
。
具體邏輯以下:
每個用戶登錄進來時,都會進入這個環節。根據request中用戶的用戶名和密碼,利用feign
客戶端的攔截器攔截request,而後使用做者寫的JwtTokenUtil
裏面的各類方法取出token中的key和密鑰,驗證token是否正確,正確則用authService.login(authenticationRequest);
的方法返回出去一個新的token。
public String login(JwtAuthenticationRequest authenticationRequest) throws Exception { UserInfo info = userService.validate(authenticationRequest); if (!StringUtils.isEmpty(info.getId())) { return jwtTokenUtil.generateToken(new JWTInfo(info.getUsername(), info.getId() + "", info.getName())); } throw new UserInvalidException("用戶不存在或帳戶密碼錯誤!"); }
下圖是詳細邏輯圖:
做者寫了個註解的入口,使用@EnableAceAuthClient
即自動開啓微服務(客戶端)的鑑權管理
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(AutoConfiguration.class) @Documented @Inherited public @interface EnableAceAuthClient { }
接着沿着註解的入口看
@Configuration @ComponentScan({"com.github.wxiaoqi.security.auth.client","com.github.wxiaoqi.security.auth.common.event"}) public class AutoConfiguration { @Bean ServiceAuthConfig getServiceAuthConfig(){ return new ServiceAuthConfig(); } @Bean UserAuthConfig getUserAuthConfig(){ return new UserAuthConfig(); } }
註解會自動的將客戶端的用戶token和服務token的關鍵信息加載到bean中
做者重寫了okhttp3
攔截器的方法,每一次微服務客戶端請求的token都會被攔截下來,驗證服務調用服務的token和用戶調用服務的token是否過時,過時則返回新的token
@Override public Response intercept(Chain chain) throws IOException { Request newRequest = null; if (chain.request().url().toString().contains("client/token")) { newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .build(); } else { newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()) .build(); } Response response = chain.proceed(newRequest); if (HttpStatus.FORBIDDEN.value() == response.code()) { if (response.body().string().contains(String.valueOf(CommonConstants.EX_CLIENT_INVALID_CODE))) { log.info("Client Token Expire,Retry to request..."); serviceAuthUtil.refreshClientToken(); newRequest = chain.request() .newBuilder() .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken()) .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()) .build(); response = chain.proceed(newRequest); } } return response; }
第二道攔截器是來自spring容器的,第一道feign攔截器只是驗證了兩個token是否過時,但token真實的權限卻沒驗證。接下來就要驗證兩個token的權限問題了。
服務調用權限代碼以下:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 配置該註解,說明不進行服務攔截 IgnoreClientToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class); if (annotation == null) { annotation = handlerMethod.getMethodAnnotation(IgnoreClientToken.class); } if(annotation!=null) { return super.preHandle(request, response, handler); } String token = request.getHeader(serviceAuthConfig.getTokenHeader()); IJWTInfo infoFromToken = serviceAuthUtil.getInfoFromToken(token); String uniqueName = infoFromToken.getUniqueName(); for(String client:serviceAuthUtil.getAllowedClient()){ if(client.equals(uniqueName)){ return super.preHandle(request, response, handler); } } throw new ClientForbiddenException("Client is Forbidden!"); }
用戶權限:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; // 配置該註解,說明不進行用戶攔截 IgnoreUserToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreUserToken.class); if (annotation == null) { annotation = handlerMethod.getMethodAnnotation(IgnoreUserToken.class); } if (annotation != null) { return super.preHandle(request, response, handler); } String token = request.getHeader(userAuthConfig.getTokenHeader()); if (StringUtils.isEmpty(token)) { if (request.getCookies() != null) { for (Cookie cookie : request.getCookies()) { if (cookie.getName().equals(userAuthConfig.getTokenHeader())) { token = cookie.getValue(); } } } } IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(token); BaseContextHandler.setUsername(infoFromToken.getUniqueName()); BaseContextHandler.setName(infoFromToken.getName()); BaseContextHandler.setUserID(infoFromToken.getId()); return super.preHandle(request, response, handler); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { BaseContextHandler.remove(); super.afterCompletion(request, response, handler, ex); }
該框架中全部的請求都會走網關服務(ace-gatev2),經過網關,來驗證token是否過時異常,驗證token是否不存在,驗證token是否有權限進行服務。
下面是核心代碼:
@Override public Mono<Void> filter(ServerWebExchange serverWebExchange, GatewayFilterChain gatewayFilterChain) { log.info("check token and user permission...."); LinkedHashSet requiredAttribute = serverWebExchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR); ServerHttpRequest request = serverWebExchange.getRequest(); String requestUri = request.getPath().pathWithinApplication().value(); if (requiredAttribute != null) { Iterator<URI> iterator = requiredAttribute.iterator(); while (iterator.hasNext()){ URI next = iterator.next(); if(next.getPath().startsWith(GATE_WAY_PREFIX)){ requestUri = next.getPath().substring(GATE_WAY_PREFIX.length()); } } } final String method = request.getMethod().toString(); BaseContextHandler.setToken(null); ServerHttpRequest.Builder mutate = request.mutate(); // 不進行攔截的地址 if (isStartWith(requestUri)) { ServerHttpRequest build = mutate.build(); return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build()); } IJWTInfo user = null; try { user = getJWTUser(request, mutate); } catch (Exception e) { log.error("用戶Token過時異常", e); return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Token Forbidden or Expired!")); } List<PermissionInfo> permissionIfs = userService.getAllPermissionInfo(); // 判斷資源是否啓用權限約束 Stream<PermissionInfo> stream = getPermissionIfs(requestUri, method, permissionIfs); List<PermissionInfo> result = stream.collect(Collectors.toList()); PermissionInfo[] permissions = result.toArray(new PermissionInfo[]{}); if (permissions.length > 0) { if (checkUserPermission(permissions, serverWebExchange, user)) { return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Forbidden!Does not has Permission!")); } } // 申請客戶端密鑰頭 mutate.header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken()); ServerHttpRequest build = mutate.build(); return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build()); }
總的來講,鑑權和網關模塊就說完了。做者代碼構思極其精妙,使用在大型的權限系統中,能夠巧妙的減小耦合性,讓服務鑑權粒度細化,方便管理。
此片完了~ 想要了解更多精彩新姿式?
請訪問個人我的博客
本篇爲原創內容,已在我的博客率先發表,隨後看心情可能會在CSDN,segmentfault,掘金,簡書,開源中國同步發出。若有雷同,緣分呢兄弟。趕快加個好友,我們兩個想個號碼, 買個彩票,先掙他個幾百萬😝