接上一篇博客:https://www.cnblogs.com/wwjj4811/p/14509864.html
前提:需要有SpringCloud微服務相關經驗。
注冊中心eureka
新建模塊cloud-eureka
pom.xml
<dependencies>
<dependency>
<groupId>com.wj</groupId>
<artifactId>cloud-oauth2-base</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
主啟動類:com.wj.oauth2.EurekaServer
@EnableEurekaServer
@SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
application.yml
server:
port: 6001 # 服務端口
eureka:
instance:
hostname: localhost # eureka服務端的實例名稱
client:
registerWithEureka: false # 服務注冊,false表示不將自已注冊到Eureka服務中
fetchRegistry: false # 服務發現,false表示自己不從Eureka服務中獲取注冊信息
serviceUrl: # Eureka客戶端與Eureka服務端的交互地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
啟動注冊中心,可以訪問主頁
認證服務注冊
修改模塊cloud-oauth2-auth-server,添加eureka依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
yml添加如下配置:
eureka:
client:
registerWithEureka: true # 服務注冊開關
fetchRegistry: true # 服務發現開關
serviceUrl: # 注冊到哪一個Eureka Server服務注冊中心,多個中間用逗號分隔
defaultZone: http://localhost:6001/eureka
instance:
instanceId: ${spring.application.name}:${server.port} # 指定實例ID,頁面會顯示主機名
preferIpAddress: true #訪問路徑可以顯示IP地址
spring:
application:
name: auth-server
主啟動類添加@EnableEurekaClient
注解,然后重啟服務,發現認證服務已經注冊到注冊中心了
資源服務注冊
修改cloud-oauth2-resource-product模塊,添加eureka依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
修改application.yml,添加配置
eureka:
client:
registerWithEureka: true # 服務注冊開關
fetchRegistry: true # 服務發現開關
serviceUrl: # 注冊到哪一個Eureka Server服務注冊中心,多個中間用逗號分隔
defaultZone: http://localhost:6001/eureka
instance:
instanceId: ${spring.application.name}:${server.port} # 指定實例ID,頁面會顯示主機名
preferIpAddress: true #訪問路徑可以顯示IP地址
spring:
application:
name: product-server
server:
port: 8080
主啟動類添加@EnableEurekaClient
注解,然后重啟服務,發現資源服務已經注冊到注冊中心了
網關服務zuul
創建模塊cloud-zuul
pom.xml
<dependencies>
<dependency>
<groupId>com.wj</groupId>
<artifactId>cloud-oauth2-base</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 一樣作為資源服務器,所以要引入 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
配置 application.yml
server:
port: 7001 # 端口號
spring:
application:
name: zuul-gateway
eureka:
client:
registerWithEureka: true # 服務注冊開關
fetchRegistry: true # 服務發現開關
serviceUrl: # 注冊到哪一個Eureka Server服務注冊中心,多個中間用逗號分隔
defaultZone: http://localhost:6001/eureka
instance:
instanceId: ${spring.application.name}:${server.port} # 指定實例ID,頁面會顯示主機名
preferIpAddress: true #訪問路徑可以顯示IP地址
zuul: # 網關配置
sensitive-headers: null # 默認Zuul認為請求頭中 "Cookie", "Set-Cookie", "Authorization" 是敏感信息,它不會轉發請求,因為把它設置為空,就會轉發了
add-host-header: true # 正確的處理重定向操作
routes:
authentication: # 路由名稱,名稱任意,保持所有路由名稱唯一
path: /auth/** # 訪問路徑,轉發到 auth-server 服務處理
serviceId: auth-server # 指定服務ID,會自動從Eureka中找到此服務的ip和端口
stripPrefix: false # 代理轉發時去掉前綴,false:代理轉發時不去掉前綴 例如:為true時請求 /product/get/1,代理轉發到/get/1
product: # 商品服務路由配置
path: /product/** # 轉發到 product-server 服務處理
serviceId: product-server
stripPrefix: false
主啟動類com.wj.oauth2.ZuulServer:
@EnableZuulProxy //開啟zuul的功能
@EnableEurekaClient
@SpringBootApplication
public class ZuulServer {
public static void main(String[] args) {
SpringApplication.run(ZuulServer.class, args);
}
}
配置網關
JWT令牌管理
將資源服務(cloud-oauth2-resource-product)的public.txt復制到網關服務的resources目錄下
資源服務的TokenConfig也復制過來

網關的資源服務配置類
網關也被認為是資源服務器,訪問每個微服務資源,都要先進入網關進行攔截。
@Configuration
public class ResourceServerConfig {
public static final String RESOURCE_ID = "product-server";
@Autowired
private TokenStore tokenStore;
// 認證服務資源
@Configuration
@EnableResourceServer
public class AuthResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
;
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 關於請求認證服務器資源,則所有請求放行
http.authorizeRequests()
.anyRequest().permitAll();
}
}
// 商品資源服務器的資源
@Configuration
@EnableResourceServer
public class ProductResourceServerConfig extends ResourceServerConfigurerAdapter{
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
;
}
@Override
public void configure(HttpSecurity http) throws Exception {
//商品資源的請求都需要有PRODUCT_API的scope
http.authorizeRequests()
.antMatchers("/product/**")
.access("#oauth2.hasScope('all')");
}
}
}
網關的安全配置類
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 當前將所有請求放行,交給資源配置類進行資源權限判斷
* 因為默認情況下會攔截所有請求
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
}
跨域配置
@Configuration
public class CorsConfig {
// 配置全局解決zuul服務中的cors跨域問題
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedMethod("*");
//↓核心代碼
corsConfiguration.addExposedHeader("Authorization");
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}
自定義過濾器
創建com.wj.oauth2.filter.AuthenticationFilter.java
/**
* 請求資源前,先通過此 過濾器進行用戶信息解析和校驗 轉發
*/
@Slf4j
@Component
public class AuthenticationFilter extends ZuulFilter {
@Override
public String filterType() {
//請求路由前調用
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
// 如果解析到令牌就會封裝到OAuth2Authentication對象
if( !(authentication instanceof OAuth2Authentication)) {
return null;
}
log.info("網關獲取到認證對象:" + authentication);
// 用戶名,沒有其他用戶信息
Object principal = authentication.getPrincipal();
// 獲取用戶所擁有的權限
Collection<? extends GrantedAuthority> authorities
= authentication.getAuthorities();
Set<String> authoritySet = AuthorityUtils.authorityListToSet(authorities);
// 請求詳情
Object details = authentication.getDetails();
Map<String, Object> result = new HashMap<>();
result.put("principal", principal);
result.put("authorities", authoritySet);
result.put("details", details);
// 獲取當前請求上下文
RequestContext context = RequestContext.getCurrentContext();
// 將用戶信息和權限信息轉成json,再通過base64進行編碼
String base64 = Base64Utils.encodeToString(JSON.toJSONString(result).getBytes());
// 添加到請求頭
context.addZuulRequestHeader("auth-token", base64);
return null;
}
}
微服務用戶授權
在微服務中接收到網關轉發過來的Token后,需要我們構建一個Authentication對象來完成微服務認證與授權,這樣這個
微服務認證與授權,這樣這個微服務就可以根據用戶擁有的權限,來判斷對應資源是否可以被用戶訪問。
在資源服務創建TokenAuthenticationFilter
/**
* 獲取網關轉發過來的請求頭中保存的明文token值,用戶信息
*/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authToken = request.getHeader("auth-token");
if(StringUtils.isNotEmpty(authToken)) {
logger.info("商品資源服務器獲取到token值:" + authToken);
// 解析token
// 1. 通過base64解碼
String authTokenJson = new String(Base64Utils.decodeFromString(authToken));
// 2. 轉成json對象
JSONObject jsonObject = JSON.parseObject(authTokenJson);
// 用戶信息(用戶名)
Object principal = jsonObject.get("principal");
// 請求詳情
Object details = jsonObject.get("details");
String authorities = jsonObject.getJSONArray("authorities").stream()
.map(Object::toString)
.collect(Collectors.joining(","));
List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
// 自已構建一個Authentication對象,SpringSecurity就會自動進行權限判斷
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(
principal, null, authorityList);
authenticationToken.setDetails(details);
// 將對象傳給安全上下文,對應的就會自動的進行權限判斷,同時也可以獲取到用戶信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
// 放行請求
filterChain.doFilter(request, response);
}
}
測試
訪問:http://localhost:7001/auth/oauth/authorize?client_id=wj-pc&response_type=code
第一次登陸,默認直接跳轉到登陸頁面
點擊Authorize
返回了code碼
通過code碼去獲取access_token令牌:http://localhost:7001/auth/oauth/token
攜帶access_token去訪問資源服務器: