在项目中大多通过认证服务颁发jwt令牌,客户端携带jwt访问资源服务,资源服务对jwt的合法性进行验证。如下图:
加上网关并完善后如下图所示:
所有访问微服务的请求都要经过网关,在网关进行用户身份的认证可以将很多非法的请求拦截到微服务以外,这叫做网关认证。
下边需要明确网关的职责:
1、网站白名单维护
针对不用认证的URL全部放行。
2、校验jwt的合法性。
除了白名单剩下的就是需要认证的请求,网关需要验证jwt的合法性,jwt合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。
网关负责授权吗?
网关不负责授权,对请求的授权操作在各个微服务进行,因为微服务最清楚用户有哪些权限访问哪些接口。
实现网关认证
下边实现网关认证,实现以下职责:
1、网站白名单维护
针对不用认证的URL全部放行。
2、校验jwt的合法性。
除了白名单剩下的就是需要认证的请求,网关需要验证jwt的合法性,jwt合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。
1、在网关工程添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
2、在网关服务中编写配置类
GatewayAuthFilter
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/**
* @description 网关认证过虑器
*/
@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {
//白名单
private static List<String> whitelist = null;
static {
//加载白名单
try (
InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
) {
Properties properties = new Properties();
properties.load(resourceAsStream);
Set<String> strings = properties.stringPropertyNames();
whitelist= new ArrayList<>(strings);
} catch (Exception e) {
log.error("加载/security-whitelist.properties出错:{}",e.getMessage());
e.printStackTrace();
}
}
@Autowired
private TokenStore tokenStore;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();
//白名单放行
for (String url : whitelist) {
if (pathMatcher.match(url, requestUrl)) {
return chain.filter(exchange);
}
}
//检查token是否存在
String token = getToken(exchange);
if (StringUtils.isBlank(token)) {
return buildReturnMono("没有认证",exchange);
}
//判断是否是有效的token
OAuth2AccessToken oAuth2AccessToken;
try {
oAuth2AccessToken = tokenStore.readAccessToken(token);
boolean expired = oAuth2AccessToken.isExpired();
if (expired) {
return buildReturnMono("认证令牌已过期",exchange);
}
return chain.filter(exchange);
} catch (InvalidTokenException e) {
log.info("认证令牌无效: {}", token);
return buildReturnMono("认证令牌无效",exchange);
}
}
/**
* 获取token
*/
private String getToken(ServerWebExchange exchange) {
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(tokenStr)) {
return null;
}
String token = tokenStr.split(" ")[1];
if (StringUtils.isBlank(token)) {
return null;
}
return token;
}
private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
String jsonString = JSON.toJSONString(new RestErrorResponse(error));
byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return 0;
}
}
RestErrorResponse
import java.io.Serializable;
/**
* 错误响应参数包装
*/
public class RestErrorResponse implements Serializable {
private String errMessage;
public RestErrorResponse(String errMessage){
this.errMessage= errMessage;
}
public String getErrMessage() {
return errMessage;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
}
SecurityConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
/**
* @description 安全配置类
*/
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {
//安全拦截配置
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/**").permitAll()
.anyExchange().authenticated()
.and().csrf().disable().build();
}
}
TokenConfig
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class TokenConfig {
String SIGNING_KEY = "mq123";
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
3、在resources中配置白名单文件security-whitelist.properties
/auth/**=认证地址
/content/open/**=内容管理公开访问接口
/media/open/**=媒资管理公开访问接口
重启网关工程,进行测试
1、申请令牌
2、通过网关访问资源服务
这里访问内容管理服务
### 通过网关访问资源服务
GET http://localhost:63010/content/course/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzNjIzMTAsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6Ijc2OTkwMGNiLWM1ZjItNGRiNC1hZWJmLWY1MzgxZDQxZWMyZCIsImNsaWVudF9pZCI6ImMxIn0.lOITjUgYg2HCh5mDPK9EvJJqz-tIupKVfmP8yWJQIKs
当token正确时可以正常访问资源服务,token验证失败返回token无效:
由于是在网关处进行令牌校验,所以在微服务处不再校验令牌的合法性,修改内相应被访问服务的ResouceServerConfig类,屏蔽authenticated()。
可查看我的另一篇文章
SpringBoot +oAuth2+JWT+Spring Security认证配置
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// .antMatchers("/r/**","/course/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()
;
}
注意:网关鉴权功能调试通过后,由于目前还没有开发认证功能,前端请求网关的URL不在白名单中间时会“没有认证”的错误,暂时在白名单中添加 全部放行配置,待认证功能开发完成再屏蔽全部放行配置,