详解项目后台Spring Security流程

前言

这周写了一下后台登陆,老师叫我参考一下教程后台,正好经过此次机会学习一下spring Security。html

Spring Security

咱们看一下官网对于spring security的介绍java

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

这段文字的大体意思是:
Spring Security是一个强大的、可高度定制化的身份验证和访问控制的框架,它基本上是保护基于Spring应用的安全标准。
Spring Security是一个专一于向Java应用程序提供身份验证和受权的框架。像全部的Spring项目同样,Spring Security的真正威力在于它能够很容易地被扩展以知足定制需求。web

咱们开发一个后台,一些资源想要供全部人访问,一些资源则只想让登陆的人访问,这时候就须要用到咱们的spring security。spring security将身份验证抽离于业务代码以外。spring

使用

首先在配置文件中引入spring security的依赖typescript

<!-- Spring Security的核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

此时,spring security就已经起做用了,咱们再向后台发送信息就会被拦截。
这是由于Spring Boot项目引入了Spring Security之后,自动装配了Spring Security的环境,Spring Security的默认配置是要求通过了HTTP Basic认证成功后才能够访问到URL对应的资源,且默认的用户名是user,密码则是一串UUID字符串,输出到了控制台日志里
image.png
这显然不是咱们想要的认证规则。可是就想前面介绍的那样,spring security强大的地方就在与咱们能够自定义认证规则。跨域

咱们如今来分析一下项目的spring security,你也能够参考官网给的demo
官网demo
项目的大体思路就用户第一次登陆后台会给一个token,再次请求时就带着token,后台经过token与用户信息绑定,从而知道登陆用户是谁。这里的token是有时效的,当token过时后,从新发送token给浏览器,浏览器缓存起来。带着这个思路让咱们看一下代码实现。浏览器

@Configuration
@EnableWebSecurity
public class MvcSecurityConfig extends WebSecurityConfigurerAdapter {
  public static String xAuthTokenKey = "x-auth-token";

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
    // 设置受权配置
        .authorizeRequests()
        // 规定开放端口与须要认证端口
        .antMatchers("/teacher/login").authenticated()
        .antMatchers("/teacher/me").authenticated()
        .antMatchers("/teacher/logout").authenticated()
        .antMatchers("/teacher/**").permitAll()
        .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
        .anyRequest().authenticated()
        // 设置cors过滤器
        .and().cors()
        // 设置httpBasic认证
        .and().httpBasic()
        // 禁用csrf过滤器
        .and().csrf().disable()
        // 在 basic 认证过滤器先后加入自定义过滤器
        .addFilterBefore(this.headerAuthenticationFilter, BasicAuthenticationFilter.class)
        .addFilterAfter(this.addAuthHeaderFilter, BasicAuthenticationFilter.class);
  }
}

咱们自定义一个MvcSecurityConfig继承WebSecurityConfigurerAdapter来自定义咱们的认证规则
再覆盖父类的configure方法,在此方法里自定义规则。
首先
咱们须要规定哪些接口能够做为公共资源任意访问,哪些接口只能登陆后才能够访问。经过antMatchers(url).authenticated()规定请求这个url须要认证,
经过antMatchers(url).permitAll()规定请求这个url不须要认证。
最后将其余url设置为须要认证anyRequest().authenticated()
而后增长cors过滤器,CORS (Cross-Origin Resource Sharing,跨域资源共享)CORS介绍
增长httpBasic认证,
而且禁用csrf过滤器,CSRF(Cross Site Request Forgery, 跨站域请求伪造)CSRF介绍
最后,在BasicAuthenticationFilter过滤器先后加入咱们自定义的过滤器headerAuthenticationFilteraddAuthHeaderFilter(经过依赖注入)。缓存

headerAuthenticationFilter

咱们先说headerAuthenticationFilter,headerAuthenticationFilter主要设置token与验证token是否有效。安全

@Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 获取token,且token为已认证,则设置PreAuthenticatedAuthenticationToken,代表当前用户已认证
    String authToken = request.getHeader(MvcSecurityConfig.xAuthTokenKey);
    if (authToken == null) {
      authToken = UUID.randomUUID().toString();
      this.userService.bindAuthTokenLoginUsername(authToken, null, false);
    } else if (this.userService.isAuth(authToken)) {
      Optional<User> teacherOptional = this.userService.getUserByToken(authToken);
      if (teacherOptional.isPresent()) {
        // token有效,则设置登陆信息
        PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(
            new UserServiceImpl.UserDetail(teacherOptional.get(), new ArrayList<>()), null, new ArrayList<>());
        SecurityContextHolder.getContext().setAuthentication(authentication);
      }
    } else if (!this.userService.getUserByToken(authToken).isPresent()) {
      this.userService.bindAuthTokenLoginUsername(authToken, null, false);
    }

    response.setHeader(MvcSecurityConfig.xAuthTokenKey, authToken);

    filterChain.doFilter(new RequestWrapper(request, authToken), response);
  }

image.png

若是用户第一次登陆,token为null,生成token并与user为null绑定,设置其未登陆,而后将token设置在相应头里,转发。
若是用户非第一次登陆,获取token并认证token是否有效,有效则设置登陆信息,无效则与user为null绑定,设置其未登陆。springboot

AddAuthHeaderFilter

AddAuthHeaderFilter只有在用户名密码正确时才会触发,做用是将Basic认证过滤器认证的用户名与token绑定并设置其已登陆。

@Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 若是用户是经过Basic认证过滤器认证的,则将认证的用户名与xAuthToken相绑定
    Authentication authResult = SecurityContextHolder.getContext().getAuthentication();
    if (authResult != null && authResult.isAuthenticated() && !(authResult instanceof PreAuthenticatedAuthenticationToken)) {
      String xAuthToken = request.getHeader(MvcSecurityConfig.xAuthTokenKey);
      if (xAuthToken == null) {
        throw new RuntimeException("未接收到xAuthToken,请在前置过滤器中加入有效的xAuthToken");
      }
      TeacherServiceImpl.UserDetail userDetail = (TeacherServiceImpl.UserDetail) authResult.getPrincipal();
      this.teacherService.bindAuthTokenLoginUsername(xAuthToken, userDetail.getTeacher(), true);
    }

    filterChain.doFilter(request, response);
  }

那咱们输入的用户名密码在哪里验证呢。
首先咱们在执行spring security中的过滤器时是按照顺序依次执行的,此被称为Spring security过滤器链
image.png
而咱们上述配置的链路大概为... -> HeaderAuthenticationFilter -> BasicAuthenticationFilter -> AddAuthHeaderFilter ...
通过测试,全部的登陆请求都会触发HeaderAuthenticationFilter,而只有用户名密码密码正确的登陆请求才会触发AddAuthHeaderFilter。因此,惟一的解释就是BasicAuthenticationFilter进行了用户名密码验证。

咱们观察BasicAuthenticationFilter源码

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
  try {
    UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
    if (authRequest == null) {
      this.logger.trace("Did not process authentication request since failed to find username and password in Basic Authorization header");
      chain.doFilter(request, response);
      return;
    }
    
    ...
  }

里面调用了authenticationConverter.convert(request)

public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
    String header = request.getHeader("Authorization");
    if (header == null) {
      return null;
    } else {
      header = header.trim();
      if (!StringUtils.startsWithIgnoreCase(header, "Basic")) {
        return null;
      } else if (header.equalsIgnoreCase("Basic")) {
        throw new BadCredentialsException("Empty basic authentication token");
      } else {
        byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
        byte[] decoded = this.decode(base64Token);
        String token = new String(decoded, this.getCredentialsCharset(request));
        int delim = token.indexOf(":");
        if (delim == -1) {
          throw new BadCredentialsException("Invalid basic authentication token");
        } else {
          UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim), token.substring(delim + 1));
          result.setDetails(this.authenticationDetailsSource.buildDetails(request));
          return result;
        }
      }
    }
  }

看了这个方法就知道前台在登陆时传输用户名密码的格式了。

const authString = encodeURIComponent(this.teacher.username) + ':'
        + encodeURIComponent(this.teacher.password);
    const authToken = btoa(authString);
    let httpHeaders = new HttpHeaders();
    httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken);

总结

token能够理解为学生的学生证,咱们经过学生证的方式证实了我是我。具体能够看
你是谁

参考

Spring Security从入门到实践(一)小试牛刀

相关文章
相关标签/搜索