【Spring】Spring Security 相关知识学习总结

在 Web 应用开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问 题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构 已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安 全相关的因素考虑进来,并在整个应用的开发过程中。

本文详细介绍了如何使用 Spring Security 来保护 Web 应用的安全。Spring Security 本身以及 Spring 框架带来的灵活性,能够满足一般 Web 应用开发的典型需求,并允许开发人员进行定制。

一.Spring Security 简介

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中 的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户 是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说, 系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

二.基本用户认证和授权

本节从最基本的用户认证和授权开始对 Spring Security 进行介绍。一般来说,Web 应用都需要保存自己系统中的用户信息。这些信息一般保存在数据库中。用户可以注册自己的账号,或是由系统管理员统一进行分配。这些用户一般都有自己的角 色,如普通用户和管理员之类的。某些页面只有特定角色的用户可以访问,比如只有管理员才可以访问 /admin 这样的网址。下面介绍如何使用 Spring Security 来满足这样基本的认证和授权的需求。

(1) 第一步,需要把 Spring Security 引入到 Web 应用中来,这是通过在 web.xml添加一个新的过滤器来实现的。
Spring Security 使用的是 Servlet 规范中标准的过滤器机制。对于特定的请求,Spring Security 的过滤器会检查该请求是否通过认证,以及当前用户是否有足够的权限来访问此资源。对于非法的请求,过滤器会跳转到指定页面让用户进行认证,或是返回出错信 息。需要注意的是,web.xml中虽然只定义了一个过滤器,Spring Security 实际上是使用多个过滤器形成的链条来工作的。在web.xml中相关配置如下所示。

<filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
</filter-mapping>

(2) 第二步,是配置 Spring Security 来声明系统中的合法用户及其对应的权限。
用户相关的信息是通过 org.springframework.security.core.userdetails.UserDetailsService 接口来加载的。该接口的唯一方法是 loadUserByUsername(String username),用来根据用户名加载相关的信息。这个方法的返回值是 org.springframework.security.core.userdetails.UserDetails 接口,其中包含了用户的信息,包括用户名、密码、权限、是否启用、是否被锁定、是否过期等。其中最重要的是用户权限,由 org.springframework.security.core.GrantedAuthority 接口来表示。虽然 Spring Security 内部的设计和实现比较复杂,但是一般情况下,开发人员只需要使用它默认提供的实现就可以满足绝大多数情况下的需求,而且只需要简单的配置声明即可。

public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        UserDetails userDetails = null;
        User user = findByUserno(s);//根据用户编号获取用户信息
        if (user == null) {
            String warnMsg = "用户(" + s + ")禁用或者不存在!";
            throw new UsernameNotFoundException(warnMsg);
        }
        String myRole = null;
        // 区分角色
        if (user.getUno().equalsIgnoreCase("admin")) {
            // 查找登录用户角色
            myRole = UserRole.SPRING_SECURITY_ROLE_PREFIX.concat(UserRole.ADMIN);
        } else {
            myRole = UserRole.SPRING_SECURITY_ROLE_PREFIX.concat(UserRole.COMM);
        }
        LOG.info("用户(" + s + ")角色信息:" + myRole);
        userDetails = new org.springframework.security.core.userdetails.User(user.getUno(), user.getPwd(), true, true,
                true, true, Arrays.asList(new GrantedAuthorityImpl(myRole)));
        return userDetails;
    }

(3) 第三步,配置用户对不同资源的访问权限了,这里的资源指的是 URL 地址。配置的内容如下所示。

<security:http auto-config="true" access-denied-page="/errors/403"
        use-expressions="true"> 
    <security:intercept-url pattern="/president.do**" access="ROLE_PRESIDENT" /> 
    <security:intercept-url pattern="/manager.do**" access="ROLE_MANAGER" /> 
    <security:intercept-url pattern="/**" access="ROLE_USER" /> 
    <security:form-login login-page="/login"
            authentication-failure-url="/login?error=true" default-target-url="/dashboard" />

    <security:logout invalidate-session="true"
            delete-cookies="JSESSIONID" logout-success-url="/login" logout-url="/logout" />
 </security:http>

一共定义了三种角色:普通用户、经理和总裁,分别用 ROLE_USER、ROLE_MANAGER 和 ROLE_PRESIDENT 来表示。代码清单 3 中定义了访问不同的 URL 模式的用户所需要的角色。这是通过 元素来实现的,其属性 pattern 声明了请求 URL 的模式,而属性 access 则声明了访问此 URL 时所需要的权限。需要按照 URL 模式从精确到模糊的顺序来进行声明。因为 Spring Security 是按照声明的顺序逐个进行比对的,只要用户当前访问的 URL 符合某个 URL 模式声明的权限要求,该请求就会被允许。如果把 代码清单 3 中本来在最后的 URL 模式 /** 声明放在最前面,那么当普通用户访问 /manager.do 的时候,该请求也会被允许。这显然是不对的。通过 元素声明了使用 HTTP 表单验证。也就是说,当未认证的用户试图访问某个受限 URL 的时候,浏览器会跳转到一个登录页面,要求用户输入用户名和密码。 元素声明了提供用户注销登录的功能。默认的注销登录的 URL 是 /j_spring_security_logout,可以通过属性 logout-url 来修改。

当完成这些配置并运行应用之后,会发现 Spring Security 已经默认提供了一个登录页面的实现,可以直接使用。开发人员也可以对登录页面进行定制。通过 的属性 login-page、login-processing-url 和 authentication-failure-url就可以定制登录页面的 URL、登录请求的处理 URL 和登录出现错误时的 URL 等。从这里可以看出,一方面 Spring Security 对开发中经常会用到的功能提供了很好的默认实现,另外一方面也提供了非常灵活的定制能力,允许开发人员提供自己的实现。

三. Spring Security 里面的核心对象。

org.springframework.security.core.context.SecurityContext接口表示的是当前应用的安全上下文,通过此接口可以获取和设置当前的认证对象。
org.springframework.security.core.Authentication接口用来表示此认证对象,通过认证对象的方法可以判断当前用户是否已经通过认证,以及获取当前认证用户的相关信息,包括用户名、密码和权限等。
要使用此认证对象,首先需要获取到 SecurityContext 对象。通过 org.springframework.security.core.context.SecurityContextHolder 类提供的静态方法 getContext() 就可以获取。再通过 SecurityContext对象的 getAuthentication()就可以得到认证对象。通过认证对象的 getPrincipal() 方法就可以获得当前的认证主体,通常是 UserDetails 接口的实现。联系到上一节介绍的 UserDetailsService,典型的认证过程就是当用户输入了用户名和密码之后,UserDetailsService通过用户名找到对应的 UserDetails 对象,接着比较密码是否匹配。如果不匹配,则返回出错信息;如果匹配的话,说明用户认证成功,就创建一个实现了 Authentication接口的对象,如 org.springframework.security. authentication.UsernamePasswordAuthenticationToken 类的对象。再通过 SecurityContext的 setAuthentication() 方法来设置此认证对象。
下面是使用 SecurityContext 和 Authentication的一个示例,用来获取当前认证用户的用户名。

 public static String getAuthenticatedUsername() { 
    String username = null; 
    Object principal = SecurityContextHolder.getContext() 
        .getAuthentication().getPrincipal(); 
    if (principal instanceof UserDetails) { 
        username = ((UserDetails) principal).getUsername(); 
    } else { 
        username = principal.toString(); 
    } 
    return username; 
 }

四. JSP 标签库,Spring Security 提供的 JSP 标签库

之前的章节中介绍了在 Java 代码中如何使用 Spring Security 提供的能力。很多情况下,用户可能有权限访问某个页面,但是页面上的某些功能对他来说是不可用的。比如对于同样的员工列表,普通用户只能查看数据,而具有 经理角色的用户则可以看到对列表进行修改的链接或是按钮等。Spring Security 提供了一个 JSP 标签库用来方便在 JSP 页面中根据用户的权限来控制页面某些部分的显示和隐藏。使用这个 JSP 标签库很简单,只需要在 JSP 页面上添加声明即可:<%@ taglib prefix=”sec” uri=”http://www.springframework.org/security/tags” %>。这个标签库包含如下 3 个标签:

  1. authorize标签:该标签用来判断其中包含的内容是否应该被显示出来。判断的条件可以是某个表达式的求值结果,或是是否能访问某个 URL,分别通过属性 access和 url来指定。如 限定内容只有具有经理角色的用户才可见。限定内容只有能访问 URL/manager_portal.do的用户才可见。
  2. authentication标签:该标签用来获取当前认证对象(Authentication)中的内容。如 可以用来获取当前认证用户的用户名。
  3. accesscontrollist标签:该标签的作用与 authorize标签类似,也是判断其中包含的内容是否应该被显示出来。所不同的是它是基于访问控制列表来做判断的。该标签的属性 domainObject表示的是领域对象,而属性 hasPermission表示的是要检查的权限。如 限定了其中包含的内容只在对领域对象 myReport有读权限的时候才可见。
    值得注意的是,在使用 authorize标签的时候,需要通过 来启用表达式的支持。查看 权限控制表达式一节了解关于表达式的更多内容。

五. 与 Spring Security 相关权限控制表达式、会话管理和记住用户等。

  1. 权限控制表达式
    有些情况下,对于某种资源的访问条件可能比较复杂,并不只是简单的要求当前用户具有某一个角色即可,而是由多种条件进行组合。权限控制表达式允许使用一种简单的语法来描述比较复杂的授权条件。Spring Security 内置了一些常用的表达式,包括 hasRole()用来判断当前用户是否具有某个角色,hasAnyRole()用来判断当前用户是否具备列表中的某个角色,以及 hasPermission()用来判断当前用户是否具备对某个领域对象的某些权限等。这些基本表达式可以通过 and和 or等组合起来,表示复杂的语义。当通过 启用了表达式支持之后,就可以在 元素的 access属性上使用表达式。例如:
<security:http auto-config="true" access-denied-page="/errors/403" use-expressions="true">
        <security:intercept-url pattern="/login*" access="isAnonymous()" />
</security:http>

表达式还可以用来对方法调用进行权限控制,主要是用在方法注解中。要启用 Spring Security 提供的方法注解,需要添加元素 。这几个方法注解分别是:

@PreAuthorize:该注解用来确定一个方法是否应该被执行。该注解后面跟着的是一个表达式,如果表达式的值为真,则该方法会被执行。如 @PreAuthorize(“hasRole(‘ROLE_USER’)”)就说明只有当前用户具有角色 ROLE_USER的时候才会执行。
@PostAuthorize:该注解用来在方法执行完之后进行访问控制检查。
@PostFilter:该注解用来对方法的返回结果进行过滤。从返回的集合中过滤掉表达式值为假的元素。如 @PostFilter(“hasPermission(filterObject, ‘read’)”)说明返回的结果中只保留当前用户有读权限的元素。
@PreFilter:该注解用来对方法调用时的参数进行过滤。
2. 会话管理
Spring Security 提供了对 HTTP 会话的管理功能。这些功能包括对会话超时的管理、防范会话设置攻击(Session fixation attack)和并发会话管理等。

如果当前用户的会话因为超时而失效之后,如果用户继续使用此会话来访问,Spring Security 可以检测到这种情况,并跳转到适当的页面。只需要在 元素下添加 元素即可,属性 invalid-session-url指明了会话超时之后跳转到的 URL 地址。

有些 Web 应用会把用户的会话标识符直接通过 URL 的参数来传递,并且在服务器端不进行验证,如用户访问的 URL 可能是 /myurl;jsessionid=xxx。 攻击者可以用一个已知的会话标识符来构建一个 URL,并把此 URL 发给要攻击的对象。如果被攻击者访问这个 URL 并用自己的用户名登录成功之后,攻击者就可以利用这个已经通过认证的会话来访问被攻击者的数据。防范这种攻击的办法就是要求用户在做任何重要操作之前都重 新认证。Spring Security 允许开发人员定制用户登录时对已有会话的处理,从而可以有效的防范这种攻击。通过 元素的属性 session-fixation-protection可以修改此行为。该属性的可选值有 migrateSession、newSession和 none。migrateSession是默认值。在这种情况下,每次用户登录都会创建一个新的会话,同时把之前会话的数据复制到新会话中。newSession表示的是只创建新的会话,而不复制数据。none表示的是保持之前的会话。

在有些情况下,应用需要限定使用同一个用户名同时进行登录所产生的会话数目。比如有些应用可能要求每个用户在同一时间最多只能有一个会话。可以通过 元素的子元素 来限制每个用户的并发会话个数。如 就限定了每个用户在同一时间最多只能有两个会话。如果当前用户的会话数目已经达到上限,而用户又再次登录的话,默认的实现是使之前的会话失效。如果希望阻止后面的这次登录的话,可以设置属性 error-if-maximum-exceeded的值为 true。这样的话,后面的这次登录就会出错。只有当之前的会话失效之后,用户才能再次登录。
3. 记住用户
有些 Web 应用会在登录界面提供一个复选框,询问用户是否希望在当前计算机上记住自己的密码。如果用户勾选此选项的话,在一段时间内用户访问此应用时,不需要输入用户名和密码进行登录。Spring Security 提供了对这种记住用户的需求的支持。只需要在 中添加 元素即可。

<security:http auto-config="true" access-denied-page="/errors/403" use-expressions="true">
        <security:remember-me key="1q2w3e4r" token-validity-seconds="2592000" />
</security:http>

一般来说,有两种方式可以实现记住用户的能力。一种做法是利用浏览器端的 cookie。当用户成功登录之后,特定内容的字符串被保存到 cookie 中。下次用户再次访问的时候,保存在 cookie 中的内容被用来认证用户。默认情况下使用的是这种方式。使用 cookie 的做法存在安全隐患,比如攻击者可能窃取用户的 cookie,并用此 cookie 来登录系统。另外一种更安全的做法是浏览器端的 cookie 只保存一些随机的数字,而且这些数字只能使用一次,在每次用户登录之后都会重新生成。这些数字保存在服务器端的数据库中。如果希望使用这种方式,需要创建 一个数据库表,并通过 data-source-ref属性来指定包含此表的数据源。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值