接上一篇博客: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去访问资源服务器: