、介绍
创建 MyControllerAdvice,并添加 @ControllerAdvice注解。
package com.sam.demo.controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class MyControllerAdvice {
/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {}
/**
* 把值绑定到Model中,使全局@RequestMapping可以获取到该值
* @param model
*/
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("author", "Magical Sam");
}
/**
* 全局异常捕捉处理
* @param ex
* @return
*/
@ResponseBody
@ExceptionHandler(value=Exception.class)
public Map errorHandler(Exception ex) {
Map map=new HashMap();
map.put("code", 100);
map.put("msg", ex.getMessage());
return map;
}
}
启动应用后,被 @ExceptionHandler、@InitBinder、@ModelAttribute 注解的方法,都会作用在 被 @RequestMapping 注解的方法上。
@ModelAttribute:在Model上设置的值,对于所有被 @RequestMapping 注解的方法中,都可以通过 ModelMap 获取,如下:
@RequestMapping("/home")
public String home(ModelMap modelMap) {
System.out.println(modelMap.get("author"));
}
//或者 通过@ModelAttribute获取
@RequestMapping("/home")
public String home(@ModelAttribute("author") String author) {
System.out.println(author);
}
@ExceptionHandler 拦截了异常,我们可以通过该注解实现自定义异常处理。其中,@ExceptionHandler 配置的 value 指定需要拦截的异常类型,上面拦截了 Exception.class 这种异常。
二、自定义异常处理(全局异常处理)
spring boot 默认情况下会映射到 /error 进行异常处理,但是提示并不十分友好,下面自定义异常处理,提供友好展示。
1、编写自定义异常类:
package com.sam.demo.custom;
public class MyException extends RuntimeException {
public MyException(String code, String msg) {
this.code=code;
this.msg=msg;
}
private String code;
private String msg;
// getter & setter
}
注:spring 对于 RuntimeException 异常才会进行事务回滚。
2、编写全局异常处理类
创建 MyControllerAdvice.java,如下:
package com.sam.demo.controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class MyControllerAdvice {
/**
* 全局异常捕捉处理
* @param ex
* @return
*/
@ResponseBody
@ExceptionHandler(value=Exception.class)
public Map errorHandler(Exception ex) {
Map map=new HashMap();
map.put("code", 100);
map.put("msg", ex.getMessage());
return map;
}
/**
* 拦截捕捉自定义异常 MyException.class
* @param ex
* @return
*/
@ResponseBody
@ExceptionHandler(value=MyException.class)
public Map myErrorHandler(MyException ex) {
Map map=new HashMap();
map.put("code", ex.getCode());
map.put("msg", ex.getMsg());
return map;
}
}
3、controller中抛出异常进行测试。
@RequestMapping("/home")
public String home() throws Exception {
// throw new Exception("Sam 错误");
throw new MyException("101", "Sam 错误");
}
启动应用,访问:http://localhost:8080/home ,正常显示以下json内容,证明自定义异常已经成功被拦截。
{"msg":"Sam 错误","code":"101"}
* 如果不需要返回json数据,而要渲染某个页面模板返回给浏览器,那么MyControllerAdvice中可以这么实现:
@ExceptionHandler(value=MyException.class)
public ModelAndView myErrorHandler(MyException ex) {
ModelAndView modelAndView=new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("code", ex.getCode());
modelAndView.addObject("msg", ex.getMsg());
return modelAndView;
}
在 templates 目录下,添加 error.ftl(这里使用freemarker) 进行渲染:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误页面</title>
</head>
<body>
<h1>${code}</h1>
<h1>${msg}</h1>
</body>
</html>
重启应用,http://localhost:8080/home 显示自定的错误页面内容。
补充:如果全部异常处理返回json,那么可以使用 @RestControllerAdvice 代替 @ControllerAdvice ,这样在方法上就可以不需要添加 @ResponseBody。
文通过一个简易安全认证示例的开发实践,理解过滤器和拦截器的工作原理。
很多文章都将过滤器(Filter)、拦截器(Interceptor)和监听器(Listener)这三者和Spring关联起来讲解,并认为过滤器(Filter)、拦截器(Interceptor)和监听器(Listener)是Spring提供的应用广泛的组件功能。
但是严格来说,过滤器和监听器属于Servlet范畴的API,和Spring没什么关系。
因为过滤器继承自javax.servlet.Filter接口,监听器继承自javax.servlet.ServletContextListener接口,只有拦截器继承的是org.springframework.web.servlet.HandlerInterceptor接口。
上面的流程图参考自网上资料,一图胜千言。看完本文以后,将对过滤器和拦截器的调用过程会有更深刻理解。
一、安全认证设计思路
有时候内外网调用API,对安全性的要求不一样,很多情况下外网调用API的种种限制在内网根本没有必要,但是网关部署的时候,可能因为成本和复杂度等问题,内外网要调用的API会部署在一起。
实现REST接口的安全性,可以通过成熟框架如Spring Security或者shiro搞定。
但是因为安全框架往往实现复杂(我数了下Spring Security,洋洋洒洒大概有11个核心模块,shiro的源码代码量也比较惊人)同时可能要引入复杂配置(能不能让人痛快一点),不利于中小团队的灵活快速开发、部署及问题排查。
很多团队自己造轮子实现安全认证,本文这个简易认证示例参考自我所在的前厂开发团队,可以认为是个基于token的安全认证服务。
大致设计思路如下:
1、自定义http请求头,每次调用API都在请求头里传人一个token值
2、token放在缓存(如redis)中,根据业务和API的不同设置不同策略的过期时间
3、token可以设置白名单和黑名单,可以限制API调用频率,便于开发和测试,便于紧急处理异状,甚至临时关闭API
4、外网调用必须传人token,token可以和用户有关系,比如每次打开页面或者登录生成token写入请求头,页面验证cookie和token有效性等
在Spring Security框架里有两个概念,即认证和授权,认证指可以访问系统的用户,而授权则是用户可以访问的资源。
实现上述简易安全认证需求,你可能需要独立出一个token服务,保证生成token全局唯一,可能包含的模块有自定义流水生成器、CRM、加解密、日志、API统计、缓存等,但是和用户(CRM)其实是弱绑定关系。某些和用户有关系的公共服务,比如我们经常用到的发送短信SMS和邮件服务,也可以通过token机制解决安全调用问题。
综上,本文的简易安全认证其实和Spring Security框架提供的认证和授权有点不一样,当然,这种“安全”处理方式对专业人士没什么新意,但是可以对外挡掉很大一部分小白用户。
二、自定义过滤器
和Spring MVC类似,Spring Boot提供了很多servlet过滤器(Filter)可使用,并且它自动添加了一些常用过滤器,比如CharacterEncodingFilter(用于处理编码问题)、HiddenHttpMethodFilter(隐藏HTTP函数)、HttpPutFormContentFilter(form表单处理)、RequestContextFilter(请求上下文)等。通常我们还会自定义Filter实现一些通用功能,比如记录日志、判断是否登录、权限验证等。
1、自定义请求头
很简单,在request header添加自定义请求头authtoken:
@RequestMapping(value="/getinfobyid", method=RequestMethod.POST)
@ApiOperation("根据商品Id查询商品信息")
@ApiImplicitParams({
@ApiImplicitParam(paramType="header", name="authtoken", required=true, value="authtoken", dataType="String"),
})
public GetGoodsByGoodsIdResponse getGoodsByGoodsId(@RequestHeader String authtoken, @RequestBody GetGoodsByGoodsIdRequest request) {
return _goodsApiService.getGoodsByGoodsId(request);
}
加了@RequestHeader修饰的authtoken字段就可以在swagger这样的框架下显示出来。
调用后,可以根据http工具看到请求头,本文示例是authtoken(和某些框架的token区分开):
备注:很多httpclient工具都支持动态传人请求头,比如RestTemplate。
2、实现Filter
Filter接口共有三个方法,即init,doFilter和destory,看到名称就大概知道它们主要用途了,通常我们只要在doFilter这个方法内,对Http请求进行处理:
package com.power.demo.controller.filter;
import com.power.demo.common.AppConst;
import com.power.demo.common.BizResult;
import com.power.demo.service.contract.AuthTokenService;
import com.power.demo.util.PowerLogger;
import com.power.demo.util.SerializeUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component
public class AuthTokenFilter implements Filter {
@Autowired
private AuthTokenService authTokenService;
@Override
public void init(FilterConfig var1) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req=(HttpServletRequest) request;
String token=req.getHeader(AppConst.AUTH_TOKEN);
BizResult<String> bizResult=authTokenService.powerCheck(token);
System.out.println(SerializeUtil.Serialize(bizResult));
if (bizResult.getIsOK()==true) {
PowerLogger.info("auth token filter passed");
chain.doFilter(request, response);
} else {
throw new ServletException(bizResult.getMessage());
}
}
@Override
public void destroy() {
}
}
注意,Filter这样的东西,我认为从实际分层角度,多数处理的还是表现层偏多,不建议在Filter中直接使用数据访问层Dao,虽然这样的代码一两年前我在很多老古董项目中看到过很多次,而且<<Spring实战>>的书里也有这样写的先例。
3、认证服务
这里就是主要业务逻辑了,示例代码只是简单写下思路,不要轻易就用于生产环境:
package com.power.demo.service.impl;
import com.power.demo.cache.PowerCacheBuilder;
import com.power.demo.common.BizResult;
import com.power.demo.service.contract.AuthTokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class AuthTokenServiceImpl implements AuthTokenService {
@Autowired
private PowerCacheBuilder cacheBuilder;
/*
* 验证请求头token是否合法
* */
@Override
public BizResult<String> powerCheck(String token) {
BizResult<String> bizResult=new BizResult<>(true, "验证通过");
System.out.println("token的值为:" + token);
if (StringUtils.isEmpty(token)==true) {
bizResult.setFail("authtoken为空");
return bizResult;
}
//处理黑名单
bizResult=checkForbidList(token);
if (bizResult.getIsOK()==false) {
return bizResult;
}
//处理白名单
bizResult=checkAllowList(token);
if (bizResult.getIsOK()==false) {
return bizResult;
}
String key=String.format("Power.AuthTokenService.%s", token);
//cacheBuilder.set(key, token);
//cacheBuilder.set(key, token.toUpperCase());
//从缓存中取
String existToken=cacheBuilder.get(key);
if (StringUtils.isEmpty(existToken)==true) {
bizResult.setFail(String.format("不存在此authtoken:%s", token));
return bizResult;
}
//比较token是否相同
Boolean isEqual=token.equals(existToken);
if (isEqual==false) {
bizResult.setFail(String.format("不合法的authtoken:%s", token));
return bizResult;
}
//do something
return bizResult;
}
}
用到的缓存服务可以参考这里,这个也是我在前厂的经验总结。
4、注册Filter
常见的有两种写法:
(1)、使用@WebFilter注解来标识Filter
@Order(1)
@WebFilter(urlPatterns={"/api/v1/goods/*", "/api/v1/userinfo/*"})
public class AuthTokenFilter implements Filter {
使用@WebFilter注解,还可以配合使用@Order注解,@Order注解表示执行过滤顺序,值越小,越先执行,这个Order大小在我们编程过程中就像处理HTTP请求的生命周期一样大有用处。当然,如果没有指定Order,则过滤器的调用顺序跟添加的过滤器顺序相反,过滤器的实现是责任链模式。
最后,在启动类上添加@ServletComponentScan 注解即可正常使用自定义过滤器了。
(2)、使用FilterRegistrationBean对Filter进行自定义注册
本文以第二种实现自定义Filter注册:
package com.power.demo.controller.filter;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.List;
@Configuration
@Component
public class RestFilterConfig {
@Autowired
private AuthTokenFilter filter;
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean registrationBean=new FilterRegistrationBean();
registrationBean.setFilter(filter);
//设置(模糊)匹配的url
List<String> urlPatterns=Lists.newArrayList();
urlPatterns.add("/api/v1/goods/*");
urlPatterns.add("/api/v1/userinfo/*");
registrationBean.setUrlPatterns(urlPatterns);
registrationBean.setOrder(1);
registrationBean.setEnabled(true);
return registrationBean;
}
}
请大家特别注意urlPatterns,属性urlPatterns指定要过滤的URL模式。对于Filter的作用区域,这个参数居功至伟。
注册好Filter,当Spring Boot启动时监测到有javax.servlet.Filter的bean时就会自动加入过滤器调用链ApplicationFilterChain。
调用一个API试试效果:
通常情况下,我们在Spring Boot下都会自定义一个全局统一的异常管理增强GlobalExceptionHandler(和上面这个显示会略有不同)。
根据我的实践,过滤器里抛出异常,不会被全局唯一的异常管理增强捕获到并进行处理,这个和拦截器Inteceptor以及下一篇文章介绍的自定义AOP拦截不同。
到这里,一个通过自定义Filter实现的简易安全认证服务就搞定了。
三、自定义拦截器
1、实现拦截器
继承接口HandlerInterceptor,实现拦截器,接口方法有下面三个:
preHandle是请求执行前执行
postHandle是请求结束执行
afterCompletion是视图渲染完成后执行
package com.power.demo.controller.interceptor;
import com.power.demo.common.AppConst;
import com.power.demo.common.BizResult;
import com.power.demo.service.contract.AuthTokenService;
import com.power.demo.util.PowerLogger;
import com.power.demo.util.SerializeUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*
* 认证token拦截器
* */
@Component
public class AuthTokenInterceptor implements HandlerInterceptor {
@Autowired
private AuthTokenService authTokenService;
/*
* 请求执行前执行
* */
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean handleResult=false;
String token=request.getHeader(AppConst.AUTH_TOKEN);
BizResult<String> bizResult=authTokenService.powerCheck(token);
System.out.println(SerializeUtil.Serialize(bizResult));
handleResult=bizResult.getIsOK();
PowerLogger.info("auth token interceptor拦截结果:" + handleResult);
if (bizResult.getIsOK()==true) {
PowerLogger.info("auth token interceptor passed");
} else {
throw new Exception(bizResult.getMessage());
}
return handleResult;
}
/*
* 请求结束执行
* */
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/*
* 视图渲染完成后执行
* */
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
示例中,我们选择在请求执行前进行token安全认证。
认证服务就是过滤器里介绍的AuthTokenService,业务逻辑层实现复用。
2、注册拦截器
定义一个InterceptorConfig类,继承自WebMvcConfigurationSupport,WebMvcConfigurerAdapter已经过时。
将AuthTokenInterceptor作为bean注入,其他设置拦截器拦截的URL和过滤器非常相似:
package com.power.demo.controller.interceptor;
import com.google.common.collect.Lists;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
@Configuration
@Component
public class InterceptorConfig extends WebMvcConfigurationSupport { //WebMvcConfigurerAdapter已经过时
private static final String FAVICON_URL="/favicon.ico";
/**
* 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/").addResourceLocations("/**");
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
/**
* 配置servlet处理
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//设置(模糊)匹配的url
List<String> urlPatterns=Lists.newArrayList();
urlPatterns.add("/api/v1/goods/*");
urlPatterns.add("/api/v1/userinfo/*");
registry.addInterceptor(authTokenInterceptor()).addPathPatterns(urlPatterns).excludePathPatterns(FAVICON_URL);
super.addInterceptors(registry);
}
//将拦截器作为bean写入配置中
@Bean
public AuthTokenInterceptor authTokenInterceptor() {
return new AuthTokenInterceptor();
}
}
启动应用后,调用接口就可以看到拦截器拦截的效果了。全局统一的异常管理GlobalExceptionHandler捕获异常后处理如下:
和过滤器显示的主要错误提示信息几乎一样,但是堆栈信息更加丰富。
四、过滤器和拦截器区别
主要区别如下:
1、拦截器主要是基于java的反射机制的,而过滤器是基于函数回调
2、拦截器不依赖于servlet容器,过滤器依赖于servlet容器
3、拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用
4、拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问
5、在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次
参考过的一些文章,有的说“拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑”,经过实际验证,这是不对的。
注意:过滤器的触发时机是容器后,servlet之前,所以过滤器的doFilter(ServletRequest request, ServletResponse response, FilterChain chain)的入参是ServletRequest,而不是HttpServletRequest,因为过滤器是在HttpServlet之前。下面这个图,可以让你对Filter和Interceptor的执行时机有更加直观的认识:
只有经过DispatcherServlet 的请求,才会走拦截器链,自定义的Servlet请求是不会被拦截的,比如我们自定义的Servlet地址http://localhost:9090/testServlet是不会被拦截器拦截的。但不管是属于哪个Servlet,只要符合过滤器的过滤规则,过滤器都会执行。
根据上述分析,理解原理,实际操作就简单了,哪怕是ASP.NET过滤器亦然。
问题:实现更加灵活的安全认证
在Java Web下通过自定义过滤器Filter或者拦截器Interceptor配置urlPatterns,可以实现对特定匹配的API进行安全认证,比如匹配所有API、匹配某个或某几个API等,但是有时候这种匹配模式对开发人员相对不够友好。
我们可以参考Spring Security那样,通过注解+SpEL实现强大功能。
又比如在ASP.NET中,我们经常用到Authorized特性,这个特性可以加在类上,也可以作用于方法上,可以更加动态灵活地控制安全认证。
我们没有选择Spring Security,那就自己实现类似Authorized的灵活的安全认证,主要实现技术就是我们所熟知的AOP。
通过AOP方式实现更灵活的拦截的基础知识本文就先不提了,更多的关于AOP的话题将在下篇文章分享。
原文:https://www.cnblogs.com/jeffwongishandsome/p/spring-boot-use-filter-and-interceptor-to-implement-an-easy-auth-system.html
言
同源策略:判断是否是同源的,主要看这三点,协议,ip,端口。
同源策略就是浏览器出于网站安全性的考虑,限制不同源之间的资源相互访问的一种政策。
比如在域名https://www.baidu.com下,脚本不能够访问https://www.sina.com源下的资源,否则将会被浏览器拦截。
注意两点:
1.必须是脚本请求,比如AJAX请求。
但是如下情况不会产生跨域拦截
<img src="xxx"/> <a href='xxx"> </a>
2.跨域拦截是前端请求已经发出,并且在后端返回响应时检查相关参数,是否允许接收后端请求。
在微服务开发中,一个系统包含多个微服务,会存在跨域请求的场景。
本文主要讲解SpringBoot解决跨域请求拦截的问题。
搭建项目
这里创建两个web项目,web1 和 web2.
web2项目请求web1项目的资源。
这里只贴关键代码,完整代码参考GitHub
WEB2
创建一个Controller返回html页面
@Slf4j
@Controller
public class HomeController {
@RequestMapping("/index")
public String home(){
log.info("/index");
return "/home";
}
}
html页面 home.html
这里创建了一个按钮,按钮按下则请求资源:"http://localhost:8301/hello"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>web2</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
<script>
$(function () {
$("#testBtn").click(function () {
console.log("testbtn ...");
$.get("http://localhost:8301/hello",function(data,status){
alert("数据: " + data + "\n状态: " + status);
});
})
})
</script>
</head>
<body>
web2
<button id="testBtn">测试</button>
</body>
</html>
WEB1
@Slf4j
@RestController
public class Web1Controller {
@RequestMapping("/hello")
public String hello(){
log.info("hello ");
return "hello," + new Date().toString();
}
}
这里配置两个项目为不同的端口。
WEB1为8301
WEB2为8302
因此是不同源的。
测试
在web1还没有配置允许跨域访问的情况下
按下按钮,将会出现错误。显示Header中没有Access-Control-Allow-Origin
Access to XMLHttpRequest at 'http://localhost:8301/hello' from origin 'http://localhost:8300' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
WEB1添加允许跨域请求,通过实现WebMvcConfigurer
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/hello");
}
}
再次访问将会返回正常数据。
除了以上的配置外,还可以做更细致的限制
比如对请求的headers,请求的方法POST/GET...。请求的源进行限制。
同时还可以使用注解 @CrossOrigin来替换上面的配置。
@Slf4j
@RestController
public class Web1Controller {
@CrossOrigin
@RequestMapping("/hello")
public String hello(){
log.info("hello ");
return "hello," + new Date().toString();
}
}
注解可以用在类上,也可以用在方法上,但必须是控制器类
配置和上面一样,也是可以对方法,header,源进行个性化限制。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
/** @deprecated */
@Deprecated
String[] DEFAULT_ORIGINS=new String[]{"*"};
/** @deprecated */
@Deprecated
String[] DEFAULT_ALLOWED_HEADERS=new String[]{"*"};
/** @deprecated */
@Deprecated
boolean DEFAULT_ALLOW_CREDENTIALS=false;
/** @deprecated */
@Deprecated
long DEFAULT_MAX_AGE=1800L;
@AliasFor("origins")
String[] value() default {};
@AliasFor("value")
String[] origins() default {};
String[] allowedHeaders() default {};
String[] exposedHeaders() default {};
RequestMethod[] methods() default {};
String allowCredentials() default "";
long maxAge() default -1L;
}
欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 721575865
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!
*请认真填写需求信息,我们会在24小时内与您取得联系。