這邊記錄下如何使用IdentityServer4 作為 Java SpringBoot 的 認證服務器和令牌頒發服務器。本人也是新手,所以理解不足的地方請多多指教。另外由於真的很久沒有寫中文了,用詞不太恰當的地方也歡迎新手大佬小伙伴指出,一起進步。另外這邊令牌的獲取需要手動使用postman根據令牌端點獲取,然后放在請求頭里面通過postman發給Java的demo,本身這個demo沒有取令牌的功能,請各位注意。
- 背景知識:什么是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.*;
差不多是這樣了,還有不是很清楚的地方直接看源代碼。