背景

spring家族的安全框架,两大核心: 认证(Authentication)和授权(Authorization) 。

通俗说:认证—你是谁?

授权----你有什么权限?

在认证和授权这两个核心功能之外,SpringSecurity还提供了很多安全管理的“周边功能”,这也是一个非常重要的特色,在开发中,即便我们不了解很多网络攻击,只要用了SpringSecurity它会帮助我们自动防御很多网络攻击,例如CSRF攻击,会话固定攻击等,同时还提供了HTTP防火墙来拦截大量的非法请求。

认证

认证规则有很多,有:

  • 表单认证
  • OAuth2.0认证
  • SAML2.0认证
  • CAS认证
  • RememberMe自动认证
  • JAAS认证
  • OpenID认证
  • Pre-Authentication Scenarios认证
  • X509认证
  • HTTP Basic认证
  • HTTP Digest认证
  • 自定义认证等等

看一下 Authentication 认证接口:

public interface Authentication extends Principal, Serializable {
    //用来获取用户的权限
	Collection<? extends GrantedAuthority> getAuthorities();
	//用来获取用户凭证,一般来说是密码
	Object getCredentials();
	//用来获取用户携带的详细信息,可能是当前请求之类等
	Object getDetails();
	//用来获取当前用户,例如是一个用户或一个用户对象
	Object getPrincipal();
	//当前用户是否认证成功。
	boolean isAuthenticated();
	
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

管理认证的接口 AuthenticationManager :

/**
 AuthenticationManager接口只有一个authenticate方法可以用来做认证,该方法有三个不同的返回值:

返回Authentication : 表示认证成功

抛出AuthenticationException异常:表示用户输入了无效的凭证.

返回null,表示不能断定

*/
public interface AuthenticationManager {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

实现认证管理的类 ProviderManager :

/**
AuthenticationManager最主要的实现类是ProviderManager,ProviderManager管理了众多的AuthenticationProvider实例,AuthenticationProvider有点类似于AuthenticationManager,但是它多了一个supports方法用来短评是否指出给定的Authenticaion类型。
*/
public class ProviderManager implements AuthenticationManager{
    private List<AuthenticationProvider> providers = Collections.emptyList();
    private AuthenticationManager parent;
    @Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ...
    }
}

具体管理某个认证的接口 AuthenticationProvider

AuthenticationProvider类似于AuthenticationManager ,只不过其只针对一个认证

/**
由于Authentication拥有众多不同的实现类,这些不同的实现类又由不能的AuthenticationProvider来处理,所以AuthenticationProvider会有一个supports方法,来判断当前的AuthenticationProvider是否支持对应的Authentication

*/
public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;
	boolean supports(Class<?> authentication);
}

授权

  • 基于URL的请求授权
  • 支持方法的访问授权
  • 支持SpEL访问控制
  • 支持域对象安全
  • 同时也支持动态权限配置
  • 支持RBAC权限模型等

认证相关接口:

投票 接口AccessDecisionVoter

public interface AccessDecisionVoter<S> {

	int ACCESS_GRANTED = 1;

	int ACCESS_ABSTAIN = 0;

	int ACCESS_DENIED = -1;
    boolean supports(ConfigAttribute attribute);
    boolean supports(Class<?> clazz);
    int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}

管理投票接口 AccessDecisionManager

AccessDecisionVoter和AccessDecisionManager都有众多的实现类,在AccessDecisionManager中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问

public interface AccessDecisionManager {
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}

过滤器

我们所见到SpringSecurity提供的功能,都是通过这些过滤器来实现的,这些过滤器按照既定的优先级排列,最终行程一个过滤器链。我们也可以自定义过滤器,并通过@Order来调整自定义过滤器在过滤链中的位置。

需要注意的是,默认过滤器并不是直接放在Web项目的原生过滤器链中,而是通过一个FilterChainProxy来统一管理。SpringSecurity中的过滤链通过FilterChainProxy嵌入到Web项目的原生过滤器链中,如下:

Springcoul中gateway中设置对接系统设置白名单时docker容器的IP是动态变化的_开发语言

具体案例

springsecurity安全额配置类

/**
 * Spring security web security config.
 *
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
  public static final String API_PATTERN = "/api/**";

  /** 无需认证的API patterns */
  public static String[] PUBLIC_API_PATTERNS =
      new String[] {
        "/api/users/login",
      	....
      };

  /** 需要认证的API patterns */
  public static String[] PRIVATE_API_PATTERNS = new String[] {API_PATTERN, "/api2/**"};
	/**X-Frame-Options设置
	deny:表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
	sameorigin:表示该页面可以在相同域名页面的 frame 中展示。
	allow-from:  这是一个被弃用的指令,不再适用于现代浏览器;意思是只能被嵌入到指定域名的框架中
	**/
  public static final String X_FRAME_OPT_DENY = "deny";
  public static final String X_FRAME_OPT_SAME_ORIGIN = "sameorigin";
  public static final String X_FRAME_OPT_ALLOW_FROM_PREFIX = "allow-from ";

  @Setter(onMethod_ = {@Autowired})
  private AccessTokenAuthenticationProvider accessTokenAuthProvider;

  @Setter(onMethod_ = {@Autowired})
  private SettingService settingService;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // 设置认证
    AccessTokenAuthenticationFilter accessTokenAuthenticationFilter =
        new AccessTokenAuthenticationFilter();
    http.authorizeRequests()
        // 公共API无需认证
        .antMatchers(PUBLIC_API_PATTERNS)
        .permitAll()
        .antMatchers(PRIVATE_API_PATTERNS)
        .authenticated()
        .and()
        // 提取令牌
        .addFilterBefore(accessTokenAuthenticationFilter, BasicAuthenticationFilter.class)
        // 验证令牌
        .authenticationProvider(this.accessTokenAuthProvider)
        .exceptionHandling()
        // 对于认证失败的返回401错误,而不是默认的403错误,请见:https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.html#authenticationEntryPoint(org.springframework.security.web.AuthenticationEntryPoint)。
        .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
        .and()
        .headers()
        // 设置X-Frame-Options响应头
        //避免自己的网页被嵌入到别的站点
        .frameOptions(
            options -> {
              String opt = 数据库或配置文件读取设置;
              if (StringUtils.isBlank(opt)) {
                options.disable();
              } else if (X_FRAME_OPT_SAME_ORIGIN.equalsIgnoreCase(opt)) {
                options.sameOrigin();
              } else if (StringUtils.startsWithIgnoreCase(opt, X_FRAME_OPT_ALLOW_FROM_PREFIX)) {
                options
                    .disable()
                    .addHeaderWriter(
                        new StaticHeadersWriter(
                            XFrameOptionsHeaderWriter.XFRAME_OPTIONS_HEADER, opt));
              } else {
                // Default policy.
                options.deny();
              }
            });

    // CSRF设置,将token存在cookie中,前端需要把cookie中的token读出来放到请求header里,必须要httpOnly的cookie才能被js读取
    boolean csrfCheckEnabled = 数据库或者配置文查找是否支持跨域;
    List<String> csrfCheckHttpMethods =
        settingService.getStringList(SettingItem.WEB_CSRF_CHECK_HTTP_METHODS);
    List<String> csrfCheckExcludePaths =
        settingService.getStringList(SettingItem.WEB_CSRF_CHECK_EXCLUDE_PATHS);
    if (csrfCheckEnabled) {
      http.csrf()
          .requireCsrfProtectionMatcher(
              request -> {
                AntPathMatcher antPathMatcher = new AntPathMatcher();
                String path = request.getRequestURI();
                String method = request.getMethod();
                // 检查白名单
                if (Seq.seq(csrfCheckExcludePaths)
                    .anyMatch(exclude -> antPathMatcher.match(exclude, path))) {
                  return false;
                }

                // 仅仅对API启用csrf
                if (!antPathMatcher.match(API_PATTERN, path)) {
                  return false;
                }

                if (Seq.seq(csrfCheckHttpMethods)
                    .noneMatch(checkMethod -> checkMethod.equalsIgnoreCase(method))) {
                  return false;
                }

                return true;
              })
          .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    } else {
      http.csrf().disable();
    }
    return http.build();
  }

  /**
	提取令牌
   */
  public static class AccessTokenAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
        @NotNull HttpServletRequest request,
        @NotNull HttpServletResponse response,
        @NotNull FilterChain chain)
        throws ServletException, IOException {
      AntPathMatcher antPathMatcher = new AntPathMatcher();
      String path = request.getRequestURI();
      // 公共API无需认证,非私用API无需认证
      boolean skipAuth =
          Seq.of(PUBLIC_API_PATTERNS).anyMatch(pattern -> antPathMatcher.match(pattern, path))
              || Seq.of(PRIVATE_API_PATTERNS)
                  .noneMatch(pattern -> antPathMatcher.match(pattern, path));
      if (skipAuth) {
        // 如果无需认证但是Authentication却被设置了,需要清空,否则还是会进行认证,这可能发生在SecurityContextPersistenceFilter从
        // session中恢复出了一个session的认证信息时。
        if (SecurityContextHolder.getContext().getAuthentication()
            instanceof AccessTokenAuthentication) {
          SecurityContextHolder.getContext().setAuthentication(null);
        }
      } else {
        Optional<String> userNameOpt = UserService.getAuthUserName(request);
        Optional<String> tokenOpt = UserService.getAuthToken(request);
        String clientIp = WebServerUtils.getClientIp(request);
        AccessTokenAuthentication auth =
            new AccessTokenAuthentication(
                tokenOpt.orElse(null), userNameOpt.orElse(null), clientIp);
        // 将authentication设置到(*****)SecurityContext(******),下面会取出来放到userContext方便随时读取用户信息
        SecurityContextHolder.getContext().setAuthentication(auth);
      }

      // 返回这个链式调用
      chain.doFilter(request, response);
    }
  }
//设置authentication中包含的信息
  public static class AccessTokenAuthentication extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 7495641216462496304L;
    private final String accessToken;
    private final String userName;
    private final String clientIp;

    public AccessTokenAuthentication(String accessToken, String userName, String clientIp) {
      super(null);
      this.accessToken = accessToken;
      this.userName = userName;
      this.clientIp = clientIp;
      setAuthenticated(false);
    }

    public String getAccessToken() {
      return this.accessToken;
    }

    public String getUserName() {
      return this.userName;
    }

    public String getClientIp() {
      return this.clientIp;
    }

    @Override
    public Object getCredentials() {
      return null;
    }

    @Override
    public Object getPrincipal() {
      return null;
    }
  }

  /** 验证令牌 */
  @Component
  @AllArgsConstructor(onConstructor_ = {@Autowired})
  public static class AccessTokenAuthenticationProvider implements AuthenticationProvider {
    private final UserService userService;

    @Override
    public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
      AccessTokenAuthentication tokenAuth = (AccessTokenAuthentication) authentication;
      //其实就是从缓存中取用户的登录信息
      userService.authenticate(tokenAuth.userName, tokenAuth.accessToken, tokenAuth.clientIp);
      authentication.setAuthenticated(true);
      return authentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
      return AccessTokenAuthentication.class.isAssignableFrom(authentication);
    }
  }

  @Bean
  public HttpFirewall httpFirewall() {
    // APT的客户端会发出路径包含"/./"的请求,默认的StrictHttpFirewall认为这是非法的url,会拒绝请求,换用DefaultHttpFirewall允许这种请求。
    return new DefaultHttpFirewall();
  }
}

csrf:

Springcoul中gateway中设置对接系统设置白名单时docker容器的IP是动态变化的_java_02

配置登录缓存和全局信息

@Configuration
public class UserConfig {
    /**
    用户登录成功后将一些登录信息放入里面(比如用户名:登录信息【ip、token、权限】)
    loginCache.put(userName, new LoginInfo(clientIp, token, authPerms))
    */
  @Bean
  public Cache<String, LoginInfo> loginCache() {
    return Caffeine.newBuilder()
        .expireAfterWrite(从数据库或者配置文件中读取过期时间)
        .build();
  }
  //将用户的登录信息
  @Bean
  @RequestScope
  public UserContext userContext(
      HttpServletRequest request,
      Cache<String, LoginInfo> loginCache,
      PreferenceService prefService) {
    UserContext context = new UserContext();
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth instanceof WebSecurityConfig.AccessTokenAuthentication) {
      // 带有令牌
      WebSecurityConfig.AccessTokenAuthentication tokenAuth =
          (WebSecurityConfig.AccessTokenAuthentication) auth;
      context.setIpAddr(tokenAuth.getClientIp());
      context.setUserName(tokenAuth.getUserName());
    } else {
      // 没有令牌
      context.setIpAddr(WebServerUtils.getClientIp(request));
      context.setUserName("");
    }

    LoginInfo loginInfo = loginCache.getIfPresent(context.getUserName());
    if (loginInfo != null) {
      // Request from logged in user.
      context.setPerms(loginInfo.getPerms());
    }
    context.setLocale(I18nService.detectLocale(request));

    return context;
  }  
    
}

方法级的授权

当我们想要开启spring方法级安全时,只需要在任何 @Configuration实例上使用 @EnableGlobalMethodSecurity 注解就能达到此目的。同时这个注解为我们提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三种不同的机制来实现同一种功能。

prePostEnabled

prePostEnabled = true 会解锁 @PreAuthorize / @PostAuthorize/ @PreFilter/@PostFilter

  • 字就可以看出@PreAuthorize 注解会在方法执行前进行验证
  • @PostAuthorize 注解会在方法执行后进行验证。
  • @PreFilter: 对集合类型的参数执行过滤,移除结果为false的元素。
  • @PostFilter 基于返回值相关的表达式,对返回值进行过滤

Secured

@Secured注解是用来定义业务方法的安全配置。在需要安全[角色/权限等]的方法上指定 @Secured,并且只有那些角色/权限的用户才可以调用该方法。 但是其不支持springrl表达式。

jsr250E

启用 JSR-250 安全控制注解,这属于 JavaEE 的安全规范(现为 jakarta 项目)。一共有五个安全注解。如果你在 @EnableGlobalMethodSecurity 设置 jsr250Enabled 为 true ,就开启了 JavaEE 安全注解中的以下三个

  • @DenyAll: 拒绝所有访问
  • @RolesAllowed({“USER”, “ADMIN”}): 该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN
  • @PermitAll: 允许所有访问
/**
 * Spring security method security config.
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
  private CustomPermissionEvaluator customPermissionEvaluator;

  @Autowired
  @Lazy
  public void setCustomPermissionEvaluator(CustomPermissionEvaluator customPermissionEvaluator) {
    this.customPermissionEvaluator = customPermissionEvaluator;
  }

  @Override
  protected MethodSecurityExpressionHandler createExpressionHandler() {
    DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
    handler.setPermissionEvaluator(this.customPermissionEvaluator);
    return handler;
  }

  @Component
  @AllArgsConstructor(onConstructor_ = {@Autowired})
  public static class CustomPermissionEvaluator implements PermissionEvaluator {
  
    /**
     * Return true if the user has permission to access the target, false otherwise.
     */
    @Override
    public boolean hasPermission(
        Authentication authentication, Object targetDomainObject, Object permission) {
      return false;
    }

    /**
     * Return true if the user has permission to access the target, false otherwise.
     */
    @Override
    @SuppressWarnings({"rawtypes"})
    public boolean hasPermission(
        Authentication authentication,
        Serializable targetId,
        String targetType,
        Object permission) {
        //将所需的权限 放在set中
      Set<Access> accesses = new HashSet<>();
      if (targetId instanceof Collection) {
        for (Object singleId : (Collection) targetId) {
          accesses.add(toAccess((Long) singleId, targetType, permission));
        }
      } else {
        accesses.add(toAccess((Long) targetId, targetType, permission));
      }
      //和用户自身的权限比对 查看是否有权限
      return hasPermission(accesses);
    }

    private Access toAccess(Long id, String targetType, Object permission) {
      return new Access(
          new Access.Target(Access.TargetType.valueOf(targetType), id),
          Access.Action.valueOf((String) permission));
    }

    private boolean hasPermission(Set<Access> accesses) {
      Set<AuthPerm> required = new HashSet<>();
      for (Access access : accesses) {
        switch (access.getTarget().getType()) {
          case Type1:
            required.addAll();
            break;
          ... 
          default:
            break;
        }
      }
      //和用户所具有的的权限(即从userContext【根本上是从数据库里】中取 )比较,看用户是否有这个权限
      return permService.hasPerms(required);
    }
  }
}

接口上使用:

@GetMapping("/get")
@PreAuthorize("hasPermission(#resourceId,'OPERATION', 'VIEW')")
public List<Resurce> getResource(@RequestParam(value = "resourceId") Long resourceId){
    
}

权限类设置

import lombok.Value;

@Value
public class Access {
  //针对什么目标  
  Target target;
  //对这个目标有什么权限  
  Action action;

  @Value
  public static class Target {
    TargetType type;
    Long id;
  }

  public enum TargetType {
    USER,
   
    BOOK,
   
    PERMISSION,
   
  
    OPERATION
  }

  public enum Action {
    
    CREATE,
   
    EDIT,
   
    DELETE,
   
    VIEW,
   
    DOWNLOAD,
  }
}

数据库权限类

@Table("role_perms")
@Data
@AllArgsConstructor
public class Perm {
  @Id
  @Column("roleName")
  private String roleName;

  @Column("perm")
  private PermType perm;

  @Column("resId")
  private long resId;
}
@AllArgsConstructor
@Getter
public enum PermType {
 
  ADMIN(false, AccessLevel.ADMIN),
 
  GUEST(false, AccessLevel.GUEST),
 
  USER_ADMIN(true, AccessLevel.ADMIN),

  USER_GUEST(true, AccessLevel.GUEST),
 
  BOOK_ADMIN(true, AccessLevel.ADMIN),
  ;

  private final boolean requireResId;
  private final AccessLevel accessLevel;
}
public enum AccessLevel {
  /** ADMIN level. */
  ADMIN,
  /** GUEST level. */
  GUEST
}