springboot学习(七)安全管理 spring security

Spring Security入门

Spring Security 是 Spring 家族中的一个安全管理框架,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security

  • 添加依赖,只要加入依赖,项目的所有接口都会被自动保护起来,访问系统会先需要登录认证

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

默认的用户名为user,密码在控制台,每次启动项目都会变,对登录的用户名/密码进行配置,有三种不同的方式:

  1. 在 application.properties 中进行配置

  2. 通过 Java 代码配置在内存中

  3. 通过 Java 从数据库中加载

  • 用户名密码配置

    • 在 application.properties 中配置

    spring.security.user.name=hjy
    spring.security.user.password=123456

    配置完成后,重启项目,就可以使用这里配置的用户名/密码登录了

    • 在Java代码中配置

    创建一个 Spring Security 的配置类,继承自 WebSecurityConfigurerAdapter 类,使用BCryptPasswordEncoder 进行密码加密(从 Spring5 开始,强制要求密码要加密)

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
       protected void configure(AuthenticationManagerBuilder auth) throws Exception {
           //配置两个用户,密码都加密
           auth.inMemoryAuthentication() .withUser("hjy1").roles("admin").password("$2a$10$D5GuLLF.OOzP28g9Xy1FKu82dj044JeFsNLpujm8sM7xti4IWCTju")
                  .and()
    .withUser("hjy2").roles("user").password("$2a$10$4HMMAfpD0xkwq15ceMY4/OZtlETHvLGhCJox3O1Cn9XmsqAUTLxZq");
      }
       @Bean
       PasswordEncoder passwordEncoder(){
           return new BCryptPasswordEncoder();
      }
    }

    Spring Security 中提供了 BCryptPasswordEncoder 密码编码工具,可以非常方便的实现密码的加密加盐,相同明文加密出来的结果总是不同,这样就不需要用户去额外保存 盐的字段了

    • 登录配置

    对于登录接口,登录成功后的响应,登录失败后的响应,我们都可以在 WebSecurityConfigurerAdapter 的实现类中进行配置

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Autowired
       VerifyCodeFilter verifyCodeFilter;
    @Override
       protected void configure(HttpSecurity http) throws Exception {
           http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
           http.authorizeRequests()//开启登录配置
              .antMatchers("/hello").hasRole("admin")//表示访问 /hello 这个接口,需要具备 admin 这个角色
              .anyRequest().authenticated()//表示剩余的其他接口,登录之后就能访问
              .and()
    .formLogin()
              .loginPage("/login_p")//定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动跳转到该页面
              .loginProcessingUrl("/doLogin")//登录处理接口
              .usernameParameter("uname")//定义登录时,用户名的 key,默认为 username
              .passwordParameter("passwd")//定义登录时,用户密码的 key,默认为 password
              .successHandler(new AuthenticationSuccessHandler(){//登录成功的处理器
    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication)throws IOException,ServletException{
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("success");
    out.flush();
    }
    })
    .failureHandler(new AuthenticationFailureHandler(){
    @Override
    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException,ServletException{
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("fail");
    out.flush();
    }
    })
    .permitAll()//和表单登录相关的接口统统都直接通过
              .and()
    .logout()
    .logoutUrl("/logout")
    .logoutSuccessHandler(new LogoutSuccessHandler(){
    @Override
    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication)throws IOException,ServletException{
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("logout success");
    out.flush();
    }
    })
    .permitAll()
    .and()
    .httpBasic()
    .and()
    .csrf()
    .disable();
      }
    }

    VerifyCodeFilter是自定义的图片验证码,参考图片验证码

    可以在 successHandler 方法中,配置登录成功的回调,如果是前后端分离开发的话,登录成功后返回 JSON 即可,同理,failureHandler 方法中配置登录失败的回调,logoutSuccessHandler 中则配置注销成功的回调

    • 忽略拦截

    如果某一个请求地址不需要拦截的话,过滤掉该地址,即该地址不走 Spring Security 过滤器链

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Override
       public void configure(WebSecurity web) throws Exception{
           web.ignoring().antMatchers("/vercode");
      }
    }

Spring Security添加验证码

  • 验证码工具类

public class VerifyCode {
   private int width = 100;// 生成验证码图片的宽度
   private int height = 50;// 生成验证码图片的高度
   private String[] fontNames = {"宋体", "楷体", "隶书", "微软雅黑"};
   private Color bgColor = new Color(255, 255, 255);// 定义验证码图片的背景颜色为白色
   private Random random = new Random();
   private String codes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
   private String text;// 记录随机字符串
   /**
    * 获取一个随意颜色
    * @return
    */
   private Color randomColor() {
       int red = random.nextInt(150);
       int green = random.nextInt(150);
       int blue = random.nextInt(150);
       return new Color(red, green, blue);
  }
   /**
    * 获取一个随机字体
    * @return
    */
   private Font randomFont() {
       String name = fontNames[random.nextInt(fontNames.length)];
       int style = random.nextInt(4);
       int size = random.nextInt(5) + 24;
       return new Font(name, style, size);
  }
   /**
    * 获取一个随机字符
    * @return
    */
   private char randomChar() {
       return codes.charAt(random.nextInt(codes.length()));
  }
   /**
    * 创建一个空白的BufferedImage对象
    * @return
    */
   private BufferedImage createImage() {
       BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
       Graphics2D g2 = (Graphics2D) image.getGraphics();
       g2.setColor(bgColor);// 设置验证码图片的背景颜色
       g2.fillRect(0, 0, width, height);
       return image;
  }
   public BufferedImage getImage() {
       BufferedImage image = createImage();
       Graphics2D g2 = (Graphics2D) image.getGraphics();
       StringBuffer sb = new StringBuffer();
       for(int i = 0; i < 4; i++) {
           String s = randomChar() + "";
           sb.append(s);
           g2.setColor(randomColor());
           g2.setFont(randomFont());
           float x = i * width * 1.0f / 4;
           g2.drawString(s, x, height - 15);
      }
       this.text = sb.toString();
       drawLine(image);
       return image;
  }
   /**
    * 绘制干扰线
    * @param image
    */
   private void drawLine(BufferedImage image) {
       Graphics2D g2 = (Graphics2D) image.getGraphics();
       int num = 5;
       for(int i = 0; i < num; i++) {
           int x1 = random.nextInt(width);
           int y1 = random.nextInt(height);
           int x2 = random.nextInt(width);
           int y2 = random.nextInt(height);
           g2.setColor(randomColor());
           g2.setStroke(new BasicStroke(1.5f));
           g2.drawLine(x1, y1, x2, y2);
      }
  }
   public String getText() {
       return text;
  }
   public static void output(BufferedImage image, OutputStream out) throws IOException {
       ImageIO.write(image, "JPEG", out);
  }
}
  • 验证码controller

@RestController
public class VerifyController {
   @GetMapping("/verifyCode")
   public void code(HttpServletRequest request, HttpServletResponse response) throws IOException {
       VerifyCode code=new VerifyCode();
       BufferedImage image=code.getImage();
       String text=code.getText();
       HttpSession session=request.getSession();
       session.setAttribute("index_code",text);
       VerifyCode.output(image,response.getOutputStream());
  }
}

创建了一个VerifyCode对象,将生成的验证码字符保存到session中,然后通过流将图片写到前端,img标签如下

<img src="/verifyCode" alt="">
  • 自定义过滤器

@Component
public class VerifyCodeFilter extends GenericFilterBean {
   private String defaultFilterProcessUrl="/doLogin";

   @Override
   public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       HttpServletRequest request=(HttpServletRequest)servletRequest;
       HttpServletResponse response=(HttpServletResponse)servletResponse;
       if("POST".equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getServletPath())){
           //验证码验证
           String requestCaptcha=request.getParameter("code");
           String genCaptcha=(String)request.getSession().getAttribute("index_code");
           if(StringUtils.isEmpty(requestCaptcha)){
               throw new AuthenticationException("验证码不能为空");
          }
           if(!genCaptcha.equalsIgnoreCase(requestCaptcha)){
               throw new AuthenticationException("验证码错误");
          }
       filterChain.doFilter(request,response);
      }
  }
}

自定义过滤器继承自GenericFilterBean,并实现其中的doFilter方法,在doFilter方法中,当请求方法是POST,并且请求地址是 /doLogin时,获取参数中的code字段值,该字段保存了用户从前端页面传来的验证码,然后获取session中保存的验证码,如果用户没有传来验证码,则抛出验证码不能为空异常,如果用户传入了验证码,则判断验证码是否正确,如果不正确则抛出异常,否则执行 chain.doFilter(request,response);使请求继续向下走

  • 在WebSecurityConfigurerAdapter 的实现类中进行配置,见入门登录配置

Spring Security登录使用json

通过分析源码我们发现,默认的用户名密码提取在UsernamePasswordAuthenticationFilter过滤器中,如果想将用户名密码通过JSON的方式进行传递,则需要自定义相关过滤器将其替换即可

自定义过滤器:将用户名/密码的获取方案重新修正下,改为了从JSON中获取用户名密码

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
   @Override
   public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
   if(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
       ObjectMapper mapper = new ObjectMapper();
       UsernamePasswordAuthenticationToken authRequest = null;
       try(InputStream is = request.getInputStream()) {
           Map < String, String > authenticationBean = mapper.readValue(is, Map.class);
           authRequest = new UsernamePasswordAuthenticationToken(authenticationBean.get("username"), authenticationBean.get("password"));
      } catch(IOException e)
      { e.printStackTrace();
           authRequest = new UsernamePasswordAuthenticationToken("", "");
      } finally
      { setDetails(request, authRequest);
           return this.getAuthenticationManager().authenticate(authRequest);
      }
        } else {
       return super.attemptAuthentication(request, response);
        }
  }
}

在SecurityConfig(WebSecurityConfigurerAdapter 实现类)中,将自定义的CustomAuthenticationFilter类加入进来即可

    @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
          .anyRequest()
          .authenticated()
          .and()
          .formLogin()
          .and()
          .csrf()
          .disable();
       http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
  }
   @Bean
   CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
       CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
       filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
           @Override
           public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
               resp.setContentType("application/json;charset=utf-8");
               PrintWriter out = resp.getWriter();
               RespBean respBean = RespBean.ok("登录成功!");
               out.write(new ObjectMapper().writeValueAsString(respBean));
               out.flush();
               out.close();
          }
      });
       filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
           @Override
           public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
               resp.setContentType("application/json;charset=utf-8");
               PrintWriter out = resp.getWriter();
               RespBean respBean = RespBean.error("登录失败!");
               out.write(new ObjectMapper().writeValueAsString(respBean));
               out.flush();
               out.close();
          }
      });
       filter.setAuthenticationManager(authenticationManagerBean());
       return filter;
  }

SpringSecurity中的角色继承

角色继承实际上是一个很常见的需求,因为大部分公司治理可能都是金字塔形的,上司可能具备下属的部分甚至所有权限,这一现实场景,反映到我们的代码中,就是角色继承了,角色继承关系的解析在RoleHierarchyImpl类的buildRolesReachableInOneStepMap方法中

配置角色的继承关系:

    @Bean
   RoleHierarchy roleHierarchy() {
       RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
       String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";
       roleHierarchy.setHierarchy(hierarchy);
       return roleHierarchy;
  }

提供了一个RoleHierarchy接口的实例,使用字符串来描述了角色之间的继承关系, ROLE_dba具备 ROLE_admin的所有权限,而 ROLE_admin则具备 ROLE_user的所有权限,继承与继承之间用一个换行符隔开。提供了这个Bean之后,以后所有具备 ROLE_user角色才能访问的资源, ROLE_dbaROLE_admin也都能访问,具备 ROLE_amdin角色才能访问的资源, ROLE_dba也能访问

在SecurityConfig(WebSecurityConfigurerAdapter 实现类)中指定角色和资源的对应关系即可

    @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
          .antMatchers("/admin/**")
          .hasRole("admin")
          .antMatchers("/db/**")
          .hasRole("dba")
          .antMatchers("/user/**")
          .hasRole("user")
          .and()
          .formLogin()
          .loginProcessingUrl("/doLogin")
          .permitAll()
          .and()
          .csrf()
          .disable();
  }

这个表示 /db/**格式的路径需要具备dba角色才能访问, /admin/**格式的路径则需要具备admin角色才能访问, /user/**格式的路径,则需要具备user角色才能访问,此时提供相关接口,会发现,dba除了访问 /db/**,也能访问 /admin/**/user/**,admin角色除了访问 /admin/**,也能访问 /user/**,user角色则只能访问 /user/**

SpringSecurity中使用JWT

  • 无状态是什么

    • 微服务集群中的每个服务,对外提供的都使用RESTful风格的接口。而RESTful风格的一个最重要的规范就是:服务的无状态性,即:

      • 服务端不保存任何客户端请求者信息

      • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

      那么这种无状态性有哪些好处呢?

      • 客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器

      • 服务端的集群和状态对客户端透明

      • 服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)

      • 减小服务端存储压力

  • 无状态登录流程

    • 首先客户端发送账户名/密码到服务端进行认证

    • 认证通过后,服务端将用户信息加密并且编码成一个token,返回给客户端

    • 以后客户端每次发送请求,都需要携带认证的token

    • 服务端对客户端发送来的token进行解密,判断是否有效,并且获取用户登录信息

JWT,全称是Json Web Token, 是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权

JWT包含三部分数据:

1.Header:头部,通常头部有两部分信息:

  • 声明类型,这里是JWT

  • 加密算法,自定义

我们会对头部进行Base64Url编码(可解码),得到第一部分数据。

2.Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:

  • iss (issuer):表示签发人

  • exp (expiration time):表示token过期时间

  • sub (subject):主题

  • aud (audience):受众

  • nbf (Not Before):生效时间

  • iat (Issued At):签发时间

  • jti (JWT ID):编号

这部分也会采用Base64Url编码,得到第二部分数据。

3.Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。

生成的数据格式如下图:

img

注意,这里的数据通过 . 隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展示方便而已。

因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了RESTful的无状态规范。

JWT的问题

  1. 续签问题,这是被很多人诟病的问题之一,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入redis,虽然可以解决问题,但是jwt也变得不伦不类了。

  2. 注销问题,由于服务端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。

  3. 密码重置,密码重置后,原本的token依然可以访问系统,这时候也需要强制修改secret。

  4. 基于第2点和第3点,一般建议不同用户取不同secret。

  • 添加依赖

        <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt</artifactId>
           <version>0.9.1</version>
       </dependency>
  • 创建User类实现UserDetails 接口

public class User implements UserDetails {
   private String username;
   private String password;
   private List < GrantedAuthority > authorities;
   public String getUsername() {
       return username;
  }
   @Override
   public boolean isAccountNonExpired() {
       return true;
  }
   @Override
   public boolean isAccountNonLocked() {
       return true;
  }
   @Override
   public boolean isCredentialsNonExpired() {
       return true;
  }
   @Override
   public boolean isEnabled() {
       return true;
  }
   //省略getter/setter
}
  • JMT过滤器配置

提供两个和 JWT 相关的过滤器配置:

  1. 一个是用户登录的过滤器,在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个token返回给客户端,登录失败则给前端一个登录失败的提示。

  2. 第二个过滤器则是当其他请求发送来,校验token的过滤器,如果校验成功,就让请求继续执行

    • 登录过滤器

    public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
       protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
           super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
           setAuthenticationManager(authenticationManager);
      }
       @Override
       public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException {
           User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
           return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
      }
       @Override
       protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
           Collection <? extends GrantedAuthority > authorities = authResult.getAuthorities();
           StringBuffer as = new StringBuffer();
           for(GrantedAuthority authority: authorities) {
               as.append(authority.getAuthority()).append(",");
          }
           String jwt = Jwts.builder()
                          .claim("authorities", as)//配置用户角色
                          .setSubject(authResult.getName())
                          .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
                          .signWith(SignatureAlgorithm.HS512, "sang@123")
                          .compact();
           resp.setContentType("application/json;charset=utf-8");
           PrintWriter out = resp.getWriter();
           out.write(new ObjectMapper().writeValueAsString(jwt));
           out.flush();
           out.close();
      }
           protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
               resp.setContentType("application/json;charset=utf-8");
               PrintWriter out = resp.getWriter();
               out.write("登录失败!");
               out.flush();
               out.close();
          }
    }

    解析:

    1. 自定义 JwtLoginFilter 继承自 AbstractAuthenticationProcessingFilter,并实现其中的三个默认方法。

    2. attemptAuthentication方法中,我们从登录参数中提取出用户名密码,然后调用AuthenticationManager.authenticate()方法去进行自动校验。

    3. 第二步如果校验成功,就会来到successfulAuthentication回调中,在successfulAuthentication方法中,将用户角色遍历然后用一个 , 连接起来,然后再利用Jwts去生成token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过期时间以及加密算法和密钥,然后将生成的token写出到客户端。

    4. 第二步如果校验失败就会来到unsuccessfulAuthentication方法中,在这个方法中返回一个错误提示给客户端即可。

    • token校验过滤器

    public class JwtFilter extends GenericFilterBean {
       @Override
       public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
           HttpServletRequest req = (HttpServletRequest) servletRequest;
           String jwtToken = req.getHeader("authorization");
           System.out.println(jwtToken);
           Claims claims = Jwts.parser().setSigningKey("sang@123").parseClaimsJws(jwtToken.replace("Bearer", "")).getBody();
           String username = claims.getSubject();
           //获取当前登录用户名
           List < GrantedAuthority > authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
           UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
           SecurityContextHolder.getContext().setAuthentication(token);
           filterChain.doFilter(req, servletResponse);
      }
    }

    解析:

    1. 首先从请求头中提取出 authorization 字段,这个字段对应的value就是用户的token。

    2. 将提取出来的token字符串转换为一个Claims对象,再从Claims对象中提取出当前用户名和用户角色,创建一个UsernamePasswordAuthenticationToken放到当前的Context中,然后执行过滤链使请求继续执行下去。

    • Spring Security 配置

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
       @Bean
       PasswordEncoder passwordEncoder() {
           return NoOpPasswordEncoder.getInstance();
      }
       @Override
       protected void configure(AuthenticationManagerBuilder auth) throws Exception {
           auth.inMemoryAuthentication()
              .withUser("admin")
              .password("123")
              .roles("admin")
              .and()
              .withUser("sang")
              .password("456")
              .roles("user");
      }
       @Override
       protected void configure(HttpSecurity http) throws Exception {
           http.authorizeRequests()
              .antMatchers("/hello")
              .hasRole("user")
              .antMatchers("/admin")
              .hasRole("admin")
              .antMatchers(HttpMethod.POST, "/login")
              .permitAll().anyRequest()
              .authenticated()
              .and()
              .addFilterBefore(new JwtLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
              .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
              .csrf()
              .disable();
      }
    }

    解析:

    1. 简单起见,这里我并未对密码进行加密,因此配置了NoOpPasswordEncoder的实例。

    2. 简单起见,这里并未连接数据库,我直接在内存中配置了两个用户,两个用户具备不同的角色。

    3. 配置路径规则时, /hello 接口必须要具备 user 角色才能访问, /admin 接口必须要具备 admin 角色才能访问,POST 请求并且是 /login 接口则可以直接通过,其他接口必须认证后才能访问。

    4. 最后配置上两个自定义的过滤器并且关闭掉csrf保护。

原文地址:https://www.cnblogs.com/yjh1995/p/14164446.html