SpringCloud Gateway + Spring Security
父模块pom.xml<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.
·
父模块
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>groupId</groupId>
<artifactId>sc-scaffold</artifactId>
<version>0.1</version>
<modules>
<module>config-center</module>
<module>gateway-center</module>
<module>user-center</module>
</modules>
<name>${project.artifactId}</name>
<packaging>pom</packaging>
<properties>
<spring-boot.version>2.4.0</spring-boot.version>
<spring-cloud.version>2020.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2020.0.RC1</spring-cloud-alibaba.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<fastjson.version>1.2.75</fastjson.version>
</properties>
<profiles>
<profile>
<id>dev</id>
<properties>
<!-- 环境标识,需要与配置文件的名称相对应 -->
<profiles.active>dev</profiles.active>
</properties>
<activation>
<!-- 默认环境 -->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
<dependencies>
<!--bootstrap 启动器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--配置文件处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日志-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--JSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- spring boot 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring cloud 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring cloud alibaba 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<!--指定filtering=true.maven的占位符解析表达式就可以用于它里面的文件-->
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!--支持yaml读取pom的参数-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<encoding>UTF-8</encoding>
<delimiters>
<delimiter>@</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</build>
</project>
User用户中心
两个controller用来测试,一个放行,一个需要Token认证。
AuthController
@RestController
@RequestMapping(path = "/auth")
public class AuthController {
@GetMapping(path = "/c")
public String c() {
return "user center ok c";
}
@GetMapping(path = "/d")
public String d() {
return "user center ok d";
}
}
UserController
/**
* @author zhe.xiao
* @date 2021-03-23 17:18
* @description
*/
@Slf4j
@RestController
@RequestMapping(path = "/user")
public class UserController {
@GetMapping(path = "/a")
public String a() {
return "user center ok a";
}
@GetMapping(path = "/b")
public String b() {
return "user center ok b";
}
}
Gateway网关
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sc-scaffold</artifactId>
<groupId>groupId</groupId>
<version>0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway-center</artifactId>
<packaging>jar</packaging>
<dependencies>
<!--gateway === 内置webflux作为web服务器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--监控信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 2000
tomcat:
uri-encoding: UTF-8
spring:
application:
name: @artifactId@
redis:
host: redis-host
port: 6379
cloud:
nacos:
discovery:
server-addr: nacos-host:8848
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
discovery:
locator:
enabled: true #使用服务发现路由
routes:
- id: config-router
uri: lb://config-center #服务注册名
predicates:
- Path=/api-config/** #路径匹配规则
filters:
- StripPrefix=1 #匹配第一个api-*后,在转发路由的时候会去除api-*
- id: user-router
uri: lb://user-center
predicates:
- Path=/api-user/**
filters:
- StripPrefix=1
核心的配置安全策略
介绍
Spring Cloud Gateway中使用的是Spring-Webflux,所以不能用Spring MVC的那套安全配置。
现在spring security设置要采用响应式配置,基于WebFlux中WebFilter实现,与Spring MVC的Security是通过Servlet的Filter实现类似,也是一系列filter组成的过滤链。
Reactor与传统MVC配置对应:
webflux | mvc | 作用 |
---|---|---|
@EnableWebFluxSecurity | @EnableWebSecurity | 开启security配置 |
ServerAuthenticationSuccessHandler | AuthenticationSuccessHandler | 登录成功Handler |
ServerAuthenticationFailureHandler | AuthenticationFailureHandler | 登陆失败Handler |
ReactiveAuthorizationManager | AuthorizationManager | 认证管理 |
ServerSecurityContextRepository | SecurityContextHolder | 认证信息存储管理 |
ReactiveUserDetailsService | UserDetailsService | 用户登录 |
ReactiveAuthorizationManager | AccessDecisionManager | 鉴权管理 |
ServerAuthenticationEntryPoint | AuthenticationEntryPoint | 未认证Handler |
ServerAccessDeniedHandler | AccessDeniedHandler | 鉴权失败Handler |
ScAccessDeniedHandler
/**
* 权限认证失败执行
*
* @author zhe.xiao
* @date 2021-04-12 17:33
* @description
*/
@Slf4j
@Component
public class ScAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
HashMap<String, String> map = new HashMap<>();
map.put("code", "000000");
map.put("message", "未授权禁止访问");
log.error("access forbidden path={}", exchange.getRequest().getPath());
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
return response.writeWith(Mono.just(dataBuffer));
}
}
ScAuthenticationEntryPoint
/**
* 认证失败执行
* @author zhe.xiao
* @date 2021-04-15 11:54
* @description
*/
@Slf4j
@Component
public class ScAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
HashMap<String, String> map = new HashMap<>();
map.put("code", "000000");
map.put("message", "未授权禁止访问");
log.error("access forbidden path={}", exchange.getRequest().getPath());
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
return response.writeWith(Mono.just(dataBuffer));
}
}
ScSecurityContextRepository
/**
* 1. 把header拿到的token放入AuthenticationToken
*
* @author zhe.xiao
* @date 2021-04-14 23:32
* @description
*/
@Slf4j
@Component
public class ScSecurityContextRepository implements ServerSecurityContextRepository {
@Autowired
ScAuthenticationManager scAuthenticationManager;
@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
String authorization = request.getHeaders().getFirst("Authorization");
log.info("ScSecurityContextRepository authorization = {}", authorization);
return scAuthenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(authorization, null))
.map(SecurityContextImpl::new);
}
}
ScAuthenticationManager
/**
* 2. 从AuthenticationToken读取Token并做用户数据解析
*
* @author zhe.xiao
* @date 2021-04-14 23:35
* @description
*/
@Slf4j
@Component
public class ScAuthenticationManager implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String tokenString = (String) authentication.getPrincipal();
//校验token
ScUser scUser = parseToken(tokenString);
log.info("ScAuthenticationManager scUser = {}", scUser);
return Mono.just(authentication).map(auth -> {
return new UsernamePasswordAuthenticationToken(scUser, null, null);
});
}
/**
* 校验token
*
* @param tokenString
* @return
*/
private ScUser parseToken(String tokenString) {
//读取token
String jwtToken = getJwtToken(tokenString);
log.info("ScAuthenticationManager jwtToken = {}", jwtToken);
//模拟认证成功
if (StringUtils.hasText(jwtToken) && jwtToken.startsWith("a")) {
return new ScUser().setId(1L).setName("zhexiao");
}
return null;
}
/**
* 读取Jwt Token
*
* @param authorization
* @return
*/
private String getJwtToken(String authorization) {
if (!StringUtils.hasText(authorization)) {
return null;
}
boolean valid = authorization.startsWith("Bearer ");
if (!valid) {
return null;
}
return authorization.replace("Bearer ", "");
}
}
ScAuthorizationManager
/**
* 3. 权限验证,是否放行
*
* @author zhe.xiao
* @date 2021-04-15 11:12
* @description
*/
@Slf4j
@Component
public class ScAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
return authentication.map(auth -> {
ScUser scUser = (ScUser) auth.getPrincipal();
log.info("ScAuthorizationManager scUser = {}", scUser);
if (Objects.isNull(scUser)) {
return new AuthorizationDecision(false);
}
return new AuthorizationDecision(true);
}).defaultIfEmpty(new AuthorizationDecision(false));
}
}
ScFilter
/**
* 4. 请求通过后的额外操作处理
*
* @author zhe.xiao
* @date 2021-04-13 15:01
* @description
*/
@Slf4j
public class ScFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
log.info("UserFilter doing.... path={}", exchange.getRequest().getPath());
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!Objects.isNull(authentication)) {
Object principal = authentication.getPrincipal();
log.info("UserFilter doing principal={}", principal);
}
return chain.filter(exchange);
}
}
WebSecurityConfig 核心配置
/**
* @author zhe.xiao
* @date 2021-04-12 17:02
* @description
*/
@Configuration
@EnableWebFluxSecurity
public class WebSecurityConfig {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
ScSecurityContextRepository scSecurityContextRepository;
@Autowired
ScAuthenticationManager scAuthenticationManager;
@Autowired
ScAuthorizationManager scAuthorizationManager;
@Autowired
ScAccessDeniedHandler scAccessDeniedHandler;
@Autowired
ScAuthenticationEntryPoint scAuthenticationEntryPoint;
/**
* 访问权限授权
*
* @param http
* @return
*/
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf().disable()
.securityContextRepository(scSecurityContextRepository) //存储认证信息
.authenticationManager(scAuthenticationManager) //认证管理
.authorizeExchange(exchange -> exchange // 请求拦截处理
.pathMatchers("/favicon.ico", "/api-user/user/**").permitAll()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange().access(scAuthorizationManager) //权限
)
.addFilterAfter(new ScFilter(), SecurityWebFiltersOrder.AUTHORIZATION) //拦截处理
.exceptionHandling().accessDeniedHandler(scAccessDeniedHandler) //权限认证失败
.and()
.exceptionHandling().authenticationEntryPoint(scAuthenticationEntryPoint); //认证失败
return http.build();
}
}
测试
- /api-user/user/a和/api-user/user/b 直接放行。
- /api-user/auth/c未加token,不放行
- /api-user/auth/d 加token,放行。(注意我的认证是模拟token字符串以a开头就属于合法)
不以a开头则认为是不合法的token。
更多推荐
已为社区贡献3条内容
所有评论(0)