天天消息!Sping Security前后端分离两种方案
本篇文章是基于Spring Security实现前后端分离登录认证及权限控制的实战,主要包括以下四方面内容:
(资料图片)
Spring Security是基于Spring框架,提供了一套Web应用安全性的完整解决方案。关于安全方面的两个核心功能是认证和授权,Spring Security重要核心功能就是实现用户认证(Authentication)和用户授权(Authorization)。
认证(Authentication)认证是用来验证某个用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。
授权(Authorization)授权发生在认证之后,用来验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
实现简单介绍Spring Security进行认证和鉴权的时候,采用的一系列的Filter来进行拦截的。 下图是Spring Security基于表单认证授权的流程,在Spring Security一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。
准备阶段整个项目结构如下,demo1部分是基于表单的认证,demo2部分是基于Token的认证,数据库采用是Mysql,访问数据库使用的JPA,Spring Boot版本是2.7.8,Spring Security版本是比较新的5.7.6,这里需要注意的是Spring Security5.7以后版本和前面的版本有一些差异,未来该Demo的版本的问题一直会持续保持升级。后续也会引用前端项目,前端后台管理部分我个人感觉后端程序员也要进行简单的掌握一些,便于工作中遇到形形色色问题更好的去处理。
Maven关于Maven部分细节这里不进行展示了,采用的父子工程,主要简单看下依赖的版本,
8 8 2.7.8 5.1.46 1.16.14 0.11.1 1.2.75
统一错误码publicenumResultCode{/*成功*/SUCCESS(200,"成功"),/*默认失败*/COMMON_FAIL(999,"失败"),/*参数错误:1000~1999*/PARAM_NOT_VALID(1001,"参数无效"),PARAM_IS_BLANK(1002,"参数为空"),PARAM_TYPE_ERROR(1003,"参数类型错误"),PARAM_NOT_COMPLETE(1004,"参数缺失"),/*用户错误*/USER_NOT_LOGIN(2001,"用户未登录"),USER_ACCOUNT_EXPIRED(2002,"账号已过期"),USER_CREDENTIALS_ERROR(2003,"密码错误"),USER_CREDENTIALS_EXPIRED(2004,"密码过期"),USER_ACCOUNT_DISABLE(2005,"账号不可用"),USER_ACCOUNT_LOCKED(2006,"账号被锁定"),USER_ACCOUNT_NOT_EXIST(2007,"账号不存在"),USER_ACCOUNT_ALREADY_EXIST(2008,"账号已存在"),USER_ACCOUNT_USE_BY_OTHERS(2009,"账号下线"),/*业务错误*/NO_PERMISSION(3001,"没有权限");privateIntegercode;privateStringmessage;ResultCode(Integercode,Stringmessage){this.code=code;this.message=message;}publicIntegergetCode(){returncode;}publicvoidsetCode(Integercode){this.code=code;}publicStringgetMessage(){returnmessage;}publicvoidsetMessage(Stringmessage){this.message=message;}privatestaticMapmap=newHashMap<>();privatestaticMapdescMap=newHashMap<>();static{for(ResultCodevalue:ResultCode.values()){map.put(value.getCode(),value);descMap.put(value.getMessage(),value);}}publicstaticResultCodegetByCode(Integercode){returnmap.get(code);}publicstaticResultCodegetByMessage(Stringdesc){returndescMap.get(desc);}}
统一返回定义publicclassCommonResponseimplementsSerializable{/***成功状态码*/privatefinalstaticStringSUCCESS_CODE="SUCCESS";/***提示信息*/privateStringmessage;/***返回数据*/privateTdata;/***状态码*/privateIntegercode;/***状态*/privateBooleanstate;/***错误明细*/privateStringdetailMessage;/***成功**@param泛型*@return返回结果*/publicstaticCommonResponseok(){returnok(null);}/***成功**@paramdata传入的对象*@param泛型*@return返回结果*/publicstaticCommonResponseok(Tdata){CommonResponseresponse=newCommonResponse();response.code=ResultCode.SUCCESS.getCode();response.data=data;response.message="返回成功";response.state=true;returnresponse;}/***错误**@paramcode自定义code*@parammessage自定义返回信息*@param泛型*@return返回信息*/publicstaticCommonResponseerror(Integercode,Stringmessage){returnerror(code,message,null);}/***错误**@paramcode自定义code*@parammessage自定义返回信息*@paramdetailMessage错误详情信息*@param泛型*@return返回错误信息*/publicstaticCommonResponseerror(Integercode,Stringmessage,StringdetailMessage){CommonResponseresponse=newCommonResponse();response.code=code;response.data=null;response.message=message;response.state=false;response.detailMessage=detailMessage;returnresponse;}publicBooleangetState(){returnstate;}publicCommonResponsesetState(Booleanstate){this.state=state;returnthis;}publicStringgetMessage(){returnmessage;}publicCommonResponsesetMessage(Stringmessage){this.message=message;returnthis;}publicTgetData(){returndata;}publicCommonResponsesetData(Tdata){this.data=data;returnthis;}publicIntegergetCode(){returncode;}publicCommonResponsesetCode(Integercode){this.code=code;returnthis;}publicStringgetDetailMessage(){returndetailMessage;}publicCommonResponsesetDetailMessage(StringdetailMessage){this.detailMessage=detailMessage;returnthis;}}
数据库设计基于RBAC模型最简单奔版本的数据库设计,用户、角色、权限表;
基于表单认证对于表单认证整体过程可以参考下图,
核心配置核心配置包含了框架开启以及权限配置,这部分内容是重点要关注的,这里可以看到所有重写的内容,主要包含以下七方面内容:
当然Spring Security还支持更多内容,比如限制用户登录个数等等,这里部分内容使用不是太多,后续大家如果有需要我也可以进行补充。
//SpringSecurity框架开启@EnableWebSecurity//授权全局配置@EnableGlobalMethodSecurity(prePostEnabled=true)@ConfigurationpublicclassSecurityConfig{@AutowiredprivateSysUserServicesysUserService;@AutowiredprivateNotAuthenticationConfignotAuthenticationConfig;@BeanSecurityFilterChainfilterChain(HttpSecurityhttp)throwsException{//支持跨域http.cors().and()//csrf关闭.csrf().disable()//配置哪些需要认证哪些不需要认证.authorizeRequests(rep->rep.antMatchers(notAuthenticationConfig.getPermitAllUrls().toArray(newString[0])).permitAll().anyRequest().authenticated()).exceptionHandling()//认证异常处理.authenticationEntryPoint(newResourceAuthExceptionEntryPoint())//授权异常处理.accessDeniedHandler(newCustomizeAccessDeniedHandler())//登录认证处理.and().formLogin().successHandler(newCustomizeAuthenticationSuccessHandler()).failureHandler(newCustomizeAuthenticationFailureHandler())//登出.and().logout().permitAll().addLogoutHandler(newCustomizeLogoutHandler()).logoutSuccessHandler(newCustomizeLogoutSuccessHandler()).deleteCookies("JSESSIONID")//自定义认证.and().userDetailsService(sysUserService);returnhttp.build();}@BeanpublicPasswordEncoderpasswordEncoder(){BCryptPasswordEncoderbCryptPasswordEncoder=newBCryptPasswordEncoder();returnbCryptPasswordEncoder;}@Bean("ssc")publicSecuritySecurityCheckServicepermissionService(){returnnewSecuritySecurityCheckService();}}
通过注解形式实现哪些需要资源不需要认证通过自定义注解@NotAuthentication,然后通过实现InitializingBean接口,实现加载不需要认证的资源,支持类和方法,使用就是通过在方法或者类打上对应的注解。
@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD,ElementType.TYPE})public@interfaceNotAuthentication{}@ServicepublicclassNotAuthenticationConfigimplementsInitializingBean,ApplicationContextAware{privatestaticfinalStringPATTERN="\\{(.*?)}";publicstaticfinalStringASTERISK="*";privateApplicationContextapplicationContext;@Getter@SetterprivateListpermitAllUrls=newArrayList<>();@OverridepublicvoidafterPropertiesSet()throwsException{RequestMappingHandlerMappingmapping=applicationContext.getBean(RequestMappingHandlerMapping.class);Mapmap=mapping.getHandlerMethods();map.keySet().forEach(x->{HandlerMethodhandlerMethod=map.get(x);//获取方法上边的注解替代pathvariable为*NotAuthenticationmethod=AnnotationUtils.findAnnotation(handlerMethod.getMethod(),NotAuthentication.class);Optional.ofNullable(method).ifPresent(inner->Objects.requireNonNull(x.getPathPatternsCondition()).getPatternValues().forEach(url->permitAllUrls.add(url.replaceAll(PATTERN,ASTERISK))));//获取类上边的注解,替代pathvariable为*NotAuthenticationcontroller=AnnotationUtils.findAnnotation(handlerMethod.getBeanType(),NotAuthentication.class);Optional.ofNullable(controller).ifPresent(inner->Objects.requireNonNull(x.getPathPatternsCondition()).getPatternValues().forEach(url->permitAllUrls.add(url.replaceAll(PATTERN,ASTERISK))));});}@OverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{this.applicationContext=applicationContext;}}
自定义认证异常实现AuthenticationEntryPoint�用来解决匿名用户访问无权限资源时的异常。
publicclassResourceAuthExceptionEntryPointimplementsAuthenticationEntryPoint{@Overridepublicvoidcommence(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionauthException)throwsIOException,ServletException{CommonResponseresult=CommonResponse.error(ResultCode.USER_NOT_LOGIN.getCode(),ResultCode.USER_NOT_LOGIN.getMessage());response.setCharacterEncoding("UTF-8");response.setContentType("application/json;charset=utf-8");response.getWriter().write(JSON.toJSONString(result));}}
自定义授权异常实现AccessDeniedHandler用来解决认证过的用户访问无权限资源时的异常。
publicclassCustomizeAccessDeniedHandlerimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptionaccessDeniedException)throwsIOException,ServletException{CommonResponseresult=CommonResponse.error(ResultCode.NO_PERMISSION.getCode(),ResultCode.NO_PERMISSION.getMessage());//处理编码方式,防止中文乱码的情况response.setContentType("text/json;charset=utf-8");//塞到HttpServletResponse中返回给前台response.getWriter().write(JSON.toJSONString(result));}}
自定义登录成功、失败AuthenticationSuccessHandler和AuthenticationFailureHandler这两个接口用于登录成功失败以后的处理。
publicclassCustomizeAuthenticationSuccessHandlerimplementsAuthenticationSuccessHandler{@OverridepublicvoidonAuthenticationSuccess(HttpServletRequestrequest,HttpServletResponseresponse,Authenticationauthentication)throwsIOException,ServletException{AuthUserauthUser=(AuthUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();//返回json数据CommonResponseresult=CommonResponse.ok(authUser);//处理编码方式,防止中文乱码的情况response.setContentType("text/json;charset=utf-8");//塞到HttpServletResponse中返回给前台response.getWriter().write(JSON.toJSONString(result));}}publicclassCustomizeAuthenticationFailureHandlerimplementsAuthenticationFailureHandler{@OverridepublicvoidonAuthenticationFailure(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionexception)throwsIOException,ServletException{//返回json数据CommonResponseresult=null;if(exceptioninstanceofAccountExpiredException){//账号过期result=CommonResponse.error(ResultCode.USER_ACCOUNT_EXPIRED.getCode(),ResultCode.USER_ACCOUNT_EXPIRED.getMessage());}elseif(exceptioninstanceofBadCredentialsException){//密码错误result=CommonResponse.error(ResultCode.USER_CREDENTIALS_ERROR.getCode(),ResultCode.USER_CREDENTIALS_ERROR.getMessage());//}elseif(exceptioninstanceofCredentialsExpiredException){////密码过期//result=CommonResponse.error(ResultCode.USER_CREDENTIALS_EXPIRED);//}elseif(exceptioninstanceofDisabledException){////账号不可用//result=CommonResponse.error(ResultCode.USER_ACCOUNT_DISABLE);//}elseif(exceptioninstanceofLockedException){////账号锁定//result=CommonResponse.error(ResultCode.USER_ACCOUNT_LOCKED);//}elseif(exceptioninstanceofInternalAuthenticationServiceException){////用户不存在//result=CommonResponse.error(ResultCode.USER_ACCOUNT_NOT_EXIST);}else{//其他错误result=CommonResponse.error(ResultCode.COMMON_FAIL.getCode(),ResultCode.COMMON_FAIL.getMessage());}//处理编码方式,防止中文乱码的情况response.setContentType("text/json;charset=utf-8");//塞到HttpServletResponse中返回给前台response.getWriter().write(JSON.toJSONString(result));}}
自定义登出LogoutHandler自定义登出以后处理逻辑,比如记录在线时长等等;LogoutSuccessHandler登出成功以后逻辑处理。
publicclassCustomizeLogoutSuccessHandlerimplementsLogoutSuccessHandler{@OverridepublicvoidonLogoutSuccess(HttpServletRequestrequest,HttpServletResponseresponse,Authenticationauthentication)throwsIOException,ServletException{CommonResponseresult=CommonResponse.ok();response.setContentType("text/json;charset=utf-8");response.getWriter().write(JSON.toJSONString(result));}}publicclassCustomizeLogoutHandlerimplementsLogoutHandler{@Overridepublicvoidlogout(HttpServletRequestrequest,HttpServletResponseresponse,Authenticationauthentication){}}
自定义认证自定义认证涉及三个对象UserDetialsService、UserDetails以及PasswordEncoder,整个流程首先根据用户名查询出用户对象交由UserDetialsService接口处理,该接口只有一个方法loadUserByUsername,通过用户名查询用户对象。查询出来的用户对象需要通过Spring Security中的用户数据UserDetails实体类来体现,这里使用AuthUser继承User,User本质上就是继承与UserDetails,UserDetails该类中提供了账号、密码等通用属性。对密码进行校验使用PasswordEncoder组件,负责密码加密与校验。
publicclassAuthUserextendsUser{publicAuthUser(Stringusername,Stringpassword,Collectionauthorities){super(username,password,authorities);}}@ServicepublicclassSysUserServiceimplementsUserDetailsService{@AutowiredprivateSysUserRepositorysysUserRepository;@AutowiredprivateSysRoleServicesysRoleService;@AutowiredprivateSysMenuServicesysMenuService;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{OptionalsysUser=Optional.ofNullable(sysUserRepository.findOptionalByUsername(username).orElseThrow(()->newUsernameNotFoundException("未找到用户名")));Listroles=sysRoleService.queryByUserName(sysUser.get().getUsername());SetdbAuthsSet=newHashSet<>();if(!CollectionUtils.isEmpty(roles)){//角色roles.forEach(x->{dbAuthsSet.add("ROLE_"+x.getName());});ListroleIds=roles.stream().map(SysRole::getId).collect(Collectors.toList());Listmenus=sysMenuService.queryByRoleIds(roleIds);//菜单Setpermissions=menus.stream().filter(x->x.getType().equals(3)).map(SysMenu::getPermission).collect(Collectors.toSet());dbAuthsSet.addAll(permissions);}Collectionauthorities=AuthorityUtils.createAuthorityList(dbAuthsSet.toArray(newString[0]));returnnewAuthUser(username,sysUser.get().getPassword(),authorities);}}
基于Token认证基于Token认证这里我采用JWT方式,下图是整个处理的流程,通过自定义的登录以及JwtAuthenticationTokenFilter来完成整个任务的实现,需要注意的是这里我没有使用缓存。
核心配置与表单认证不同的是这里关闭和FormLogin表单认证以及不使用Session方式,增加了JwtAuthenticationTokenFilter,此外ResourceAuthExceptionEntryPoint兼职处理之前登录失败以后的异常处理。
@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true)@ConfigurationpublicclassSecurityConfig{@AutowiredprivateSysUserServicesysUserService;@AutowiredprivateNotAuthenticationConfignotAuthenticationConfig;@BeanSecurityFilterChainfilterChain(HttpSecurityhttp)throwsException{//支持跨域http.cors().and()//csrf关闭.csrf().disable()//不使用session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests(rep->rep.antMatchers(notAuthenticationConfig.getPermitAllUrls().toArray(newString[0])).permitAll().anyRequest().authenticated()).exceptionHandling()//异常认证.authenticationEntryPoint(newResourceAuthExceptionEntryPoint()).accessDeniedHandler(newCustomizeAccessDeniedHandler()).and()//token过滤.addFilterBefore(newJwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class).userDetailsService(sysUserService);returnhttp.build();}/***获取AuthenticationManager**@paramconfiguration*@return*@throwsException*/@BeanpublicAuthenticationManagerauthenticationManager(AuthenticationConfigurationconfiguration)throwsException{returnconfiguration.getAuthenticationManager();}/***密码**@return*/@BeanpublicPasswordEncoderpasswordEncoder(){BCryptPasswordEncoderbCryptPasswordEncoder=newBCryptPasswordEncoder();returnbCryptPasswordEncoder;}@Bean("ssc")publicSecuritySecurityCheckServicepermissionService(){returnnewSecuritySecurityCheckService();}}
Token创建@ServicepublicclassLoginService{@AutowiredprivateAuthenticationManagerauthenticationManager;@AutowiredprivateSysUserServicesysUserService;publicLoginVOlogin(LoginDTOloginDTO){//创建Authentication对象UsernamePasswordAuthenticationTokenauthenticationToken=newUsernamePasswordAuthenticationToken(loginDTO.getUsername(),loginDTO.getPassword());//调用AuthenticationManager的authenticate方法进行认证Authenticationauthentication=authenticationManager.authenticate(authenticationToken);if(authentication==null){thrownewRuntimeException("用户名或密码错误");}//登录成功以后用户信息、AuthUserauthUser=(AuthUser)authentication.getPrincipal();LoginVOloginVO=newLoginVO();loginVO.setUserName(authUser.getUsername());loginVO.setAccessToken(JwtUtils.createAccessToken(authUser));loginVO.setRefreshToken(JwtUtils.createRefreshToken(authUser));returnloginVO;}publicLoginVOrefreshToken(StringaccessToken,StringrefreshToken){if(!JwtUtils.validateRefreshToken(refreshToken)&&!JwtUtils.validateWithoutExpiration(accessToken)){thrownewRuntimeException("认证失败");}OptionaluserName=JwtUtils.parseRefreshTokenClaims(refreshToken).map(Claims::getSubject);if(userName.isPresent()){AuthUserauthUser=sysUserService.loadUserByUsername(userName.get());if(Objects.nonNull(authUser)){LoginVOloginVO=newLoginVO();loginVO.setUserName(authUser.getUsername());loginVO.setAccessToken(JwtUtils.createAccessToken(authUser));loginVO.setRefreshToken(JwtUtils.createRefreshToken(authUser));returnloginVO;}thrownewInternalAuthenticationServiceException("用户不存在");}thrownewRuntimeException("认证失败");}}
Token过滤publicclassJwtAuthenticationTokenFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainchain)throwsServletException,IOException{//checkTokenif(checkJWTToken(request)){//解析token中的认证信息OptionalclaimsOptional=validateToken(request).filter(claims->claims.get("authorities")!=null);if(claimsOptional.isPresent()){ListauthoritiesList=castList(claimsOptional.get().get("authorities"),String.class);Listauthorities=authoritiesList.stream().map(String::valueOf).map(SimpleGrantedAuthority::new).collect(Collectors.toList());UsernamePasswordAuthenticationTokenusernamePasswordAuthenticationToken=newUsernamePasswordAuthenticationToken(claimsOptional.get().getSubject(),null,authorities);SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}else{SecurityContextHolder.clearContext();}}chain.doFilter(request,response);}publicstaticListcastList(Objectobj,Classclazz){Listresult=newArrayList();if(objinstanceofList>){for(Objecto:(List>)obj){result.add(clazz.cast(o));}returnresult;}returnnull;}privateOptionalvalidateToken(HttpServletRequestreq){StringjwtToken=req.getHeader("token");try{returnJwtUtils.parseAccessTokenClaims(jwtToken);}catch(ExpiredJwtException|SignatureException|MalformedJwtException|UnsupportedJwtException|IllegalArgumentExceptione){//输出日志returnOptional.empty();}}privatebooleancheckJWTToken(HttpServletRequestrequest){StringauthenticationHeader=request.getHeader("token");returnauthenticationHeader!=null;}}
授权处理全局授权的配置已经在核心配置中开启,核心思路是通过SecurityContextHolder获取当前用户权限,判断当前用户的权限是否包含该方法的权限,此部分设计后续如果存在性能问题,可以设计缓存来解决。
授权检查publicclassSecuritySecurityCheckService{publicbooleanhasPermission(Stringpermission){returnhasAnyPermissions(permission);}publicbooleanhasAnyPermissions(String...permissions){if(CollectionUtils.isEmpty(Arrays.asList(permissions))){returnfalse;}Authenticationauthentication=SecurityContextHolder.getContext().getAuthentication();if(authentication==null){returnfalse;}Collectionauthorities=authentication.getAuthorities();returnauthorities.stream().map(GrantedAuthority::getAuthority).filter(x->!x.contains("ROLE_")).anyMatch(x->PatternMatchUtils.simpleMatch(permissions,x));}publicbooleanhasRole(Stringrole){returnhasAnyRoles(role);}publicbooleanhasAnyRoles(String...roles){if(CollectionUtils.isEmpty(Arrays.asList(roles))){returnfalse;}Authenticationauthentication=SecurityContextHolder.getContext().getAuthentication();if(authentication==null){returnfalse;}Collectionauthorities=authentication.getAuthorities();returnauthorities.stream().map(GrantedAuthority::getAuthority).filter(x->x.contains("ROLE_")).anyMatch(x->PatternMatchUtils.simpleMatch(roles,x));}}
如何使用@PreAuthorize("@ssc.hasPermission("sys:user:query")")@PostMapping("/helloWord")publicStringhellWord(){return"helloword";}
跨域问题处理关于这部分跨域部分的配置还可以更加细化一点。
@ConfigurationpublicclassCorsConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddCorsMappings(CorsRegistryregistry){//设置允许跨域的路径registry.addMapping("/**")//设置允许跨域请求的域名.allowedOriginPatterns("*")//是否允许cookie.allowCredentials(true)//设置允许的请求方式.allowedMethods("GET","POST","DELETE","PUT")//设置允许的header属性.allowedHeaders("*")//跨域允许时间.maxAge(3600);}}
vue-admin-template登录的简单探索感悟这部分就是有些感悟(背景自身是没有接触过Vue相关的知识),具体的感悟就是不要畏惧一些自己不知道以及不会的东西,大胆的去尝试,因为自身的潜力是很大的。为什么要这么讲,通过自己折腾3个小时,自己完成整个登录过程,如果是前端可能会比较简单,针对我这种从没接触过的还是有些难度的,需要一些基础配置更改以及流程梳理,这里简单来让大家看下效果,后续我也会自己把剩下菜单动态加载以及一些简单表单交互来完成,做到简单的表单可以自己来实现。
结束欢迎大家点点关注,点点赞! 具体代码可参考github
关键词:
- 新品上市!劲爆来袭!叼嘴巴叼嘴王胶姆糖重磅招商中!
- 边城战“疫”:夜晚七点的暂停键
- 风雪高原战“疫”长卷 寒潮下的西宁疫情防控观察
- 海口市1例治愈后的境外输入病例复阳 已转至定点医院隔离
- 四川新增本土确诊病例4例
- 黑龙江省新增新冠肺炎本土确诊病例6例
- 河南新增本土确诊病例18例 其中郑州市16例周口市2例
- 河北新增确诊病例8例 新增无症状感染者1例
-
寒潮持续发威!南方气温纷纷触底 强降雪中心转移至东北
中国天气网讯 今天(11月8日),寒潮继续南下,持续发威,南方大部最高气温将纷纷触底。强降雪中心将转移至东北地区,预计今天,黑龙江
-
雪后寒!今日北京晴天回归北风劲吹 最高气温5℃上下
中国天气网讯 今天(11月8日)北京晴天回归,但在风寒效应下,“冷”仍然是天气的主题。气温方面,今天北京最高气温在5℃左右,最低气温
-
黑龙江新增本土确诊病例6例 均在黑河市爱辉区
中新网11月8日电 据黑龙江省卫健委网站消息,2021年11月7日0-24时,黑龙江省新增新冠肺炎本土确诊病例6例(黑河市爱辉区6例),均为集中
X 关闭
X 关闭