关于springboot:详解项目后台Spring-Security流程

5次阅读

共计 6795 个字符,预计需要花费 17 分钟才能阅读完成。

前言

这周写了一下后盾登录,老师叫我参考一下教程后盾,正好通过这次机会学习一下 spring Security。

Spring Security

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

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 的真正威力在于它能够很容易地被扩大以满足定制需要。

咱们开发一个后盾,一些资源想要供所有人拜访,一些资源则只想让登录的人拜访,这时候就须要用到咱们的 spring security。spring security 将身份验证抽离于业务代码之外。

应用

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

<!-- 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 字符串,输入到了控制台日志里

这显然不是咱们想要的认证规定。然而就想后面介绍的那样,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);
  }

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

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 过滤器链

而咱们上述配置的链路大略为 … -> 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 从入门到实际(一)小试牛刀

正文完
 0