irpas技术客

2万字带你从0到1搭建一套企业级微服务安全框架_步尔斯特

网络投稿 4114

💥《微服务核心技术》专栏已收录,欢迎订阅 💥

文章目录 技术栈数据交互与实现认证设计授权设计核心配置自定义权限注解权限异常处理网关处理内部流量处理

基于上面Spring Security的几十个章节的学习,想必大家对Spring Security框架已经有了一定的了解。

那么我们开始从零开始搭建一套微服务的安全框架,希望其中的一些思想能给大家一些启发。

技术栈 spiring securityjwtredisnacos registryspring cloud gatewaysentinelnacos configseatamybatismybatis-plusxxl-jobrocketmq 数据交互与实现

说到安全就会涉及认证和授权,那么对什么认证,对什么授权,于是引出如下几张表。

用户表角色表权限表

这也是典型的RBAC模型。

所有数据表以及项目源码可以搜公号【步尔斯特】回复「1024」即可获得。

有了数据表,我们来完善具体的代码实现。

数据交互的实现

部分代码:

package com.ossa.system.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ossa.common.api.bean.User; import org.springframework.stereotype.Component; @Component public interface UserMapper extends BaseMapper<User> { } package com.ossa.system.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ossa.common.api.bean.User; public interface UserService extends IService<User> { } package com.ossa.system.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ossa.common.api.bean.User; import com.ossa.system.mapper.UserMapper; import com.ossa.system.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { } 认证设计

通过登录操作完成认证,首先在配置类中应该放过登录的请求,我在这里实现一个匿名注解,会在后面给出代码和解析。

整体的设计思想:通过用户名和密码完成认证,确认用户可信,根据用户信息获取token,每次请求都带上token,完成校验。

获取传参的用户信息,用户名、密码等。String password = authUser.getPassword();将用户名、密码、封装成UsernamePasswordAuthenticationToken对象UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);获取认证管理器AuthenticationManager authenticationManager = authenticationManagerBuilder.getObject();认证Authentication authentication = authenticationManager.authenticate(authenticationToken);重写UserDetailsService,从数据库获取用户信息,以完成认证流程。认证成功后,根据认证信息生成token可将token作为key存入redis,用redis的过期时间代替jwt的token令牌的过期时间获取用户身份信息将token信息及用户信息返回。

代码实现:

@PostMapping("/login") @AnonymousAccess public ResponseEntity<Object> login(@Validated @RequestBody AuthUserDto authUser){ // 密码解密 // String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUser.getPassword()); String password = authUser.getPassword(); // 将用户名、密码、封装成UsernamePasswordAuthenticationToken对象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser.getUsername(), password); // 获取认证管理器 AuthenticationManager authenticationManager = authenticationManagerBuilder.getObject(); // 认证核心方法 Authentication authentication = authenticationManager.authenticate(authenticationToken); // // 认证成功之后,将认证信息保存至SecurityContext中 // SecurityContextHolder.getContext().setAuthentication(authentication); // 根据认证信息生成token String token = tokenProvider.createToken(authentication); // 获取用户身份信息 User one = userService.getOne(new QueryWrapper<User>().eq("username", authUser.getUsername())); UserDto userDto = new UserDto(); BeanUtils.copyProperties(one,userDto); stringRedisTemplate.opsForValue().set(properties.getOnlineKey() + token, JSONUtil.toJsonStr(userDto), properties.getTokenValidityInSeconds()/1000, TimeUnit.SECONDS); // 返回 token 与 用户信息 Map<String, Object> authInfo = new HashMap<String, Object>(2) {{ put("token", properties.getTokenStartWith() + token); put("user", userDto); }}; return ResponseEntity.ok(authInfo); } package com.ossa.system.filter; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.ossa.common.api.bean.Privilege; import com.ossa.common.api.bean.Role; import com.ossa.common.api.bean.User; import com.ossa.system.mapper.PrivilegeMapper; import com.ossa.system.mapper.RoleMapper; import com.ossa.system.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.persistence.EntityNotFoundException; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @RequiredArgsConstructor @Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { private final UserService userService; private final RoleMapper roleMapper; private final PrivilegeMapper privilegeMapper ; @Override public UserDetails loadUserByUsername(String username) { User user; org.springframework.security.core.userdetails.User userDetails; try { user = userService.getOne(new QueryWrapper<User>().eq("username", username)); } catch (EntityNotFoundException e) { // SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException throw new UsernameNotFoundException("", e); } if (user == null) { throw new UsernameNotFoundException(""); } else { List<Role> roles = roleMapper.listByUserId(user.getId()); ArrayList<Privilege> privileges = new ArrayList<>(); roles.forEach(role -> privileges.addAll(privilegeMapper.listByRoleId(role.getId()))); ArrayList<String> tag = new ArrayList<>(); privileges.forEach(p -> tag.add(p.getTag())); List<SimpleGrantedAuthority> collect = tag.stream().map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); userDetails = new org.springframework.security.core.userdetails.User(username, user.getPassword(), collect); } return userDetails; } } package com.ossa.system.filter; import cn.hutool.core.date.DateField; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; import com.ossa.common.bean.SecurityProperties; import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.security.Key; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.TimeUnit; @Slf4j @Component public class TokenProvider implements InitializingBean { private final SecurityProperties properties; private final StringRedisTemplate stringRedisTemplate; public static final String AUTHORITIES_KEY = "user"; private JwtParser jwtParser; private JwtBuilder jwtBuilder; public TokenProvider(SecurityProperties properties, StringRedisTemplate stringRedisTemplate) { this.properties = properties; this.stringRedisTemplate = stringRedisTemplate; } @Override public void afterPropertiesSet() { byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret()); Key key = Keys.hmacShaKeyFor(keyBytes); jwtParser = Jwts.parserBuilder() .setSigningKey(key) .build(); jwtBuilder = Jwts.builder() .signWith(key, SignatureAlgorithm.HS512); } /** * 创建Token 设置永不过期, * Token 的时间有效性转到Redis 维护 * * @param authentication / * @return / */ public String createToken(Authentication authentication) { return jwtBuilder // 加入ID确保生成的 Token 都不一致 .setId(IdUtil.simpleUUID()) .claim(AUTHORITIES_KEY, authentication.getName()) .setSubject(authentication.getName()) .compact(); } /** * 依据Token 获取鉴权信息 * * @param token / * @return / */ Authentication getAuthentication(String token) { Claims claims = getClaims(token); User principal = new User(claims.getSubject(), "******", new ArrayList<>()); return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>()); } public Claims getClaims(String token) { return jwtParser .parseClaimsJws(token) .getBody(); } /** * @param token 需要检查的token */ public void checkRenewal(String token) { // 判断是否续期token,计算token的过期时间 Long expire = stringRedisTemplate.getExpire(properties.getOnlineKey() + token, TimeUnit.SECONDS); long time = expire == null ? 0 : expire * 1000; Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time); // 判断当前时间与过期时间的时间差 long differ = expireDate.getTime() - System.currentTimeMillis(); // 如果在续期检查的范围内,则续期 if (differ <= properties.getDetect()) { long renew = time + properties.getRenew(); stringRedisTemplate.expire(properties.getOnlineKey() + token, renew, TimeUnit.MILLISECONDS); } } public String getToken(HttpServletRequest request) { final String requestHeader = request.getHeader(properties.getHeader()); if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) { return requestHeader.substring(7); } return null; } } 授权设计 设计自己filter,拦截我们生成的token,如果token合法,则将token解析并封装成UsernamePasswordAuthenticationToken,存到安全上下文中为了确保授权成功,我们需要将我们的filter放在UsernamePasswordAuthenticationFilter前执行 package com.ossa.system.filter; import cn.hutool.core.util.StrUtil; import com.ossa.common.bean.SecurityProperties; import io.jsonwebtoken.ExpiredJwtException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; import org.springframework.web.filter.GenericFilterBean; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; public class OssaTokenFilter extends GenericFilterBean { private static final Logger log = LoggerFactory.getLogger(OssaTokenFilter.class); private final StringRedisTemplate stringRedisTemplate; private final TokenProvider tokenProvider; private final SecurityProperties properties; /** * @param tokenProvider Token * @param properties JWT */ public OssaTokenFilter(TokenProvider tokenProvider, SecurityProperties properties, StringRedisTemplate stringRedisTemplate) { this.properties = properties; this.tokenProvider = tokenProvider; this.stringRedisTemplate = stringRedisTemplate; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String token = resolveToken(httpServletRequest); // 对于 Token 为空的不需要去查 Redis if (StrUtil.isNotBlank(token)) { String s = null; try { s = stringRedisTemplate.opsForValue().get(properties.getOnlineKey() + token); } catch (ExpiredJwtException e) { log.error(e.getMessage()); } if (s != null && StringUtils.hasText(token)) { Authentication authentication = tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); // Token 续期 tokenProvider.checkRenewal(token); } } filterChain.doFilter(servletRequest, servletResponse); } /** * 初步检测Token * * @param request / * @return / */ private String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader(properties.getHeader()); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(properties.getTokenStartWith())) { // 去掉令牌前缀 return bearerToken.replace(properties.getTokenStartWith(), ""); } else { log.debug("非法Token:{}", bearerToken); } return null; } } 核心配置 package com.ossa.common.security.core.config; import com.ossa.common.api.anno.AnonymousAccess; import com.ossa.common.api.bean.SecurityProperties; import com.ossa.common.api.enums.RequestMethodEnum; import com.ossa.common.security.core.filter.OssaTokenFilter; import com.ossa.common.security.core.filter.TokenProvider; import com.ossa.common.security.core.handler.JwtAccessDeniedHandler; import com.ossa.common.security.core.handler.JwtAuthenticationEntryPoint; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.util.*; @Configuration @EnableWebSecurity @RequiredArgsConstructor @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class OssaSecurityConfigurer extends WebSecurityConfigurerAdapter { private final TokenProvider tokenProvider; private final SecurityProperties properties; private final ApplicationContext applicationContext; private final JwtAuthenticationEntryPoint authenticationErrorHandler; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; private final StringRedisTemplate stringRedisTemplate; @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { // 去除 ROLE_ 前缀 return new GrantedAuthorityDefaults(""); } @Bean public PasswordEncoder passwordEncoder() { // 密码加密方式 return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { OssaTokenFilter customFilter = new OssaTokenFilter(tokenProvider, properties,stringRedisTemplate); // 搜寻匿名标记 url: @AnonymousAccess RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping"); Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); // 获取匿名标记 Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap); httpSecurity // 禁用 CSRF .csrf().disable() .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class) // 授权异常 .exceptionHandling() .authenticationEntryPoint(authenticationErrorHandler) .accessDeniedHandler(jwtAccessDeniedHandler) // 防止iframe 造成跨域 .and() .headers() .frameOptions() .disable() // 不创建会话 .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 静态资源等等 .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/webSocket/**" ).permitAll() // swagger 文档 .antMatchers("/swagger-ui.html").permitAll() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/webjars/**").permitAll() .antMatchers("/*/api-docs").permitAll() // 文件 .antMatchers("/avatar/**").permitAll() .antMatchers("/file/**").permitAll() // 阿里巴巴 druid .antMatchers("/druid/**").permitAll() // 放行OPTIONS请求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 自定义匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型 // GET .antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll() // POST .antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll() // PUT .antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll() // PATCH .antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll() // DELETE .antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll() // 所有类型的接口都放行 .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll() // 所有请求都需要认证 .anyRequest().authenticated(); } private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) { Map<String, Set<String>> anonymousUrls = new HashMap<>(6); Set<String> get = new HashSet<>(); Set<String> post = new HashSet<>(); Set<String> put = new HashSet<>(); Set<String> patch = new HashSet<>(); Set<String> delete = new HashSet<>(); Set<String> all = new HashSet<>(); for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) { HandlerMethod handlerMethod = infoEntry.getValue(); AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class); if (null != anonymousAccess) { List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods()); RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name()); switch (Objects.requireNonNull(request)) { case GET: get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case POST: post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case PUT: put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case PATCH: patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; case DELETE: delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; default: all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); break; } } } anonymousUrls.put(RequestMethodEnum.GET.getType(), get); anonymousUrls.put(RequestMethodEnum.POST.getType(), post); anonymousUrls.put(RequestMethodEnum.PUT.getType(), put); anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch); anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete); anonymousUrls.put(RequestMethodEnum.ALL.getType(), all); return anonymousUrls; } } 自定义权限注解 package com.ossa.common.security.core.config; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @Service(value = "pc") public class PermissionConfig { public Boolean check(String... permissions) { // 获取当前用户的所有权限 List<String> permission = SecurityContextHolder.getContext() .getAuthentication() .getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors .toList()); // 判断当前用户的所有权限是否包含接口上定义的权限 return permission.contains("ADMIN") || permission.contains("INNER") || permission.contains("OFFICEIT") || Arrays.stream(permissions).anyMatch(permission::contains); } } 权限异常处理 package com.ossa.common.security.core.handler; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应 response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } } package com.ossa.common.security.core.handler; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应 response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException == null ? "Unauthorized" : authException.getMessage()); } } 网关处理

网关只需要转发token到具体服务即可

在写这篇文章之前,此部分我已经升级成UAA认证授权中心,故没有此处相关代码。

内部流量处理

在内部流量的设计过程中,我们并不需要网关分发的token,故在此设计时,我只在feign的api接口处统一增加权限标识,并经过简单加密。

并在上述的自定的权限注解处放过该标识,不进行权限校验。

package com.ossa.feign.config; import com.ossa.feign.util.EncryptUtil; import feign.Logger; import feign.Request; import feign.RequestInterceptor; import feign.RequestTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * @author issavior * * ================================= * ** * * 修改契约配置,支持Feign原生的注解 * * @return 返回 new Contract.Default() * * * &#064;Bean * public Contract feignContract(){ * return new Contract.Default(); * } * ==================================== */ @Configuration public class FeignClientConfig implements RequestInterceptor { /** * 超时时间配置 * * @return Request.Options */ @Bean public Request.Options options() { return new Request.Options(5, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true); } /** * feign的日志级别 * * @return 日志级别 */ @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } /** * 重写请求拦截器apply方法,循环请求头 * * @param requestTemplate 请求模版 */ @Override public void apply(RequestTemplate requestTemplate) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (Objects.isNull(requestAttributes)) { return; } HttpServletRequest request = ((ServletRequestAttributes) (requestAttributes)).getRequest(); Enumeration<String> headerNames = request.getHeaderNames(); if (headerNames != null) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String values = request.getHeader(name); requestTemplate.header(name, values); } } Enumeration<String> bodyNames = request.getParameterNames(); // body.append("token").append("=").append(EncryptUtil.encodeUTF8StringBase64("INNER")).append("&"); if (bodyNames != null) { while (bodyNames.hasMoreElements()) { String name = bodyNames.nextElement(); String values = request.getParameter(name); requestTemplate.header(name,values); } } requestTemplate.header("inner",EncryptUtil.encodeUTF8StringBase64("INNER")); } // /** // * 修改契约配置,支持Feign原生的注解 // * @return 返回 new Contract.Default() // */ // @Bean // public Contract feignContract(){ // return new Contract.Default(); // } }

热门专栏 欢迎订阅

《Java系核心技术》《中间件核心技术》《微服务核心技术》《云原生核心技术》


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #基于Spring