JavaWeb中的权限控制——Spring Security

  • A+

SpringSecurity概述

   Spring Security是一个灵活、强大和可定制的身份验证和访问控制框架,以确保基于Spring的Java Web应用程序的安全。

  Spring Security是基于Spring AOP和Servlet过滤器的安全框架。通过若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,增强应用的安全性。

  相关特点:

  • 身份验证和授权提供全面可拓展的支持
  • 可防御会话固定、点击劫持、csrf跨站请求伪造等攻击
  • Servlet API集成
  • 方便集成,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以零配置使用Spring Security
  • .....

工作方式

  spring security内部其实是通过一个过滤器链来实现认证流程的:

permission_Audit14.png
  常见的例如AuthenticationProcessingFilter处理用户登陆相关的操作,ExceptionTranslationFilter用于处理异常并返回对应的页面或者状态码,SessionFixationProtectionFilter用于防止csrf攻击,还有主要用户权限控制的FilterSecurityInterceptor等。
  简单的过滤器链如下:
  以认证授权流程为例,UsernamePasswordAuthenticationFilter用于拦截我们通过表单提交接口提交的用户名和密码,然后在AuthenticationProcessingFilter处理用户登陆认证相关的操作,最后的FilterSecurityInterceptor是首先判断我们当前请求的url是否需要认证,如果需要认证,那么就看当前请求是否已经认证,是的话就放行到我们要访问的接口,否则重定向到认证页面。

permission_Audit15.png

相关依赖

  要使用Spring Security需要引入spring-security-web和spring-security-config:

图片.png
  在Spring Boot中使用Spring Security非常容易,引入spring-boot-starter-security依赖即可。

用户认证与授权

  Spring Security中进行身份验证的是AuthenticationManager接口,ProviderManager是它的一个默认实现,但它并不用来处理身份认证,而是委托给配置好的AuthenticationProvider,每个AuthenticationProvider会轮流检查身份认证。检查后或者返回Authentication对象或者抛出异常。
  Spring Security其支持在内存中设置身份验证,可以直接在配置文件中配置:

图片.png
  注解形式:

```java
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{

@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception{
    auth.inMemoryAuthentication().withUser("test").password("123456").roles("USER");
}

}
```
  实际开发中用于验证的用户一般都存储在数据库中,上述方式明显不太合适,那么就需要在AuthenticationProvider中创建基于数据库自定义UserDetailsService进行相关的认证操作。下面是相应实现的流程:
  验证身份就是加载响应的UserDetails,看看是否和用户输入的账号、密码、权限等信息匹配。此步骤由实现AuthenticationProvider的DaoAuthenticationProvider(它利用UserDetailsService验证用户名、密码和授权)处理。包含 GrantedAuthority 的 UserDetails对象在构建 Authentication对象时填入数据。

图片.png
  举例如下,因为Spring Security是通过委托AuthenticationProvider进行认证的,那么首先需要在其配置中注入userDetailsService():

图片.png
  若使用注解方式的话通过auth.userDetailsService()方法即可。
  根据上述配置,可以直接定位到userDetailsService的具体实现在com.framework.security.UserDetailsServiceImpl。
  在loadUserByUsername()方法,根据username查询用户实体,可以实现该接口覆盖该方法,实现自定义获取用户过程。

```java
public class UserDetailService implements UserDetailsService{
@Autowired
private IUserService userService;

@Overrid
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
    UserDetails userDetails=null;
    try{
        com.security.entity.user user = this.userService.Login(username);
        Collection<GrantedAuthority> authList = getAuthorities(user.getRole());
        userDetails = new User(username,user.getPassword,true,true,true,true,authList);
    }catch(Exception e){
        //TODO:handle exception
        throw new UsernameNotFoundException(username);
    }           
    return userDetails;
}
//获取当前登陆用户的角色权限
private Collection<GrantedAuthority> getAuthorities(String role){
    List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
    if(role.equals("ROLE_ADMIN")){
        authList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }else{
        authList.add(new SimpleGrantedAuthority("ROLE_STATIC));
    }
    return authList;
}

}
```

其他配置

  除了认证授权以外,还有很多强大的过滤器可以进行相关的安全防护,例如csrf防护、单点登录限制等,均可以通过相关的标签或者方法进行实现:

permission_Audit17.png

权限控制方式

  通过UserDetailsService完成用户认证以及权限角色赋予后,可以结合以下方式完成相关的权限控制:

通过HttpSecurity对象

  Spring Security的xml配置文件命名空间配置中的<http>元素。它允许对特定的http请求基于安全考虑进行配置。
  例如如下配置,pattern代表匹配的路径,security="none"表示放行,不进行拦截及权限检查:
xml
<http pattern="/static/**" security="none"></http>

  通过其子元素<intercept-url>可以对相关接口进行权限控制。例如/system下的所有接口仅仅只有ROLE_ADMIN,ROLE_USER权限的用户才可以访问,login接口支持匿名访问:

xml
<intercept-url pattern="/system/**" access="ROLE_ADMIN,ROLE_USER" />
<intercept-url pattern="/login" access="IS_AUTHENTICATED_ANONYMOUSLY"/>

  此外,还可以通过SpEL表达式进行配置,可以通过<http>元素的use-expressions来设置(默认为true,开启),例如下面的例子,用户匹配ROLE_ADMIN,ROLE_USER其中的任何一个均可放行:

xml
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/system/**" access="hasRole('ROLE_USER','ROLE_ADMIN')"/>
</http>

  常用的有:

| 表达式 | 功能 |
| ------------------------- | ------------------------------------------------ |
| hasRole([role]) | 用户拥有指定的角色role时候返回true |
| hasAnyRole([role1,role2]) | 用户拥有role1,role2任意一个指定的角色时返回true |
| permitAll | 任何用户均可访问 |
| hasIpAddress (ipAddress) | 匹配一个请求的IP地址 |
| denyAll | 任何用户都不可以访问 |
| authenticated | 检查用户是否认证通过 |
| anonymous | 匿名用户即可访问 |

  除此之外,也可以使用@EnableWebSecurity注解,然后继承适配器WebSecurityConfigurerAdapter,通过HttpSecurity对象的相关配置进行权限控制。主要是通过http.authorizeRequests()方法,通过每个子匹配器进行相关的权限声明。例如下面的例子:

java
protected void configure(HttpSecurity http ) throws Exception {
http.authorizeRequests()
.antMatchers( "/resources/**", "/signup" , "/about").permitAll()//permitAll代表这类资源任何用户均可访问
.antMatchers( "/admin/**").hasRole("ADMIN" )//只有具有ROLE_ADMIN角色的用户才能访问/admin开头的接口
.antMatchers( "/db/**").access("hasRole('ADMIN') and hasRole('DBA')") //同时拥有ROLE_ADMIN和ROLE_DBA角色的用户才能访问/db开头的接口
.anyRequest().authenticated()//没有匹配的请求只需要用户认证成功即可访问
// ...
.formLogin();
}

  通过antMatchers方法即可定义什么样的请求可以放过,什么样的请求需要验证。也可以通过regexMatchers()方法通过正则表达式来定义请求路径。

通过相关标签

  在实际业务场景中,对于已登陆的用户角色,需要根据不同的权限对view层进行保护,显示/隐藏Web应用程序的JSP/View。例如查看所有用户信息的页面仅仅只有管理员角色才可以查看。对于普通用户来说需要隐藏。

  在jsp页面中我们可以使用spring security提供的权限标签来进行权限控制,需要引入spring security-taglibs标记库的依赖库:

xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.0.1.RELEASE</version>
</dependency>

  然后通过在jsp页面中引入标签库,通过对应的标签来实现:

java
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%>

  常见的标签如下:

| 标签 | 功能 |
| ---------------------------------- | :----------------------------------------------------------- |
| <security:authenticatio> | 获取当前认证对象信息,例如用户名 |
| <security:authorize> | 若当前用户满足特定权限,则显示标签范围的内容 |
| <security:accesscontrollist> | 若认证用户具有权限列表中的某一个权限,那这个标签范围的内容将显示(一般用于鉴定ACL权限) |

  例如下面的例子:

  只有拥有ADMIN角色的用户才能查看相关的页面:

java
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<div>
<sec:authorize access="hasRole("ADMIN)">
......
<form name="contact-form" class="contact-form" method="post" action="./deleteUser/ByUserId">
......
</sec:authorize>
</div>

通过权限注解

  Spring Security默认是禁用注解的,要想开启注解,有以下方式:

  • 继承WebSecurityConfigurerAdapter并且加上@EnableGlobalMethodSecurity注解,并在该类中将AuthenticationManager定义为Bean:

  例如下面是开启了secured的注解支持:

```java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled=true)
public class CasSecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
AuthenticationManager authenticationManager;

}
```

  • 在xml配置文件中开启注解支持:

java
<security:global-method-security pre-post-annotations="enabled">
<security:global-method-security jsr250-annotations="enabled">
<security:global-method-security secured-annotations="enabled">

  常见的注解:

  • securedEnabled

  开启@Secured 注解过滤权限,可以在需要安全[角色/权限等]的方法上指定 @Secured,并且只有那些角色/权限的用户才可以调用该方法。如果有人不具备要求的角色/权限但试图调用此方法,将会抛出AccessDenied异常。但是有一个缺点是,无法满足&&的关系(例如角色同时为admin和system时才能调用该接口)。

  例如下面的例子:

```java
import org.springframework.security.access.annotation.Secured;

public interface UserService{

@Secured("ROLE_ADMIN")//具备ROLE_ADMIN角色的用户才可以访问
List<User> findAllUsers();

@Secured({"ROLE_ADMIN","ROLE_USER"})//具备ROLE_ADMIN或者ROLE_USER角色的用户均可以访问。
User getUserByName(String name);

......

}
```

  • jsr250Enabled

  主要用于提供角色的权限约束,常用注解有:

| 注解 | 功能 |
| -------------------------------- | ------------------------------------------- |
| @DenyAll | 拒绝所有访问 |
| @PermitAll | 允许所有访问 |
| @RolesAllowed({"USER", "ADMIN"}) | 具有"USER", "ADMIN"任意一种权限就可以访问。 |

  • prePostEnabled

  基于表达式的注解,可以自定义扩展。主要包括以下注解:

| 注解 | 功能 |
| -------------- | ------------------------------------------------------------ |
| @PreAuthorize | 在方法调用之前,基于表达式的结果来限制对方法的访问 |
| @PostAuthorize | 在方法执行后再给予表达式的结果进行权限验证,适合验证带有返回值的权限 |
| @PostFilter | 在方法执行之后执行,这里可以调用方法的返回值,然后对返回值进行过滤或处理或修改并返回。 |
| @PreFilter | 在方法执行之前执行,而且这里可以调用方法的参数,然后对参数值进行过滤或处理或修改。 |

  比较常用的是@PreAuthorize和@PostAuthorize。

  例如下面的例子:

  通过@PostAuthorize注解确保登录用户只能获取他自己的用户对象防止平行越权,还有在调用删除用户的方法前先检查是否具有ADMIN角色权限:

```java
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;

......

public interface UserService{

@PostAuthorize("returnObject.type == authentication.name ")
User findById(int id);

@PreAuthorize("hasRole('ADMIN')")
boolean deleteUser(int id)

......

}
```

审计要点

  权限控制相关的审计要点有:

平行越权

  SpringSecurity可以通过如下方法获取当前用户:
java
SecurityContextHolder.getContext().getAuthentication()

  一般来说,使用类似@PostAuthorize注解可以比较好的解决平行越权的问题,但是在多表关联的场景下很难进行覆盖。所以针对类似的业务接口还是需要进行相关的检查。

垂直越权

  一般使用标签库对普通用户与管理员用户的在view层页面上进行了区分,但是没有细粒度覆盖接口,认为只要在界面上没法操作就可以防止垂直越权了。
  例如下面的例子,通过<sec:authorize>标签,在view层进行了限制,仅仅拥有ADMIN角色的用户才能查看到删除用户对应的功能模块:
java
<div>
<sec:authorize access="hasRole('ADMIN')">
<form action="${pageContext.request.contextPath}/system/delete" method="POST">
.........
</form>
</sec:authorize>
</div>

  可以直接定位/system/delete接口,结合配置文件查看对应的权限细粒度覆盖.
  在安全配置时仅仅考虑到了未授权访问的问题:

xml
<http use-expression="false">
<intercept-url pattern="login" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/system/**" access="ROLE_USER"/>
......

  再查看实际的接口鉴权,可以看到接口处也没有任何的权限注解,也就是说只要通过登陆认证,以普通用户的角色也可以访问管理员删除用户的接口:
java
@RequestMapping("/delete")
public Boolean deleteUser(String userId){
Boolean result = userservice.deleteById(userId);
return result;
}

  Tips:在进行审计时,可以先在view层搜索对应的SpringSecurity标签关键字(例如<sec:authorize>),然后查看对应隐藏/显示的接口,通过检查相关的权限配置以及接口具体实现,快速寻找垂直越权缺陷

静态资源

  一般静态资源和登陆接口可以非登陆状态下匿名使用,一般在对应的权限配置中会做如下配置:
xml
<http pattern="/common/**" security="none"/>
<http pattern="/*.pdf" security="none"/>

  或者是如下方式:
java
antMatchers("/","/login**", "/webjars*", "/static*").permitAll()

  但是部分静态文件可能存在未授权访问,例如网站部分业务的模版文件、内部资料,或者是通过相关接口道出的统计Excel表、PDF单据等,都可能存在匿名下载的风险。

  所以可以定位相关的目录,审计目录下的内容以及可能关联的接口。

权限绕过

  同时还需要进行版本检查,避免相关的权限绕过:
* CVE-2010-3700: Spring Security bypass of security constraints
* 影响范围:(需部署在IBM WebSphere Application Server (WAS) 6.1 and 7.0容器中)
* Spring Security
* 3.0.0 to 3.0.3
* 2.0.0 to 2.0.5
* CVE-2016-5007 Spring Security / MVC Path Matching Inconsistency
* 影响范围
* Spring Security
* 3.2.x,4.0.x,4.1.0
* 具体分袖参考CVE-2016-5007,跟之前的shiro绕过权限控制原理类似。
* CVE-2016-9879 Encoded "/" in path variables
* 影响范围(需部署在IBMWebSphereApplication Server 8.5.x 的容器中)
* SpringSecurity
* 3.2.0 - 3.2.9
* 4.0.x - 4.1.3
* 4.2.0

* 旧的不维护的版本也会受到影响
* CVE-2018-1199: Security bypass with static resources
* 影响范围
* Spring Security
- 4.1.0 - 4.1.4
- 4.2.0 - 4.2.3
- 5.0.0

参考资料

https://docs.spring.io/spring-security/site/docs/current/reference/html5/

相关推荐: JavaWeb中常见的信息泄漏——druid监控台

关于druid       Druid是阿里巴巴开源平台上的一个数据库连接池实现,结合了C3P0、DBCP、PROXOOL等数据库连接池的优点,同时加入了日志监控,可以很好的监控DB池连接以及SQL的…