Spring Security OAuth2.0認證授權系列文章
Spring Security OAuth2.0認證授權一:框架搭建和認證測試
Spring Security OAuth2.0認證授權二:搭建資源服務
Spring Security OAuth2.0認證授權三:使用JWT令牌
前面幾篇文章講解了如何從頭開始搭建認證服務和資源服務,從頒發普通令牌到頒發jwt令牌,最終完成了jwt令牌的頒發和校驗。本篇文章將會講解分散式環境下如何進行認證和授權。
一、設計思路
一般來說,一個典型的分散式系統架構如上圖所示,這裡進行一個簡單的設計,來完成分散式系統下的認證和授權。
整體設計思路是使用OAuth2.0頒發令牌,使用JWT對令牌簽名並頒發JWT令牌給客戶端。既然決定使用JWT令牌了,則不需要再呼叫認證伺服器對令牌進行驗證了,因為JWT本身就包含了所需要的資訊,而且只要驗籤成功,則可認為令牌可信任且有效。
如上所述,則可以如此設計:
- 使用者請求登陸之後認證服務頒發令牌給使用者,瀏覽器將令牌儲存下來。
- 瀏覽器請求資源的的時候攜帶著令牌,閘道器攔截請求對令牌驗證,驗證的方法很簡單,不請求認證服務而是直接使用金鑰(對稱或非對稱)驗籤,只要驗證成功則將jwt payload中的資訊解析成明文放到請求頭中轉發請求到資源服務。
- 資源服務拿到明文資訊,根據明文資訊中的許可權資訊驗證是否有許可權訪問該資源,有許可權則返回資源資訊,無許可權則返回401。
綜上,整體思路就是閘道器認證,資源服務鑑權。
典型的微服務架構下會有註冊中心、閘道器等服務,接下來會依次介紹和搭建相關服務。
二、註冊中心搭建
為了方便程式本地除錯方便,這裡使用eureka server作為服務註冊中心,使用起來也非常簡單
1.新增maven依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
2.新建啟動類
@SpringBootApplication
@EnableEurekaServer
public class RegisterServer {
public static void main(String[] args) {
SpringApplication.run(RegisterServer.class,args);
}
}
3.新建配置檔案
spring:
application:
name: register-server
server:
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,出現如下頁面即表示已經成功
二、閘道器搭建
這裡選用spring cloud gateway作為閘道器(不是zuul)
1.新增maven依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--gateway 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<!--actuator 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- jwt依賴 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
</dependencies>
2.新建啟動類
@SpringBootApplication
public class GatewayServer {
public static void main(String[] args) {
SpringApplication.run(GatewayServer.class, args);
}
}
3.新建配置檔案
server:
port: 8761
spring:
cloud:
gateway:
routes:
- id: resource_server
uri: "lb://resource-server"
predicates:
- Path=/r**
application:
name: gateway-server
eureka:
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值中傳遞給目標服務
實現程式碼如下:
@Component
@Slf4j
public 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<Void> 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<Void> 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依賴,直接將以下兩個依賴移除就好
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
第二步,刪除相關配置
將ResouceServerConfig、TokenConfig兩個類直接刪除 即可。
2.新增過濾器
這裡需要使用過濾器做,首先寫一個過濾器,實現OncePerRequestFilter介面,該過濾器的作用就是獲取閘道器傳過來的token-info明文資料,封裝成JwtTokenInfo物件,並將該相關資訊新增到SpringSecurity上下文以備之後的鑑權使用。
程式碼實現如下:
@Component
@Slf4j
public 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<String> 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類,使用如下方法註冊過濾器:
.addFilterAfter(authFilterCustom, BasicAuthenticationFilter.class)//新增過濾器
同時,一定要關閉session功能,否則會出現上下文快取問題
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);//禁用session
完整程式碼如下:
@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
- 啟動閘道器 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
- 啟動資源服務 resource-server:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0/resource-server
第一步,獲取token
這裡使用password模式直接獲取token,POST請求如下介面:
即可獲取token。
第二步,訪問資源
通過閘道器請求資源服務的r1介面,GET請求如下介面:
需要帶上Header,key為Authorization
,value格式如下:
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE2MTAzNzI5MzUsImF1dGhvcml0aWVzIjpbInAxIiwicDIiXSwianRpIjoiOWQzMzRmZGMtOTcwZC00YmJkLWI2MmMtZDU4MDZkNTgzM2YwIiwiY2xpZW50X2lkIjoiYzEifQ.gZraRNeX-o_jKiH7XQgg3TlUQBpxUcXa2-qR_Treu8U
如果相應結果如下,則表示測試通過
訪問資源r1
否則,會返回401狀態碼。
六、專案原始碼
專案原始碼:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v5.0.0
我的部落格原文地址:https://blog.kdyzm.cn/post/30