Spring Security前后端分离最佳实践(登录+JWT)

Spring Security前后端分离最佳实践(登录+JWT)

在这里插入图片描述

现在前后端分离的Web应用越来越流行,后端提供一系列API,前端通过调用这些API完成系统的各个需求的呈现。那么这篇文章主要讲一讲在前后端分离的开发场景中,后端使用Spring Security如何实现登录以及API请求时的身份和权限校验。

Spring Security的核心就是各种Filter的运用,我们需要设计两个主要的Filter

  • LoginAuthenticationFilter
    • 限定只支持POST的登录请求
    • 用于处理登录逻辑
  • JwtTokenAuthenticationFilter
    • 支持忽略不需要校验的path请求
    • 从请求头获取JWT进行校验

开发环境

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
    </parent>

jar包依赖

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

Spring Security配置

继承WebSecurityConfigurerAdapter

为了便于大家理解,我尽量让演示代码精简。定义一个类继承WebSecurityConfigurerAdapter,完整代码如下:

@Component
public class DemoWen3Security extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            // 需要忽略的请求,不校验,直接放行
            .requestMatchers(new AntPathRequestMatcher("/**/test")).permitAll()
            .anyRequest().authenticated();

    // 登录过滤器
    http.addFilterBefore(new LoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    // token校验过滤器
    http.addFilterBefore(new JwtTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

    // 禁用表单提交
    http.formLogin().disable();
    // 禁用注销
    http.logout().disable();
    // 禁用csrf功能,方便使用curl或postman测试
    http.csrf().disable();
  }
}
  • 这个配置项还有非常丰富的功能,比如未登录状态下请求了受保护的资源的处理逻辑
  • 还有权限不足时的处理逻辑
  • 示例代码没有把所有的配置项逐一演示,有兴趣的小伙伴欢迎继续沟通交流

登录过滤器LoginAuthenticationFilter

代码中已经有关键注释对逻辑进行说明,完成代码如下:

public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

  public LoginAuthenticationFilter() {
    // 根据需要自定义处理登录请求的Action
    setFilterProcessesUrl("/user/login");
    // 校验用户名密码
    setAuthenticationManager(new AuthenticationManager() {
      @Override
      public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken)authentication;
        if("xxx".equals(token.getPrincipal()) &&
                "xxx".equals(token.getCredentials())) {
          return new TestingAuthenticationToken("xxx", "xxx");
        }
        throw new BadCredentialsException("用户名密码错误");
      }
    });
    // 登录成功处理逻辑
    setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
      @Override
      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.getWriter().write("login successfully, token:" + UUID.randomUUID().toString());
      }
    });
    // 登录失败处理逻辑
    setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
      @Override
      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.getWriter().write("login fail:" + exception.getMessage());
      }
    });
  }
}
  • Spring Security提供了非常丰富的功能,校验用户名密码有很多种实现方式,示例代码只是其中的一种
  • 登录成功和失败可以根据需要返回自定义JSON数据

token校验过滤器JwtTokenAuthenticationFilter

代码中已经有关键注释对逻辑进行说明,完成代码如下:

public class JwtTokenAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

  public JwtTokenAuthenticationFilter() {
    // token校验逻辑
    setAuthenticationManager(new AuthenticationManager() {
      @Override
      public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        TestingAuthenticationToken token = (TestingAuthenticationToken)authentication;
        String accessToken = (String)token.getCredentials();
        // 校验token逻辑,这里演示只是判空
        if(StringUtils.isNoneBlank(accessToken)) {
          return new TestingAuthenticationToken("xxx", "xxx", AuthorityUtils.createAuthorityList(new String[]{"admin"}));
        }
        throw new BadCredentialsException("token is empty");
      }
    });
    // 校验失败处理逻辑
    setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
      @Override
      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write("401 token is invalid:" + exception.getMessage());
      }
    });
  }

  // 获取token并触发校验
  @Override
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    // 从请求头获取token
    String token = request.getHeader(HttpHeaders.AUTHORIZATION);
    if(StringUtils.isBlank(token)) {
      throw new BadCredentialsException("token is empty");
    }
    TestingAuthenticationToken testingAuthenticationToken = new TestingAuthenticationToken(token, token);
    return this.getAuthenticationManager().authenticate(testingAuthenticationToken);
  }

  // 判断当前请求是否需要进行token校验
  @Override
  protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
    // 不需要校验的请求直接忽略放行
    if(new AntPathRequestMatcher("/**/test").matches(request)) {
      return false;
    }
    // 如果已经校验通过,不重复校验
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if(null!=authentication && authentication.isAuthenticated()) {
      return false;
    }
    return true;
  }

  // 校验通过后的处理逻辑
  @Override
  protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    // 把身份数据放入上下文,后续代码可以随时通过静态方法获取SecurityContextHolder.getContext().getAuthentication()
    SecurityContextHolder.getContext().setAuthentication(authResult);
    // 继续往下进入controller或其它被访问的资源
    chain.doFilter(request, response);
  }
}
  • Spring Securitytoken的校验也提供了多种校验方式
  • 示例代码只是展示了一种最简单直接的方式

main()函数启动类

@SpringBootApplication
public class DemoSpringSecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoSpringSecurityApplication.class, args);
    }
}

测试验证

  • 使用curl命令进行测试
    curl -ik -XPOST "http://localhost:8080/user/login?username=xxx&password=xxx"
  • 响应结果
$ curl -ik -XPOST "http://localhost:8080/user/login?username=xxx&password=xxx"
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=D5240B930D22093D35DFC10A5860CD14; Path=/; HttpOnly
Content-Length: 62
Date: Thu, 31 Mar 2022 15:29:22 GMT

login successfully, token:44a3e490-06ef-4b18-aec5-ad3e7511e3bd
  • 登录成功,并返回了token
  • 继续请求,带token请求API
    curl -ik -XGET -H "Authorization: 44a3e490-06ef-4b18-aec5-ad3e7511e3bd" "http://localhost:8080/getUser"
  • 请求成功,响应如下
HTTP/1.1 404
Set-Cookie: JSESSIONID=E701BA24983F2463C4FFD0AC7EEF72E4; Path=/; HttpOnly
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 31 Mar 2022 15:36:47 GMT

{"timestamp":"2022-03-31T15:36:47.504+00:00","status":404,"error":"Not Found","path":"/getUser"}
  • 为什么返回404呢?因为我们并没有定义/getUser这个API,因为是演示token的校验机制,所以可以看到请求的校验也是通过了的
  • 试试不带token的请求
    curl -ik -XGET "http://localhost:8080/getUser"
  • 后端的响应如下
HTTP/1.1 401
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 35
Date: Thu, 31 Mar 2022 15:41:00 GMT

401 token is invalid:token is empty
  • 返回了401,符合我们的预期
  • 至此,我们对登录、token的校验的演示就全部完成了

总结

Spring Security功能很强大,对安全认证这一块提供了非常丰富的支持,篇幅有限,只是演示了非常基础的功能。如果大家在进行前后端分离的开发时,后端也想集成Spring Security,相信通过这篇文章的介绍,可以帮助大家解决一定的困惑。

本人对Spring Security的研究非常深入,几乎翻看了底层所有的源码,熟悉Spring Security运行的机制、原理,如果大家在使用Spring Security的过程中遇到什么难题,欢迎进一步沟通、交流。

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Spring Boot和Vue.js是两个非常流行的技术栈,可以非常好地实现前后端分离的开发模式。SecurityJWT是两个很好的工具,可以帮助我们实现安全的登录和授权机制。 以下是实现Spring Boot和Vue.js前后端分离的步骤: 1.创建Spring Boot工程 首先,我们需要创建一个Spring Boot工程,可以使用Spring Initializr来生成一个基本的Maven项目,添加所需的依赖项,包括Spring SecurityJWT。 2.配置Spring SecuritySpring Security中,我们需要定义一个安全配置类,该类将定义我们的安全策略和JWT的配置。在这里,我们可以使用注解来定义我们的安全策略,如@PreAuthorize和@Secured。 3.实现JWT JWT是一种基于令牌的身份验证机制,它使用JSON Web Token来传递安全信息。在我们的应用程序中,我们需要实现JWT的生成和验证机制,以便我们可以安全地登录和授权。 4.配置Vue.js 在Vue.js中,我们需要创建一个Vue.js项目,并使用Vue CLI来安装和配置我们的项目。我们需要使用Vue Router来定义我们的路由,并使用Axios来发送HTTP请求。 5.实现登录和授权 最后,我们需要实现登录和授权机制,以便用户可以安全地登录和访问我们的应用程序。在Vue.js中,我们可以使用Vue Router和Axios来发送HTTP请求,并在Spring Boot中使用JWT来验证用户身份。 总结 以上是实现Spring Boot和Vue.js前后端分离的步骤,我们可以使用SecurityJWT来实现安全的登录和授权机制。这种开发模式可以让我们更好地实现前后端分离,提高我们的开发效率和应用程序的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

太空眼睛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值