OAuth2.0 - 使用 SpringGateWay 网关实现统一鉴权
Posted 小毕超
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OAuth2.0 - 使用 SpringGateWay 网关实现统一鉴权相关的知识,希望对你有一定的参考价值。
一、OAuth2.0
上篇文章我们学习了将用户信息存储至数据库中,前面的学习大家肯定已经对Oauth2.0已经有了自己认识,从前面我们可以了解到了,在用户认证时可以得到自己的用户信息及权限信息,然后权限的校验还是在资源服务实现的,从前面讲解SpringSecurity
单体环境的时候我们要做统一的动态鉴权操作,需要实现FilterInvocationSecurityMetadataSource
接口,在其中进行地址的动态角色权限的返回。现在服务的大环境下,每个业务服务都可以称为一个资源服务,那众多的资源服务,每个都实现这个相同的逻辑,有点显得程序的冗余了。
这个时候我们就可以从服务的统一入口gateway
网关处寻找突破口了,我们完全可以将每次请求,地址鉴权的操作移交到gateway
网关处进行统一拦截处理,并且还可以将Jwt
的校验也在gateway
网关中进行,现在就不显得程序的冗余了,因为我们只在网关做了改动,其他业务服务不做权限的校验了。
但是这样还是有一个问题,从整体上看我们的服务是安全的,但是每个业务服务之间还是裸奔的形式,一旦黑客绕过了网关,那我们的业务服务就没有安全可言了。而且还有一个问题就是有的接口我们不想配制在网关的统一把控,要通过SpringSecurity
的权限注解进行操作,这个时候不就是不生效了吗。
面对上面问题,我们既要保证每个服务是安全的,也要允许每个服务拥有自定义权限的操作,这个时候我们可以就选择对当网关校验成功后,在header
中再添加一个Token,这个Token可以还是一个JWT,不过可以把过期时长设置的短一些,也可以是Base64
加密之后的字符,业务服务接收到请求,通过继承BasicAuthenticationFilter
过滤器,将用户角色权限信息添加到UsernamePasswordAuthenticationToken
中,交给SpringSecurity
管理,这样每个服务就拥有自定义权限的功能,就算绕过网关,还是需要token
的校验。
上面已经保证了服务的安全,但是给用户的Jwt
是通过对称加密的也有点不太安全,我们可以采用RSA
非对称加密的方式生成JWT
,认证服务用私钥加密,网关通过公钥校验解析数据,然后转发到每个微服务的Token
,如果采用Jwt就可以使用对称加密的方式,减少性能的开销。
总结上面的过程,就是下面我画的这个流程图:
注意:这里为了方便大家的理解,把认证服务和网关分离开来了,正常大家微服务项目中,认证服务也要经过网关的。
下面我们结合前面文章的内容,继续修改项目,实现上面所说的功能,其中网关转发到业务服务,业务服务进行token的解析,由于篇幅,这里就不做展示了,因为这个和前面讲解SpringSecurity
整合JWT完全就是一个类型,这里就没必要再拿出来说一遍了,大家可以看下这篇博客:
二、认证服务
认证服务前面是通过对称加密生成的JWT
,这里我们要修改为RSA
非对称加密的方式。
首先生成密钥,可以通过JDK
自带的keytool
工具进行生成,在cmd
中输入以下命令:
keytool -genkey -alias bxc -keyalg RSA -keypass 123456 -keystore bxc.jks -storepass 123456
上面提示的信息可填写也可以不填,最后的一定要填是
,不然一直循环。
在当前目录就可以看到生成的jks
文件了:
将上面生成的文件复制到认证服务的resources
下:
在开始前我们还要对资源服务再引入一个依赖,就是nimbus
,也是一个JWT的生成库,要比jjwt
更加强大,有兴趣的可以学习下
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.16</version>
</dependency>
下面修改TokenConfig
,修改为使用非对称密钥:
@Configuration
public class TokenConfig
@Bean
public TokenStore tokenStore()
return new JwtTokenStore(accessTokenConverter());
@Bean
public JwtAccessTokenConverter accessTokenConverter()
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair()); //非对称秘钥
return converter;
@Bean
public KeyPair keyPair()
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("bxc.jks"), "123456".toCharArray());
return factory.getKeyPair("bxc", "123456".toCharArray());
下面还要编写一个公钥的接口,既然是公钥,就要公开,当然也可以使用keytool
和openssl
生成公钥放在资源服务的resources
下,通过本地读取的方式获取公钥,这里先提供一个公钥的接口:
@RestController
public class KeyController
@Autowired
KeyPair keyPair;
//获取公钥
@GetMapping("/key/public-key")
public Map<String, Object> getPublicKey()
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
将该接口放行,不需要认证,修改WebSecurityConfig
的configure(HttpSecurity http)
,将/key/**
规则的直接permitAll()
即可
@Override
protected void configure(HttpSecurity http) throws Exception
http.csrf().disable()
.authorizeRequests()
.antMatchers("/key/**").permitAll()
.antMatchers("/login/*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
到这认证服务就已经修改完成,重启服务,先看下获取的JWT:
可以明显看到使用RAS
非对称生成的JWT
要比对称的内容要长,下面再看下公钥的样子:
下面我们就可以去实现网关的逻辑了。
三、GateWay网关
新建一个SpringBoot的module,在依赖中添加GateWay 和 Oauth2.0 的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
修改配制文件,转发到资源服务,这里我直接配制的地址转发,由于篇幅这里没有继承注册中心,对网关不了解的,可以看下本专栏关于GateWay
网关的讲解。
server:
port: 80
spring:
application:
name: gateway
cloud:
gateway:
httpclient:
connect-timeout: 2000
response-timeout: 10s
routes:
- id: resuorce
uri: http://127.0.0.1:8080
order: 0
predicates:
- Path=/resuorce/**
filters:
- StripPrefix=1
下面我们先创建两个Handler,一个无权访问的,一个登录失效的:
@Component
public class AccessDeniedHandler implements ServerAccessDeniedHandler
@Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e)
JSONObject params = new JSONObject();
params.put("code", 403);
params.put("msg", "权限不足!");
ServerHttpResponse response = serverWebExchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Mono<Void> ret = null;
try
ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
catch (UnsupportedEncodingException e0)
e0.printStackTrace();
return ret;
@Component
public class LoginLoseHandler extends HttpBasicServerAuthenticationEntryPoint
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e)
JSONObject params = new JSONObject();
params.put("code", 401);
params.put("msg", "登录失效!");
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
Mono<Void> ret = null;
try
ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
catch (UnsupportedEncodingException e0)
e0.printStackTrace();
return ret;
下面创建一个AuthManagerHandler
类进行统一权限的控制,其中这块在注释中也写的比较明确了,就看你们的逻辑自行发挥
@Component
public class AuthManagerHandler implements ReactiveAuthorizationManager<AuthorizationContext>
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object)
ServerHttpRequest request = object.getExchange().getRequest();
String requestUrl = request.getPath().pathWithinApplication().value();
//这里可以根据requestUrl查询redis,或者数据库得到requestUrl所需的角色,放入roles中
List<String> roles = new ArrayList<>();
if (antPathMatcher.match("/resuorce/admin/**",requestUrl))
roles.add("ROLE_admin");
else
roles.add("ROLE_common");
roles.add("ROLE_admin");
return authentication
.filter(a -> a.isAuthenticated())
.flatMapIterable(a -> a.getAuthorities())
.map(g -> g.getAuthority())
.any(c ->
if (roles.contains(String.valueOf(c)))
return true;
return false;
)
.map(hasAuthority -> new AuthorizationDecision(hasAuthority))
.defaultIfEmpty(new AuthorizationDecision(false));
@Override
public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object)
return null;
编写接受JWT,并将其转化放入header
传入资源服务过滤器:
@Component
@Slf4j
@RequiredArgsConstructor
public class SecurityGlobalFilter implements WebFilter
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain)
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String token = request.getHeaders().getFirst("Authorization");
if (StringUtils.isEmpty(token))
JSONObject jsonObject = setResultErrorMsg(401, "登录失效");
DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toString().getBytes());
return response.writeWith(Mono.just(buffer));
// 解析JWT获取jti,以jti为key判断redis的黑名单列表是否存在,存在则拦截访问
token = token.replace("Bearer ", "");
String payload = "";
try
payload = StrUtil.toString(JWSObject.parse(token).getPayload());
catch (Exception e)
JSONObject jsonObject = setResultErrorMsg(401, "登录失效");
DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toString().getBytes());
return response.writeWith(Mono.just(buffer));
if (StringUtils.isEmpty(payload))
JSONObject jsonObject = setResultErrorMsg(401, "登录失效");
DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toString().getBytes());
return response.writeWith(Mono.just(buffer));
//给header里面添加值
JSONObject jsonObject = JSONUtil.parseObj(payload);
String base64 = Base64.encode(jsonObject.toString());
ServerHttpRequest tokenRequest = exchange.getRequest().mutate().header("token", base64).build();
ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
return chain.filter(build);
private JSONObject setResultErrorMsg(Integer code, String msg)
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", code);
jsonObject.put("message", msg);
return jsonObject;
下面就要创建ResourceServerConfig
,对上面的进行整合及,JWT的配制:
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig
@Autowired
AuthManagerHandler authManagerHandler;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Autowired
LoginLoseHandler loginLoseHandler;
@Autowired
SecurityGlobalFilter securityGlobalFilter;
// security的鉴权排除列表
private static final String[] excludedAuthPages =
"/auth/**"
;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.jwkSetUri("http://localhost:8020/key/public-key") // 远程获取公钥
;
http.authorizeExchange()
.pathMatchers(excludedAuthPages).permitAll() //无需进行权限过滤的请求路径
.pathMatchers(HttpMethod.OPTIONS).permitAll() //o
.pathMatchers("/**").access(authManagerHandler)
.anyExchange().authenticated()
.and()
.addFilterAfter(securityGlobalFilter, SecurityWebFiltersOrder.FIRST)
.exceptionHandling().authenticationEntryPoint(loginLoseHandler)
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and().cors().disable().csrf().disable();
return http.build();
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter()
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
启动网关服务
四、测试
如果不携带JWT访问:
登录获取JWT
访问资源服务接口:
访问/common/**
,应该无权访问:
通过Debug也可以看出,权限校验统一在AuthManagerHandler
中了:
喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!
以上是关于OAuth2.0 - 使用 SpringGateWay 网关实现统一鉴权的主要内容,如果未能解决你的问题,请参考以下文章
Spring OAuth2.0 - 动态注册 OAuth2.0 客户端