一文入门SpringSecurity

本文已参与「新人创作礼」活动,一起开启掘金创作之路

SpringSecurity应用

Spring Security 功能简介

  1. 认证:用户登录,两种认证方式:httpBasic、formLogin
  2. 授权:判断用户权限,可以访问什么资源
  3. 安全防护,防止跨站请求,session攻击等。

应用场景:

  1. 登录
  2. 授权
  3. 单一登录,一个账户同一时间只能在一个地方登录
  4. 集成cas,单点登录
  5. 集成oauth2,可以做第三方登录

基础入门

  1. 引入依赖:
 <!--添加Spring Security 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
复制代码
  1. 再写一个简单的controller
@RestController
public class HelloController {


    @RequestMapping("/hello")
    public String hello(){
        return "hello security";
    }
}
复制代码
  1. 访问http://localhost:8080/hello,发现自动跳到了登录页面。

  1. 进行登录,默认用户名:user,密码看启动日志
Using generated security password: bbb788d7-13aa-4e6d-9dc6-b7923a27e6a9
复制代码

SpringSecurity认证基本原理

在使用SpringSecurity框架,该框架会默认自动地替我们将系统中的资源进行保护,每次访问资源的 时候都必须经过一层身份的校验,如果通过了则重定向到我们输入的url中,否则访问是要被拒绝的。那 么SpringSecurity框架是如何实现的呢? ==Spring Security功能的实现主要是由一系列过滤器相互配合完 成。也称之为过滤器链==

  1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调

用处理拦截器 2. org.springframework.security.web.context.SecurityContextPersistenceFilter

SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存

或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter 建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。 3. org.springframework.security.web.header.HeaderWriterFilter

向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制

  1. org.springframework.security.web.csrf.CsrfFilter

csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的

token信息,如果不包含,则报错。起到防止csrf攻击的效果。 5. org.springframework.security.web.authentication.logout.LogoutFilter

匹配URL为/logout的请求,实现用户退出,清除认证信息。

  1. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。

  1. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。

  1. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

由此过滤器可以生产一个默认的退出登录页面

  1. org.springframework.security.web.authentication.www.BasicAuthenticationFilter

此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。

  1. org.springframework.security.web.savedrequest.RequestCacheAwareFilter

通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存

HttpServletRequest 11. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

针对ServletRequest进行了一次包装,使得request具有更加丰富的API

  1. org.springframework.security.web.authentication.AnonymousAuthenticationFilter

当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到

SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程, 只不过是一个匿名的身份。 13. org.springframework.security.web.session.SessionManagementFilter

securityContextRepository限制同一用户开启多个会话的数量

  1. org.springframework.security.web.access.ExceptionTranslationFilter

异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异

常 15. org.springframework.security.web.access.intercept.FilterSecurityInterceptor

获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其

是否有权限。

表单认证

3.1 自定义表单登录页

在config包下编写SecurityConfiguration配置类

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    
    @Override
    public void configure(WebSecurity web) throws Exception {
        //对静态资源放行
        web.ignoring().antMatchers("/css/**", "/images/**", "/js/**",
                "/favicon.ico");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()        //开启表单认证
        .loginPage("/toLoginPage")
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()   //登录请求不需要认证
                .anyRequest().authenticated();      //所有请求都需要认证
    }
}

复制代码

访问http://localhost:8080/,会自动跳转自定义的登录页面。

这时的登录页面的表单的请求参数必须得跟springSecurity默认的一致才行,那么可以自定义吗?答案是肯定的。

默认值:

自定义参数代码:

 protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()        //开启表单认证
        .loginPage("/toLoginPage")
                .loginProcessingUrl("/login")  //登录处理url
                .usernameParameter("username")  //自定义用户名参数
                .passwordParameter("password")//自定义密码参数
                .defaultSuccessUrl("/")   //登录成功跳转路径
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()   //登录请求不需要认证
                .anyRequest().authenticated();      //所有请求都需要认证
    http.csrf().disable();

    }
复制代码
3.2 基于数据库实现认证功能

之前我们所使用的用户名和密码是来源于框架自动生成的, 那么我们如何实现基于数据库中的用户名和 密码功能呢? 要实现这个得需要实现security的一个UserDetailsService接口, 重写这个接口里面 loadUserByUsername即可

  1. 编写MyUserDetailsService并实现UserDetailsService接口,重写loadUserByUsername方法
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByName(username);
        if(user==null){
            throw new UsernameNotFoundException(username);
        }

        // 先声明一个权限集合, 因为构造方法里面不能传入null
        Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails=new org.springframework.security.core.userdetails.User(username,"{noop}"+user.getPassword()
        ,true,true,true,true,authorities);
        return userDetails;
    }
}
复制代码

在上面new UserDetails里面的{noop}代表的是加密方式为:不加密 2. 在SecurityConfiguration配置类中指定自定义用户认证

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }
    
    //省略部分代码...
}
复制代码
3.3 数据库用户密码加密认证

Spring Security 中 PasswordEncoder 就是我们对密码进行编码的工具接口。该接口只有两个功能: 一个是匹配验证。另一个是密码编码。一般我们常用的算法是BCrypt算法。

BCrypt算法介绍

任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很 多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security 提供了 BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密 密码。BCrypt强哈希方法每次加密的结果都不一样,所以更加的安全。

bcrypt加密后的字符串形如:

2 a 2a 10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq

其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再 然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值12。

  1. 修改loadUserByUsername方法里的加密方式
  @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByName(username);
        if(user==null){
            throw new UsernameNotFoundException(username);
        }

        // 先声明一个权限集合, 因为构造方法里面不能传入null
        Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails=new org.springframework.security.core.userdetails.User(username,"{bcrypt}"+user.getPassword()
        ,true,true,true,true,authorities);
        return userDetails;
    }
复制代码
  1. 修改数据库里的密码为加密格式

加密代码如下:

BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
String encode = encoder.encode("123456");
复制代码
3.4 获取当前登录用户

在传统web系统中, 我们将登录成功的用户放入session中, 在需要的时候可以从session中获取用户,那么Spring Security中我们如何获取当前已经登录的用户呢?

方式1:SecurityContextHolder

UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
复制代码

方式2:Authentication

 @RequestMapping("/getUser2")
    public UserDetails getUser2(Authentication authentication){
        UserDetails principal = (UserDetails) authentication.getPrincipal();
        return principal;
    }
复制代码

方式3:@AuthenticationPrincipal

@RequestMapping("/getUser3")
    public UserDetails getUser3(@AuthenticationPrincipal UserDetails userDetails){
        return userDetails;
    }
复制代码
3.5 remember me功能

在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现, 下面我们来看下他的原理图.

简单token方式

Token=MD5(username+分隔符+expiryTime+分隔符+password)

注意:这种方式不推荐使用,是将用户密码信息存在前端浏览器的cookie中,不安全。

实现方式:

  1. 前端页面需要增加remember-me的复选框
<div class="form-group">
    <div >
      <!--记住我 name为remember-me value值可选true yes 1 on 都行-->
      <input type="checkbox"  name="remember-me" value="true"/>记住我
    </div>
</div>
复制代码
  1. 后端代码开启remember-me功能
 .and().rememberMe()  //开启remeberMe功能
                .tokenValiditySeconds(60*60)        //token失效时间
                .rememberMeParameter("remember-me")     //自定义表单名称
复制代码
  1. 验证,登录成功后查看cookie

登录成功后,关掉浏览器,再次访问,也不需要登录了。

持久化的Token生成方式

token: 随机生成策略,每次访问都会重新生成

series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用remember-me功能,该值保持不变

expiryTime: token过期时间。

CookieValue=encode(series+token)

  1. 后台代码
 @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository=new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
//        启动时创建一张表, 第一次启动的时候创建, 第二次启动的时候需要注释掉, 否则
//        会报错
        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
复制代码

注意: tokenRepository.setCreateTableOnStartup(true); 第一次设置为true,后面启动需要设置为false

  1. 配置持久化token
 .and().rememberMe()  //开启remeberMe功能
                .tokenValiditySeconds(60*60)        //token失效时间
                .rememberMeParameter("remember-me")     //自定义表单名称
                .tokenRepository(persistentTokenRepository())  //设置tokenRepository
复制代码
  1. 登录成功,查看数据

==这两种方式都是依赖cookie的,如果cookie被窃取,会有安全问题。不需要登录,将cookie拷贝到postman里面就能够直接调用接口。==

对重要的接口我们需要处理,处理方法如下:

 public String hello(){
        //获取认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //判断如果认证信息是来源于remember-me就拦截
        if(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())){
            throw new RememberMeAuthenticationException("请重写登录");
        }
        return "hello security";
    }
复制代码
3.6 自定义登录成功或失败

有些时候登录成功或失败后需要做一些后续操作,比如日志收集,发送请求等。

自定义成功处理

实现AuthenticationSuccessHandler接口,并重写onAnthenticationSuccesss()方法

自定义失败处理

实现AuthenticationFailureHandler接口,并重写onAuthenticationFailure()方法

  1. 登录处理类
@Service
public class LoginHandler implements AuthenticationSuccessHandler,AuthenticationFailureHandler {

    private RedirectStrategy redirectStrategy = new
            DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("登录失败");
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登录成功");
        redirectStrategy.sendRedirect(request,response,"/");
    }
}
复制代码
  1. 配置登录处理
.successHandler(loginHandler)
.failureHandler(loginHandler)
复制代码

完整配置如下:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()        //开启表单认证
        .loginPage("/toLoginPage")
                .loginProcessingUrl("/login")  //登录处理url
                .usernameParameter("username")  //自定义用户名参数
                .passwordParameter("password")//自定义密码参数
                .defaultSuccessUrl("/")   //登录成功跳转路径
                .successForwardUrl("/")
                .successHandler(loginHandler)
                .failureHandler(loginHandler)
                .and().rememberMe()  //开启remeberMe功能
                .tokenValiditySeconds(60*60)        //token失效时间
                .rememberMeParameter("remember-me")     //自定义表单名称
                .tokenRepository(persistentTokenRepository())  //设置tokenRepository
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()   //登录请求不需要认证
                .anyRequest().authenticated();      //所有请求都需要认证
    http.csrf().disable();
    // 允许iframe加载页面
    http.headers().frameOptions().sameOrigin();
    }
复制代码

异步登录

  1. 前端页面改造
<form id="formLogin" action="/login" method="post">
        <div class="panel loginbox">
         .....
          <div style="padding:30px;">
            <input type="button" onclick="login()"
               class="button button-block bg-main text-
big input-big" value="登录">
          </div>
        </div>
      </form>
    </div>
  </div>
</div>
<script>
  function login() {
    $.ajax({
      type: "POST",//方法类型
      dataType: "json",//服务器预期返回类型
      url: "/login",  // 登录url
      data: $("#formLogin").serialize(),
      success: function (data) {
        console.log(data)
        if (data.code == 200) {
          window.location.href = "/";
       } else {
          alert(data.message);
       }
     }
   });
 }
</script>
复制代码
  1. 后端改造
  @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登录成功");
        Map result = new HashMap();
        result.put("code", HttpStatus.OK.value());// 设置响应码
        result.put("message", "登录成功");// 设置响应信息
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
复制代码
3.7 退出登录

只需要发送请求,请求路径为/logout即可, 当然这个路径也可以自行在配置类中自行指定, 同时退出 操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后执行,退出的同时如果有remember-me的数据,同时一并删除

  1. 前端页面
<a class="button button-little bg-red" href="/logout">
      <span class="icon-power-off"></span>退出登录</a></div>
复制代码
  1. 后端配置

实现LogoutSuccessHandler接口

 @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("退出登录后续处理");
    }
复制代码
.and().logout().logoutUrl("/logout")    //设置退出登录url
                .logoutSuccessHandler(loginHandler)     //自定义退出处理
复制代码

图形验证码

  1. 验证码图片生成代码
@RequestMapping("/image/code")
    public void imageCode(HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode();
        BufferedImage image = imageCode.getImage();
        response.setContentType("image/jpeg");
        ImageIO.write(image,"jpeg",response.getOutputStream());
    }

    private ImageCode createImageCode() {
        int width = 100;    // 验证码图片宽度
        int height = 36;    // 验证码图片长度
        int length = 4;     // 验证码位数
        //创建一个带缓冲区图像对象
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        //获得在图像上绘图的Graphics对象
        Graphics g = image.getGraphics();

        Random random = new Random();

        //设置颜色、并随机绘制直线
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("宋体", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        //生成随机数 并绘制
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < length; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }
        g.dispose();
        return new ImageCode(image, sRand.toString());
    }

    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
复制代码
  1. 校验验证码是否正确

Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作.

实现OncePerRequestFilter接口,写过滤其代码校验验证码。代码略

  1. 配置过滤器

将我们的过滤器配置在UsernamePassword校验前

 http.addFilterBefore(validateCodeFilter,
UsernamePasswordAuthenticationFilter.class);
复制代码

Session管理

5.1 会话超时
  1. 配置session会话超时时间,默认30分钟
server.servlet.session.timeout=60s
复制代码

==注意:设置低于1分钟不起效==

  1. 自定义设置session超时后跳转地址
http.sessionManagement() //设置session管理
        .invalidSessionUrl("/toLoginPage");
复制代码
5.2 并发控制

并发控制即同一个账号同时在线个数,同一个账号同时在线个数如果设置为1表示,该账号在同一时间内只能有一个有效的登录,如果同一个账号又在其它地方登录,那么就将上次登录的会话过期,即后面的登录会踢掉前面的登录

 http.sessionManagement() //设置session管理
        .invalidSessionUrl("/toLoginPage")             //session无效后跳转路径
        .maximumSessions(1)  //设置session最大会话数量,1代表同一时间只能一个用户登录
        .expiredUrl("/toLoginPage");        //session过期后跳转路径
复制代码

阻止用户二次登录

sessionManagement也可以配置 maxSessionsPreventsLogin的值,当达到maximumSessions设置的最大会话个数时阻止登录。

 http.sessionManagement() //设置session管理
        .invalidSessionUrl("/toLoginPage")             //session无效后跳转路径
        .maximumSessions(1)  //设置session最大会话数量,1代表同一时间只能一个用户登录
        .maxSessionsPreventsLogin(true)    //达到最大会话时阻止登录
        .expiredUrl("/toLoginPage");        //session过期后跳转路径
复制代码
5.3 集群session

问题描述: 实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡, 用户访问nginx,nginx再决定去访问哪一台服务器。当一台服务宕机了之后,另一台服务器也可以继续 提供服务,保证服务不中断。如果我们将session保存在Web容器(比如tomcat)中,如果一个用户第一 次访问被分配到服务器1上面需要登录,当某些访问突然被分配到服务器二上,因为服务器二上没有用 户在服务器一上登录的会话session信息,服务器二还会再次让用户登录,用户已经登录了还让登录就 感觉不正常了。

解决这个问题有一个方案是将session共享,可以存在单独的地方(redis、数据库、mongodb等)。

  1. 引入依赖
<!-- 基于redis实现session共享 -->
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
</dependency>
复制代码
  1. 设置session存储类型
#使用redis共享session
spring.session.store-type=redis
复制代码

csrf防护机制

什么是csrf?

CSRF(Cross-site request forgery),中文名称:跨站请求伪造

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成三个步骤:

  1. 登录一个受信任网站A,并生成本地Cookie
  2. 在不登出A的情况下,访问危险网站B
  3. 网站B是个黑客做的网站,他里面有些链接和按钮会让用户调用A网站的接口去做一些对你不利的操作。

csrf的防御策略

  1. 验证http referer

接收到请求的时候判断是不是由自己网站发来的 2. 在请求地址中添加tokon并验证

黑客之所以能完全伪造用户的请求,是因为验证用户的信息放在cookie里的,所以他可以利用你的cookie来通过安全校验。要抵御csrf,只要在请求里加入黑客不能伪造的信息就可以了。

比如在请求参数中加入一个token,并且在服务端建一个拦截器来校验这个token。这个token是服务器给前端的,黑客在别的网站上拿不到,这样就可以避免csrf攻击了。 3. 在http头中自定义属性并验证

这种方法也是使用token来验证,和上一种方法不同的是是将token放在http头中。

security防御csrf

Security依靠org.springframework.security.web.csrf.CsrfFilter拦截器来进行验证token。

  1. 页面发请求时增加token值
 <div>
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
</div>
复制代码
  1. 后端(默认是开启防护的,如某接口不需要防护可以配置)
//哪些接口不做csrf防护
http.csrf().ignoringAntMatchers("/user/save");
复制代码

跨域

跨域,实际上是浏览器的一种保护处理,如果产生了跨域,服务器在返回结果时就会被浏览器拦截。

两个网站端口不同,域名不同,协议不同都会被认为是跨域。

解决跨域

  1. JSONP

浏览器允许一些带src属性的标签跨域,也就是在某些标签的src属性上写url地址是不会产生跨 域问题 2. CORS解决跨域

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。浏览器在发起真正的请求之前,会发起一个OPTIONS类型的预检请求,用于请求服务器是否允许跨域,在得到许可的情况下才会发起请求。

Security处理跨域

  1. 配置cors
public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 设置允许跨域的站点
        corsConfiguration.addAllowedOrigin("*");
        // 设置允许跨域的http方法
        corsConfiguration.addAllowedMethod("*");
        // 设置允许跨域的请求头
        corsConfiguration.addAllowedHeader("*");
        // 允许带凭证
        corsConfiguration.setAllowCredentials(true);

        // 对所有的url生效
        UrlBasedCorsConfigurationSource source = new
                        UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
}
复制代码
http.cors().configurationSource(corsConfigurationSource());
复制代码

再发请求,成功了

猜你喜欢

转载自juejin.im/post/7055112198099566606