Spring Security OAuth2.0認證授權系列文章> [Spring Security OAuth2.0認證授權一:框架搭建和認證測試](https://blog.kdyzm.cn/post/24) > [Spring Security OAuth2.0認證授權二:搭建資源服務](https://blog.kdyzm.cn/post/25) > [Spring Security OAuth2.0認證授權三:使用JWT令牌](https://blog.kdyzm.cn/post/26)前面幾篇文章講解了如何從頭開始搭建認證服務和資源服務,從頒發普通令牌到頒發jwt令牌,最終完成了jwt令牌的頒發和校驗。本篇文章將會講解分散式環境下如何進行認證和授權。## 一、設計思路一般來說,一個典型的分散式系統架構如上圖所示,這裡進行一個簡單的設計,來完成分散式系統下的認證和授權。整體設計思路是使用OAuth2.0頒發令牌,使用JWT對令牌簽名並頒發JWT令牌給客戶端。既然決定使用JWT令牌了,則不需要再呼叫認證伺服器對令牌進行驗證了,因為JWT本身就包含了所需要的資訊,而且只要驗籤成功,則可認為令牌可信任且有效。如上所述,則可以如此設計:1. 使用者請求登陸之後認證服務頒發令牌給使用者,瀏覽器將令牌儲存下來。2. 瀏覽器請求資源的的時候攜帶著令牌,閘道器攔截請求對令牌驗證,驗證的方法很簡單,不請求認證服務而是直接使用金鑰(對稱或非對稱)驗籤,只要驗證成功則將jwt payload中的資訊解析成明文放到請求頭中轉發請求到資源服務。3. 資源服務拿到明文資訊,根據明文資訊中的許可權資訊驗證是否有許可權訪問該資源,有許可權則返回資源資訊,無許可權則返回401。綜上,整體思路就是閘道器認證,資源服務鑑權。典型的微服務架構下會有註冊中心、閘道器等服務,接下來會依次介紹和搭建相關服務。## 二、註冊中心搭建為了方便程式本地除錯方便,這裡使用eureka server作為服務註冊中心,使用起來也非常簡單### 1.新增maven依賴``` xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
org.springframework.boot
spring-boot-starter-actuator
```### 2.新建啟動類``` java@SpringBootApplication@EnableEurekaServerpublic class RegisterServer { public static void main(String[] args) { SpringApplication.run(RegisterServer.class,args); }}```### 3.新建配置檔案``` yamlspring: application: name: register-serverserver: port: 8765 #啟動埠eureka: server: enable-self-preservation: false #關閉伺服器自我保護,客戶端心跳檢測15分鐘內錯誤達到80%服務會保護,導致別人還認為是好用的服務 eviction-interval-timer-in-ms: 10000 #清理間隔(單位毫秒,預設是60*1000)5秒將客戶端剔除的服務在服務註冊列表中剔除# shouldUseReadOnlyResponseCache: true #eureka是CAP理論種基於AP策略,為了保證強一致性關閉此切換CP 預設不關閉 false關閉 client: register-with-eureka: false #false:不作為一個客戶端註冊到註冊中心 fetch-registry: false #為true時,可以啟動,但報異常:Cannot execute request on any known server instance-info-replication-interval-seconds: 10 serviceUrl: defaultZone: http://localhost:${server.port}/eureka/ instance: hostname: ${spring.cloud.client.ip-address} prefer-ip-address: true instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}```然後啟動啟動類,訪問瀏覽器,[http://127.0.0.1:8765](http://127.0.0.1:8765),出現如下頁面即表示已經成功## 二、閘道器搭建這裡選用spring cloud gateway作為閘道器(不是zuul)### 1.新增maven依賴``` xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-gateway
2.2.5.RELEASE
org.springframework.boot
spring-boot-starter-actuator
org.projectlombok
lombok
org.springframework.security
spring-security-jwt
```### 2.新建啟動類``` java@SpringBootApplicationpublic class GatewayServer { public static void main(String[] args) { SpringApplication.run(GatewayServer.class, args); }}```### 3.新建配置檔案``` yamlserver: port: 8761spring: cloud: gateway: routes: - id: resource_server uri: "lb://resource-server" predicates: - Path=/r** application: name: gateway-servereureka: client: service-url: defaultZone: http://127.0.0.1:8765/eureka instance: prefer-ip-address: true instance-id: ${spring.application.name}:${spring.cloud.client.ip‐address}:${spring.application.instance_id:${server.port}}```如此,一個閘道器就已經搭建好了,但是還不具備我們想要的認證功能。### 4.新增token全域性過濾器知識點有以下幾點:- 全域性過濾器要實現GlobalFilter介面- 為了實現token過濾器最先被呼叫,要實現Order介面並將優先順序調到最大- 使用JwtHelper工具類對jwt驗籤,簽名的key必須和認證中心中配置的key保持一致- 驗籤成功後將jwt中payload明文資訊放到token-info的header值中傳遞給目標服務實現程式碼如下:```java@Component@Slf4jpublic class TokenFilter implements GlobalFilter, Ordered { private static final String BEAR_HEADER = "Bearer "; /** * 該值要和auth-server中配置的簽名相同 * * com.kdyzm.spring.security.auth.center.config.TokenConfig#SIGNING_KEY */ private static final String SIGNING_KEY = "auth123"; @Override public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); //如果沒有token,則直接返回401 if(StringUtils.isEmpty(token)){ return unAuthorized(exchange); } //驗籤並獲取PayLoad String payLoad; try { Jwt jwt = JwtHelper.decodeAndVerify(token.replace(BEAR_HEADER,""), new MacSigner(SIGNING_KEY)); payLoad = jwt.getClaims(); } catch (Exception e) { log.error("驗籤失敗",e); return unAuthorized(exchange); } //將PayLoad資料放到header ServerHttpRequest.Builder builder = exchange.getRequest().mutate(); builder.header("token-info", payLoad).build(); //繼續執行 return chain.filter(exchange.mutate().request(builder.build()).build()); } private Mono
unAuthorized(ServerWebExchange exchange){ exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } /** * 將該過濾器的優先順序設定為最高,因為只要認證不通過,就不能做任何事情 * * @return */ @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }}```## 三、資源服務修改原來資源服務已經集成了OAuth2.0、Spring Security、JWT等元件,根據現在的設計方案,需要刪除OAuth2.0和JWT元件,只留下Spring Security元件。### 1.移除OAuth2.0、JWT元件這裡要刪除maven依賴,同時將相關配置刪除**第一步,刪除maven依賴**,直接將以下兩個依賴移除就好``` xml
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.security
spring-security-jwt
```**第二步,刪除相關配置**將ResouceServerConfig、TokenConfig兩個類直接刪除 即可。### 2.新增過濾器這裡需要使用過濾器做,首先寫一個過濾器,實現OncePerRequestFilter介面,該過濾器的作用就是獲取閘道器傳過來的token-info明文資料,封裝成JwtTokenInfo物件,並將該相關資訊新增到SpringSecurity上下文以備之後的鑑權使用。程式碼實現如下:``` java@Component@Slf4jpublic class AuthFilterCustom extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ObjectMapper objectMapper = new ObjectMapper(); String tokenInfo=request.getHeader("token-info"); if(StringUtils.isEmpty(tokenInfo)){ log.info("未找到token資訊"); filterChain.doFilter(request,response); return; } JwtTokenInfo jwtTokenInfo = objectMapper.readValue(tokenInfo, JwtTokenInfo.class); log.info("tokenInfo={}",objectMapper.writeValueAsString(jwtTokenInfo)); List
authorities1 = jwtTokenInfo.getAuthorities(); String[] authorities=new String[authorities1.size()]; authorities1.toArray(authorities); //將使用者資訊和許可權填充 到使用者身份token物件中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtTokenInfo.getUser_name(),null, AuthorityUtils.createAuthorityList(authorities)); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //將authenticationToken填充到安全上下文 SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request,response); }}```### 3.將過濾器註冊到過濾器鏈修改WebSecurityConfig類,使用如下方法註冊過濾器:``` java.addFilterAfter(authFilterCustom, BasicAuthenticationFilter.class)//新增過濾器```同時,一定要關閉session功能,否則會出現上下文快取問題``` java.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS);//禁用session```完整程式碼如下:``` java @Autowired private AuthFilterCustom authFilterCustom; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf() .disable() .authorizeRequests()// .antMatchers("/r/r1").hasAuthority("p2")// .antMatchers("/r/r2").hasAuthority("p2") .antMatchers("/**").authenticated()//所有的請求必須認證通過 .anyRequest().permitAll()//其它所有請求都可以隨意訪問 .and() .addFilterAfter(authFilterCustom, BasicAuthenticationFilter.class)//新增過濾器 .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS);//禁用session }```## 四、其他注意事項認證服務auth-server以及資源服務resource-server、閘道器服務gateway-server都要整合eureka client元件## 五、測試測試前需要將各個服務依次啟動起來:- 啟動註冊中心 register-server:[https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/register-server](https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/register-server)- 啟動閘道器 gateway-server:[https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/gateway-server](https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/gateway-server)- 啟動認證服務 auth-server:[https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/auth-server](https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/auth-server)- 啟動資源服務 resource-server:[https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/resource-server](https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/resource-server)**第一步,獲取token**這裡使用password模式直接獲取token,POST請求如下介面:[http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123](http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123)即可獲取token。**第二步,訪問資源**通過閘道器請求資源服務的r1介面,GET請求如下介面:[http://127.0.0.1:8761/r1](http://127.0.0.1:8761/r1)需要帶上Header,key為`Authorization`,value格式如下:``` textBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE2MTAzNzI5MzUsImF1dGhvcml0aWVzIjpbInAxIiwicDIiXSwianRpIjoiOWQzMzRmZGMtOTcwZC00YmJkLWI2MmMtZDU4MDZkNTgzM2YwIiwiY2xpZW50X2lkIjoiYzEifQ.gZraRNeX-o_jKiH7XQgg3TlUQBpxUcXa2-qR_Treu8U```如果相應結果如下,則表示測試通過```訪問資源r1```否則,會返回401狀態碼。## 六、專案原始碼專案原始碼:[https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0](https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0)我的部落格原文地址:[https://blog.kdyzm.cn/post/30](https://blog.kdyzm.cn/p