這邊記錄下如何使用IdentityServer4 作為 Java SpringBoot 的 認證伺服器和令牌頒發伺服器。本人也是新手,所以理解不足的地方請多多指教。另外由於真的很久沒有寫中文了,用詞不太恰當的地方也歡迎新手大佬小夥伴指出,一起進步。
- 背景知識:什麼是JWT
- 第一部分:IdentityServer4的伺服器搭建
- 第二部分:Java SpringBoot框架和IDS4的結合
- SpringBoot Demo 原始碼:https://github.com/Danni-Ke/SpringBootDemo
1.什麼是Jwt?
關於什麼是Jwt,包括裡面的引數是什麼,這邊可以參考下面這個連結做一些瞭解:
https://www.cnblogs.com/zjutzz/p/5790180.html
下面這個連結是全英文的,但是對jwt是什麼是比較詳細的,英文好的同學可以上了。
https://tools.ietf.org/html/rfc7519
這個連結主要說了jwt和refernce token不一樣,真的很重要,裡面也有些說的不對,我就被坑了:
https://www.cnblogs.com/Irving/articles/9357539.html
2.IdentityServer認證&令牌頒發伺服器準備
關於IdentityServer4怎麼搭建使用,網上已經有太多的教程了,這邊我就不多做別的講解,因為我也是新手。但是我目前自己正在使用的是一個帶UI介面的IdentityServer4和Identity(作為使用者管理的部分)結合的伺服器,很多東西已經幫你搭建好了,對新手可以說是十分友好,省去了探索的步驟。但是不建議新手直接使用,作為自己搭建IdentityServer後還有對IdentitySevrer4一些引數不太理解的地方,可以做進一步的理解。下面是IdentityServer4 UI 的github原始碼連結:
https://github.com/skoruba/IdentityServer4.Admin
- 這邊要是有人感興趣配置這個IdentityServer4 UI,後期也會記錄下相對的這個事怎麼搭建的。這個IdentityServer上面在配置相關的資訊,比如API,Client,還有使用者資源之後,我們會用到如下端點:
- http://x.x.x.x:5000/connect/token 請求令牌的埠,需要提供客戶端id,客戶端secret,使用者名稱字,使用者密碼,還有授權方式,這裡我選的grant_type是 password。
- http://x.x.x.x:5000/connect/introspect 令牌自省端點,很多國內的說法是用於refenrece token,然後還有很多大佬翻譯的官方文件根本就是沒認真翻譯(估計也不知道實際意思)。這個端點實際上可以用於那些沒由相應的包或者library可以用於解析jwt令牌的程式來驗證令牌的合法性。只是注意這邊唯一不同的是,對於renfence token和jwt token 要發給這個端點的引數是不一樣的。對於reference,要發的是 client_id 和 secret。但是對於 jwt token,要發的是 base64編碼在請求頭部的 Api_name 和 Api_secret, 這裡就是為什麼 Api有secret這個引數,但是我們幾乎沒有用到過。
- http://x.x.x.x:5000/.well-known/openid-configuration/jwks (公鑰開放端點) 用於獲取解析jwt令牌的公鑰開放端點。
3.Java SpringBoot
關於如何開啟一個新的專案,這邊就不多說了,網上教程很多,我們直接進入正題,這邊我用的Intellij IDEA。然後註冊了過濾器,然後這邊提供了兩種辦法驗證jwt:
- 通過自省端點返回驗證結果,使用http://x.x.x.x:5000/connect/introspect
- 通過公鑰開放端點本地解析token,使用http://x.x.x.x:5000/.well-known/openid-configuration/jwks
由於沒有客戶端,這邊用postman代替求取token,使用http://x.x.x.x:5000/connect/token,然後給我們的java程式發起請求。
3.1通過自省端點返回驗證結果
這個就十分簡單了,上程式碼,主要是Filter裡面dofiler的部分:
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("開始進行過濾請求,由認證伺服器自省端點驗證token"); boolean authenticated = false; HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse rep = (HttpServletResponse) servletResponse; //--------------給自省端點傳送請求------------------------------- //--------------準備請求資訊---------------------------------------- //其實一個url請求就是一組組鍵對值,getHeader()方法獲取的是頭部的你想要的 //鍵名後面的值,由於請求裡面token的keyname是這個,倒是要是要改這裡也要改 //這裡面header要是沒有token這個就不行,會異常 boolean authorizationHeaderExist = req.getHeader("Authorization") != null; if (!authorizationHeaderExist) { rep.setStatus(HttpServletResponse.SC_BAD_REQUEST); return; } String token = cutToken(req.getHeader("Authorization")); //獲取編碼後的ApiSecret和ApiName,在application.propertiesz中 String apiNameSecret = "Basic " + ApiNameSecretbase64(); //倒是可以放到配置裡面去,那裡統一改 String introspectEndpoint = "http://localhost:5000/connect/introspect"; //-------------創造請求---------------------------------------------- //protected HttpClient client = new DefaultHttpClient();已過時 HttpClient client = HttpClientBuilder.create().build(); HttpPost post = new HttpPost(introspectEndpoint); //新增請求頭 post.setHeader("Authorization", apiNameSecret); //新增請求主體(body) List<NameValuePair> urlBodys = new ArrayList<NameValuePair>(); urlBodys.add(new BasicNameValuePair("token", token)); post.setEntity(new UrlEncodedFormEntity((urlBodys))); HttpResponse response = client.execute(post); System.out.println("\nSending 'POST' request to URL : " + introspectEndpoint); System.out.println("Post parameters : " + post.getEntity()); System.out.println("Response Code : " + response.getStatusLine().getStatusCode()); //讀取返回reponse的content的資訊,含有決定結果 BufferedReader rd = new BufferedReader( new InputStreamReader(response.getEntity().getContent())); //注意StringBuffer不是String StringBuffer result = new StringBuffer(); String line = ""; while ((line = rd.readLine()) != null) { result.append(line); } //除錯用,列印得到的請求的content System.out.println(result.toString()); //-------------------------------決定authenticated結果--------------------------- JSONObject jo = new JSONObject(result.toString()); Boolean active = jo.getBoolean("active"); if (response.getStatusLine().getStatusCode() == 200&& active==true) { String role = jo.getString("role"); authenticated = true; } //--------------------------------處理authenticated結果,決定是否發出401----------- if (authenticated) { //呼叫該方法後,表示過濾器經過原來的url請求處理方法 filterChain.doFilter(servletRequest, servletResponse); } else { rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } //返回Api名字和secret的編碼,請求頭的一部分 public String ApiNameSecretbase64() { String result = api.getapiName()+":"+api.getapiSecret(); byte[] data=Base64.encodeBase64(result.getBytes()); return new String(data); } //處理token字串,去掉Bearer public String cutToken(String originToken) { String[] temp = originToken.split(" "); return temp[1]; }
上面的 ApiNameSecretbase64 的功能是讀取配置中的資訊,返回編碼好的 Api Name 和 Secret, 下面是我application.properties相關的配置,同樣這些配置需要放到IdentityServer那邊,可以是通過記憶體的方式也可以是通過像我一樣使用 UI管理介面直接新增:
#IdentityServer4 配置檔案引數 api.name = Api1 api.secret=secreta
同時啟動java專案和IdentityServer4之後,請求就可以看到結果了,如果沒有帶token就會是無授權,這邊就不放postman的結果了,因為本篇主要側重於程式碼。關於請求這個端點的效果,返回格式參考官方文件,各位自己可以做本地相應的基於回覆的驗證方式。我這邊直接提取了裡面active這個布林量,來確定這個令牌是否合法。
3.2通過請求公鑰本地解析返回驗證token
廢話不多說先上程式碼:
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("開始進行過濾請求,由認證伺服器jwk公鑰解析驗證token"); boolean authenticated = false; HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse rep = (HttpServletResponse) servletResponse; boolean authorizationHeaderExist = req.getHeader("Authorization") != null; if (!authorizationHeaderExist) { rep.setStatus(HttpServletResponse.SC_BAD_REQUEST); return; } String jwkEndpoint = "http://localhost:5000/.well-known/openid-configuration/jwks"; //String token = cutToken(((HttpServletRequest) servletRequest).getHeader("Authorization")); String token = cutToken(req.getHeader("Authorization")); //------------解析------------------------------------------------------ //com.nimbusds JWT解析包,這個包目前沒有找到原始碼, //https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens //建立解析處理物件 ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor(); //提供公鑰地址來獲取 JWKSource keySource = new RemoteJWKSet(new URL(jwkEndpoint)); //提供解析演算法,演算法型別要寫對,伺服器用的是什麼就是什麼,目前是RSA256演算法 JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256; //填寫 RSA 公鑰來源從提供公鑰地址獲取那邊得到 JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource); if(keySelector==null) { rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED); System.out.println("無法獲取公鑰。"); return; } //設定第一步建立的解析處理物件 jwtProcessor.setJWSKeySelector(keySelector); //處理收到的token(令牌),錯誤則返回物件 SecurityContext ctx = null; JWTClaimsSet claimsSet = null; try { claimsSet = jwtProcessor.process(token, ctx); authenticated = true; } catch (ParseException e) { rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED); e.printStackTrace(); return; } catch (BadJOSEException e) { rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED); e.printStackTrace(); return; } catch (JOSEException e) { rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED); e.printStackTrace(); return; } //除錯用,列印出來 System.out.println(claimsSet.toJSONObject()); //失敗返回無授權 if(claimsSet==null) { rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } //解碼裡面具體內容,尤其角色,雖然這裡不需要,順利取出 JSONObject jo = new JSONObject(claimsSet.toJSONObject()); String role = jo.getString("role"); //試一下過期的token,刪除使用者的可以不試試 //--------------------------------處理authenticated結果,決定是否發出401----------- if (authenticated) { //呼叫該方法後,表示過濾器經過原來的url請求處理方法 filterChain.doFilter(servletRequest, servletResponse); } else { rep.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } //幫助類 public String cutToken(String originToken) { String[] temp = originToken.split(" "); return temp[1]; }
這邊主要是用到了一個包,用法連結如下,英文好的同學可以直接研究這個連結:
https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens
這個包需要import的東西還要maven依賴如下:
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>7.3</version> </dependency>
import com.nimbusds.jose.*; import com.nimbusds.jose.jwk.source.*; import com.nimbusds.jwt.*; import com.nimbusds.jose.proc.JWSKeySelector; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jwt.proc.*;
差不多是這樣了,還有不是很清楚的地方直接看原始碼。