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

前言

这周写了一下后盾登录,老师叫我参考一下教程后盾,正好通过这次机会学习一下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从入门到实际(一)小试牛刀

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理