- Nacos-注册中心搭建
1.1 注册中心Nacos与Eureka对比
在此项目中naocs服务器是通过mysql来进行连接的,nacos不用手动搭建服务器,对于开发者来说,上手很快。
1.2 Nacos安装和启动
nacos 的下载和启动方法请参考Nacos 官网。
在启动nacos2.01的时候,有个坑,默认启动方式是以集群的方式启动,需要修改, 直接使用命令启动
startup.sh -m standalone
1.3 注册中心客户端搭建
Pom依赖导入:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
注解添加:
注解添加,交给nacos注册中心管理服务@EnableDiscoveryClient.
Yml文件配置添加:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8000
交给注册中心管理之后的服务会在nacos服务器上的服务列表多出一个服务,可以实时查看服务状态。
2. Nacos-配置中心搭建
2.1 配置中心Nacos与Config区别
搭建:config需要手动搭建服务,nacos不需要。
动态变更:spring cloud config大部分场景结合git 使用, 动态变更还需要依赖Spring Cloud Bus 消息总线来通过所有的客户端变化.
nacos config使用长连接更新配置, 一旦配置有变动后,通知Provider的过程非常的迅速, 从速度上秒杀springcloud原来的config几条街.
2.2 配置中心客户端搭建
pom依赖:
<!--nacos config client 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
客户端不需要添加注解,导入依赖之后会自动加载配置文件中配置中的的地址。
客户端Yml文件配置:
spring:
cloud:
nacos:
config:
enabled: true # 如果不想使用 Nacos 进行配置管理,设置为 false 即可
server-addr: 127.0.0.1:8000 # Nacos Server 地址
prefix: test-application
file-extension: yml # 配置内容的数据格式,默认为 properties
group: pull # 组,默认为 DEFAULT_GROUP
存储在配置中心的文件:
3. Gateway-网关搭建3.1 网关Gateway与Zuul区别
3.2 网关gateway-server搭建
- 将此服务交给nacos管理
- 配置信息从配置中心拉取
- 网关依赖导入:
<!--GateWay 网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 配置各个服务的路由:
spring:
cloud:
gateway:
routes:
- id: test1-server
uri: http://localhost:9100
predicates:
- Path=/test1/**
filters:
- StripPrefix=1
- id: test2-server
uri: http://localhost:9101
predicates:
- Path=/test2/**
- filters:
- StripPrefix=1
- id: test3-server
uri: http://localhost:9102
predicates:
- Path=/test3/**
filters:
- StripPrefix=1
- Dubbo-服务间调用搭建
pom文件依赖导入:
<!-- Dubbo Spring Cloud Starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
dubbo服务提供者+注解@service(此注解为dubbo的注解):
dubbo:
scan:
base-packages: com.dc.dubbo.provider
protocol:
name: dubbo
port: -1
registry:
address: nacos://127.0.0.1:8000
Nacos配置文件服务消费者(gateway):
dubbo:
cloud:
subscribed-services: test-server
registry:
address: nacos://127.0.0.1:8000
consumer:
check: false
timeout: 20000
使用dubbo产生的服务列表:
5. Spring Cloud Security
鉴权解决方案:在网关(gateway)集成security实现微服务统一鉴权.
优势:简单,省去了微服务之间的鉴权操作.
引入security包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
5.1. 配置security安全策略
@EnableWebFluxSecurity
public class SecurityConfig {
// 登录接口地址
private static final String LOGIN_URI = "/login";
// 放行的uri地址(所有请求方式)
private static final String[] EXCLUDED_AUTH_URI_ALL_METHOD = {
"/plugins/api/v1/test"
};
// 放行的uri地址(GET)
private static final String[] EXCLUDED_AUTH_URI_GET_METHOD = {
"/plugins/api/v1/test2"
};
@Resource
private MyAuthorizationManager authorizationManager;
@Resource
private MyServerAuthenticationSuccessHandler serverAuthenticationSuccessHandler;
@Resource
private MyServerAuthenticationFailureHandler serverAuthenticationFailureHandler;
@Resource
private MyServerAuthenticationEntryPoint serverAuthenticationEntryPoint;
@Resource
private MyServerAccessDeniedHandler serverAccessDeniedHandler;
@Resource
private MyReactiveAuthenticationManager reactiveAuthenticationManager;
@Resource
private MyCorsConfigurationSource corsConfigurationSource;
@Resource
private MyServerSecurityContextRepository securityContextRepository;
/**
* 配置安全策略
*
* @param http http安全请求配置对象
* @return org.springframework.security.web.server.SecurityWebFilterChain
* @author Reagan
*/
@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
//无需进行权限过滤的请求路径
.pathMatchers(EXCLUDED_AUTH_URI_ALL_METHOD).permitAll()
.pathMatchers(HttpMethod.GET, EXCLUDED_AUTH_URI_GET_METHOD).permitAll()
//option 请求默认放行
.pathMatchers(HttpMethod.OPTIONS).permitAll()
//自定义的鉴权服务,通过鉴权的才能继续访问某个请求
.anyExchange()
.access(authorizationManager)
.and()
.httpBasic()
// 登录接口地址配置
.and().formLogin().loginPage(LOGIN_URI)
// 认证管理器
.authenticationManager(reactiveAuthenticationManager)
//认证成功
.authenticationSuccessHandler(serverAuthenticationSuccessHandler)
//认证失败
.authenticationFailureHandler(serverAuthenticationFailureHandler)
.and()
.exceptionHandling()
//基于http的接口请求鉴权失败
.authenticationEntryPoint(serverAuthenticationEntryPoint)
.accessDeniedHandler(serverAccessDeniedHandler)
.and()
.securityContextRepository(securityContextRepository)
.logout().disable()
.csrf().disable().cors().configurationSource(corsConfigurationSource)
.and()
.headers().cache().disable();
return http.build();
}M
}
- @EnableWebFluxSecurity注解:开启security.
- MyReactiveAuthenticationManager: 自定义认证管理器,查询用户,校验密码.
- MyServerAuthenticationSuccessHandler: 自定义认证成功处理器,认证成功返回token.
- MyServerAuthenticationFailureHandler: 自定义认证失败处理器, 认证失败返回异常信息.
- MyServerAuthenticationEntryPoint: 自定义认证入口,没有认证的用户访问资源接口,引导用户前往登录(前后分离,直接返回401由交给前端引导登录).
- MyAuthorizationManager: 自定义鉴权管理, 用户权限决策.
- MyServerAccessDeniedHandler: 自定义鉴权失败处理器,用户无权访问资源时的处理方式(提示用户无权限).
- MyCorsConfigurationSource: 跨域管理.
- MyServerSecurityContextRepository:自定义security上下文管理仓库.
5.2. MyReactiveAuthenticationManager认证管理
@Component
public class MyReactiveAuthenticationManager implements ReactiveAuthenticationManager {
@Resource
private SecurityUserService securityUserService;
@Resource
private AbstractAuthenticationCheck authenticationCheck;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
// 获取表单提交的用户名和密码
String username= authentication.getName();
String password = authentication.getCredentials().toString();
return securityUserService.findByUsername(username).publishOn(Schedulers.parallel())
// 过滤掉为null的
.filter(Objects::nonNull)
// 如果没有Mono里面元素了就说明没有查询到用户,抛出错误信息
.switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("用户没有找到:" + username))))
// 校验用户信息, 并返回验证信息
.map(userDetails -> {
// 用户信息校验
authenticationCheck.check(userDetails, password);
// 校验通过,组装验证信息
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
});
}
}
- securityUserService.findByUsername(email) 根据用户名查询用户信息.
- authenticationCheck.check(userDetails, password); 自定义用户信息校验.
5.3. MyServerAuthenticationSuccessHandler认证成功处理
@Component
public class MyServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
@Reference
private UserService userService;
@Resource
private RedisTemplate<String, Authentication> redisTemplate;
@Resource
private LoginConfigProperties loginConfigProperties;
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
// 取用户信息
SecurityUser securityUser = ((SecurityUser) authentication.getPrincipal());
String userName= securityUser.getUserName();
String relName= securityUser.getRelName();
// 取角色列表
String roleCodes = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining());
// 生成token
String token = SecurityUtils.makeToken();
// 存储身份信息
redisTemplate.opsForValue().set(token, authentication, loginConfigProperties.getTimeout(), TimeUnit.MINUTES);
// 查询角色有权限的菜单
List<Integer> roleIdList = securityUser.getShopifyRoleList().stream().map(ShopifyRole::getId).collect(Collectors.toList());
List<ShopifyMenu> shopifyMenuList = userService.findMenuByRoleIds(roleIdList);
// 组装响应的数据
JSONObject result = new JSONObject();
result.put("token", token);
result.put("username", userName);
result.put("relName", relName);
result.put("role", roleCodes);
result.put("menu", shopifyMenuList);
// 写响应并返回
return ResponseUtils.write(webFilterExchange, BaseVo.success(result));
}
}
- loginConfigProperties.getTimeout() 读取登录过期时间
- userService.findMenuByRoleIds 查询角色对应的菜单
- ResponseUtils.write() 写响应的工具类
- SecurityUser 是UserDetail(security框架中的用户信息类)的子类
5.4. MyServerAuthenticationFailureHandler认证失败处理
@Component
public class MyServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
BaseVo<?> fail = BaseVo.fail(exception.getMessage());
// 取响应对象
return ResponseUtils.write(webFilterExchange, fail);
}
}
- ResponseUtils.write() 写响应的工具类
- 认证失败直接返回错误信息
5.5. MyServerAuthenticationEntryPoint认证入口
@Component
public class MyServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
return Mono.fromRunnable(() -> exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED));
}
}
- 直接返回401交给前端引导用户登录.
5.6. MyAuthorizationManager鉴权管理
@Component
public class MyAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
@Reference
private UserService userService;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
return authentication
// 过滤掉没有认证过的
.filter(Authentication::isAuthenticated)
// 授权抉择
.map(auth -> decision(auth, authorizationContext));
}
/**
* 决策
*
* @param authentication 认证信息
* @param authorizationContext 认证上下文(请求+响应【)
* @return org.springframework.security.authorization.AuthorizationDecision
* @author Reangan 下午
*/
private AuthorizationDecision decision(Authentication authentication, AuthorizationContext authorizationContext) {
// 取请求对象
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
// 取地址
String requestUrl = request.getURI().getPath();
// 取权限列表(角色代码)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 转成角色代码
List<String> roleCodeList = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 权限验证
boolean isGranted = userService.findPermissionByRole(roleCodeList)
.stream()
// 取permission uri
.map(ShopifyPermission::getPermissionUrl)
// 有一个匹配就算是鉴权通过
.anyMatch(uri -> ANT_PATH_MATCHER.match(uri, requestUrl));
// 如果授权通过,把用户信息设置到request
if (isGranted) {
// 取用户信息
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
addUserInfo(authorizationContext, securityUser);
}
// 返回授权对象
return new AuthorizationDecision(isGranted);
}
/**
* 添加用户信息
*
* @param authorizationContext 认证上下文
* @param securityUser 用户信息
* @author Reagan 下午
*/
private void addUserInfo(AuthorizationContext authorizationContext, SecurityUser securityUser) {
ServerWebExchange exchange = authorizationContext.getExchange();
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String userInfo = Base64.getEncoder().encodeToString(ByteUtils.objToByte(toUserModel(securityUser)));
mutate.header(UserUtils.USERINFO, new String[]{userInfo});
ServerHttpRequest build = mutate.build();
exchange.mutate().request(build).build();
}
/**
* 对象转换,SecurityUser -> UserModel
*
* @param securityUser security用户
* @return com.witemedia.model.UserModel
* @author Reagan 下午
*/
private UserModel toUserModel(SecurityUser securityUser) {
UserModel userModel = new UserModel();
userModel.setShopifyRoleList(securityUser.getShopifyRoleList());
userModel.setId(securityUser.getId());
userModel.setEmail(securityUser.getEmail());
userModel.setUserStatus(securityUser.getUserStatus());
userModel.setUsername(securityUser.getUsername());
userModel.setEncryptedPassword(securityUser.getEncryptedPassword());
userModel.setMobilePhone(securityUser.getMobilePhone());
userModel.setLastPasswordChangeTime(securityUser.getLastPasswordChangeTime());
userModel.setPasswordStatusint(securityUser.getPasswordStatusint());
userModel.setUpdateTime(securityUser.getUpdateTime());
return userModel;
}
- userService.findPermissionByRole(roleCodeList) 权限查询
- UserModel是自定义的远程传输对象,用于传输用户信息的
5.7 MyServerAccessDeniedHandler鉴权失败处理
@Component
public class MyServerAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
BaseVo<?> fail = BaseVo.result(RtnCode.AUTH_VALID_ERROR.getCode(), RtnCode.AUTH_VALID_ERROR.getMessage());
return ResponseUtils.write(exchange.getResponse(), fail);
}
}
- 直接返回鉴权失败的信息(权限不足)
6.8. MyCorsConfigurationSource跨域管理
实现CorsConfigurationSource接口,然后自定义配置,示例:
@Component
public class MyCorsConfigurationSource implements CorsConfigurationSource {
// 允许的源
private static final List<String> ALLOWED_ORIGINS;
// 允许的请求方式
private static final List<String> ALLOWED_METHODS;
// 暴露给客户端的响应头
private static final List<String> EXPOSED_HEADERS;
// 允许的请求头,用户预检请求
private static final List<String> ALLOWED_HEADERS;
static {
// 允许的源
ALLOWED_ORIGINS = Arrays.asList(“”);
// 允许的请求方式
ALLOWED_METHODS = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS");
// 暴露给客户端的响应头
EXPOSED_HEADERS = Arrays.asList("Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma");
// 允许的请求头,用户预检请求
ALLOWED_HEADERS = Arrays.asList("Accept", "Authorization", "Content-Type", "Origin", "X-Requested-With");
}
@Override
public CorsConfiguration getCorsConfiguration(@NonNull ServerWebExchange exchange) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedOrigins(ALLOWED_ORIGINS);
corsConfiguration.setAllowedMethods(ALLOWED_METHODS);
corsConfiguration.setExposedHeaders(EXPOSED_HEADERS);
corsConfiguration.setAllowedHeaders(ALLOWED_HEADERS);
corsConfiguration.setMaxAge(3600L);
return corsConfiguration;
}
}
5.9. MyServerSecurityContextRepository上下文仓库
@Component
public class MyServerSecurityContextRepository implements ServerSecurityContextRepository {
private static final String AUTHORIZATION = "Authorization";
@Resource
private RedisTemplate<String, Authentication> redisTemplate;
@Resource
private LoginConfigProperties loginConfigProperties;
@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
exchange.getResponse().setStatusCode(HttpStatus.OK);
// 获取请求头中的token
String token = request.getHeaders().getFirst(AUTHORIZATION);
if (token != null && !"".equals(token)) {
// 取用户信息(redis)
Boolean isExpire = redisTemplate.hasKey(token);
// 判断token是否过期
if (Boolean.TRUE.equals(isExpire)) {
// 用token去redis取令牌
Authentication authentication = redisTemplate.opsForValue().get(token);
// 刷新缓存时间
redisTemplate.expire(token, loginConfigProperties.getTimeout(), TimeUnit.MINUTES);
// 把令牌传给Security
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
emptyContext.setAuthentication(authentication);
return Mono.just(emptyContext);
} else {
// 返回token已过期
return Mono.error(new LoadContextException(RtnCode.UNLOGIN_ERROR.getCode(), RtnCode.UNLOGIN_ERROR.getMessage()));
}
}
return Mono.error(new LoadContextException("401", "找不到身份凭据"));
}
}
从redis中加载用户信息,用于鉴权
- new LoadContextException() 自定义异常,在全局异常中拦截处理,表示加载用户信息失败(也算是鉴权失败)
暂时写到这里,这里的spring security鉴权摸索了一下,webflux框架可以深度再研究一下。