赞
踩
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搭建
<!--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 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 }
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()); }); } }
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)); } }
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);
}
}
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));
}
}
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; }
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中加载用户信息,用于鉴权
暂时写到这里,这里的spring security鉴权摸索了一下,webflux框架可以深度再研究一下。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。