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

本文介绍了在前后端分离的场景下,如何使用SpringSecurity进行登录认证和JWT令牌验证。文章详细展示了配置HttpSecurity,创建登录过滤器LoginAuthenticationFilter和JWT校验过滤器JwtTokenAuthenticationFilter的步骤,并提供了相关代码示例。测试验证部分展示了登录成功和带令牌的API请求的响应情况。
摘要由CSDN通过智能技术生成

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的过程中遇到什么难题,欢迎进一步沟通、交流。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

太空眼睛

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

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

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

打赏作者

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

抵扣说明:

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

余额充值