简介

  • 基于Spring的应用程序提供声明式安全保护的安全性框架,提供了完整的安全性解决方案,能够在web请求级别和方法调用级别处理身份验证和授权,充分使用了依赖注入和面向切面的技术。
  • 主要从两个方面解决安全性问题
    • web请求级别:使用servlet过滤器保护web请求并限制url的访问
    • 方法调用级别:使用Spring AOP保护方法调用,确保具有适当权限的用户才能访问
  • 总结:访问url时首先判断是否登录,其次用户是否具有权限访问。

    SpringMvc整合Spring Security

  • pom.xml添加依赖

    <!--版本管理,只需管理这个依赖即可使所有spring security的依赖版本统一-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-bom</artifactId>
                <version>5.2.0.BUILD-SNAPSHOT</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <!--最少的版本-->
    <dependencies>
        <!--过滤器:身份验证与url接入控制-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
        </dependency>
        <!--包含命名空间,springMVC整合时需要在配置文件中配置spring security,所以这个必须要-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId> <!--版本是n.n则命名空间也得一样-->
        </dependency>
    </dependencies>
    
  • 还有一个单点登录,有时也需要

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-cas</artifactId>
    </dependency>
    
  • 整合方法:
  1. 配置文件实现:在配置文件中指定拦截的url、所需权限、配置userDetailsService指定用户名密码对应权限即可
  2. Java Configuration:以下可以只选一个部分来用java实现,其他可由配置文件代替,毕竟不是所有的模块都需要自定义。
    • 实现UserDetailsService,重写loadUserByUsername(String userName),根据username来实现自己的业务逻辑,返回UserDetails的实现类,还需要自定义实体类User implement UserDetails(顺便作为系统的用户类使用了),主要重写getAuthorities()返回该用户所拥有的权限
    • 自定义filter重写Spring Security拦截器,实现动态过滤用户权限
    • 自定义filter重写Spring Security拦截器,实现自定义参数来检验用户,并且过滤权限

      配置文件实现

  • web.xml中声明代理servlet过滤器。原本的实现是配置一系列相关的,使得配置文件臃肿难以阅读,而Spring security提供的代理servlet过滤器功能可以解决该问题。

    <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>
    
  • 其中,DelegatingFilterProxy是一个代理的servlet过滤器,它主要负责将工作委托给javax.servlet.Filter实现类,这个实现类作为一个已经注册在Spring上下文中,其id便是上面的名字。也即它会自动到Spring容器查找名为filter-name的bean并把所有Filter的操作委托给它,所以如果不是默认注入到Spring容器的bean,就需要手动注入。
  • applicationContext.xml中配置拦截请求

    //添加命名空间
    xmlns:sec="http://www.springframework.org/schema/security"
    xsi:http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd" //对应版本是5.2.xsd才对
    
    <!-- 配置为none的不经过任何spring的过滤器,主要是静态资源,我们不需要经过过滤器 -->
    <http pattern="/resources/**" security="none" />
    <http pattern="/sitemap.xml" security="none" />
    <http pattern="/favicon.ico" security="none" />
    
    <!--或者-->
    <security:http use-expressions="true">
        <!-- 顺序不能乱 ,按照从上往下依次过滤 -->
        <security:intercept-url pattern="/ps_service/index.jsp" access="permitAll"/>
        <security:intercept-url pattern="/ps_service/login.do" access="permitAll"/>
        <security:intercept-url pattern="/ps_service/loginfailed.do" access="permitAll"/>
        <security:intercept-url pattern="/cs/404.html" access="permitAll"/>
        <!--过滤浏览器自动发起的链接-->
        <security:intercept-url pattern="/favicon.ico"access="permitAll"/>
    </security:http>
    
    <!-- 
    auto-config: true自动加载,自动生成一个默认登陆页面,url:localhost:8080/project/login;
        如果不使用该属性 则默认为http-basic(没有session).;一般都是true了
    lowercase-comparisons:表示URL比较前先转为小写。
    path-type:表示使用Apache Ant的匹配模式。
    access-denied-page:访问拒绝时转向的页面。
    access-decision-manager-ref:指定了自定义的访问策略管理器。当系统角色名的前缀不是默认的ROLE_时,需要自定义访问策略管理器。
    -->
    <sec:http auto-config="true" servlet-api-provision="false" lowercase-comparisons="false"
    access-denied-page="/html/error_page_access_denied.html" path-type="ant"
    access-decision-manager-ref="accessDecisionManager">
        <!-- 
        login-page:指定登录页面。
        login-processing-url:指定了客户在登录页面中按下 Sign In 按钮时要访问的 URL。与登录页面form的action一致。其默认值为:/j_spring_security_check。
        authentication-failure-url:登录的用户没有指定权限时跳转到的页面;指定权限在第120行
        default-target-url:登录成功呈现给用户的页面。
        always-use-default-target:指定了是否在身份验证通过后总是跳转到default-target-url属性指定的URL。
        -->
        <sec:form-login login-page="/admin/page!login.action" login-processing-url="/admin/login"
        default-target-url="/admin/page!main.action" authentication-failure-url="/admin/page!login.action" 
        authentication-success-handler-ref="myAuthenticationSuccessHandler"
        always-use-default-target="true"  />
    
        <!-- "记住我"功能,采用持久化策略(将用户的登录信息存放在数据库表中) -->
        <sec:remember-me data-source-ref="dataSource" key="e37f8888-0ooo-22dd-bd0b-9900211c9a66" />
    
        <!-- 
        logout-url:指定了用于响应退出系统请求的URL,即注销页面。其默认值为:/j_spring_security_logout。下例与与 <a href="<c:url value="/admin/logout" />">注销</a> 配合使用
        logout-success-url:退出系统后转向的URL,即注销成功后跳转到的页面。
        invalidate-session:指定在退出系统时是否要销毁Session。
        -->
        <sec:logout invalidate-session="true" logout-success-url="/admin/page!login.action" 
        logout-url="/admin/logout" />
    
        <!-- 
        max-sessions:允许用户帐号登录的次数。范例限制用户只能登录一次。
        exception-if-maximum-exceeded: 默认为false,此值表示:用户第二次登录时,前一次的登录信息都被清空。
        当exception-if-maximum-exceeded="true"时系统会拒绝第二次登录。
        -->
        <sec:concurrent-session-control max-sessions="1" exception-if-maximum-exceeded="false" />
    
        <!-- 后台登录 -->
        <!--intercept-url:拦截器,可以设定哪些路径需要哪些权限来访问. filters=none 不使用过滤,也可以理解为忽略--> 
        <sec:intercept-url pattern="/admin/page!login.action" filters="none" />
        <!-- 商品管理 -->
        <!--限制具有ROLE_GOODS权限的用户才能访问
        (4.0后版本只能用hasRole("ROLE_GOODS")-->
        <sec:intercept-url pattern="/admin/goods!**" access="ROLE_GOODS" />
        <!-- 基础管理权限 -->
        <!--拦截/admin下的所有请求-->
        <sec:intercept-url pattern="/admin/**" access="ROLE_BASE" />
    </sec:http>
    
    <sec:http auto-config="true" use-expressions="true">
        <!-- 配置为permitAll允许所有已登录或者未登录用户访问,但依然经过过滤器处理。除此之外所有的页面都需要登录访问 -->
        <sec:intercept-url pattern="/"  access="permitAll" />
        <sec:intercept-url pattern="/index*" access="permitAll" />
        <sec:intercept-url pattern="/signin*" access="permitAll" />
        <sec:intercept-url pattern="/login*" access="permitAll" />
        <sec:intercept-url pattern="/register*" access="permitAll" />
        <sec:intercept-url pattern="/invalidsession*" access="permitAll" />
        <sec:intercept-url pattern="/404*" access="none" />
    
        <sec:access-denied-handler error-page="/403" />
    
        <!-- 关闭跨域请求,默认开启,最好关闭 -->
        <security:csrf disabled="true"/>
    
    </sec:http>
    
    <!--角色继承-->
    <bean id="roleHierarchy"
          class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
        <property name="hierarchy"><!-- 角色继承关系 -->
            <value>
                ROLE_ADMIN > ROLE_USER //ADMIN继承USER的所有权限,后者能访问的资源它也可以
                ROLE_A > ROLE_B
                ROLE_B > ROLE_C
                ROLE_C > ROLE_D
            </value>
         </property>
    </bean>
    
  • http元素将创建一个FilterChainProxy以及链中的所有过滤器的bean,同时会将FilterChainProxy的bean托管给配置在web.xml中的DelegatingFilterProxy。

    登录验证
  • auto-config=”true”自动加载的同时生成默认登录页面,相当于如下配置:

    <http>
        <form-login/>
       <!--HTTP 基本认证 -->
        <http-basic/>
      <!-- 可以通过logout-url属性设置用户退出的url-->
        <logout />
        <intercept pattern="/**" access="ROLE_DEMO" />
    </http>
    
  • 登录请求提交地址还是登录页面的地址,但是过滤器(不管是否自定义)会把参数获取过去验证。
  • 一个简单的例子

    <!--过滤器,拦截/admin下的所有请求,限制具有admin权限的用户才能访问-->
    <sec:http auto-config="true">
    <!--true则自动生成一个登陆页面,url:localhost:8080/project/login-->
        <sec:intercept-url pattern="/**" access="authenticated" /> <!--访问任何页面都跳转登录界面-->
    </sec:http>
    
    <!-- 安全认证管理,先写个简单的后面再改-->
    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <!--设置用户库中的用户;用此用户登录才能成功-->
                <sec:user name="admin" password="123456" authorities="ROLE_USER"/>
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
    
  • 登录页面

  • 登录失败转回登录页面
  • 登录成功则进入原本要访问的页面或默认主页
  • 虽然设置auto-config=”true”可以自动生成登录页面,但是一般都是使用自定义的登录页面

    <http auto-config="true">
        <!-- 设置登录页配置 login-page指定了登录界面的视图,authentication-failure-url则设置失败后的重定向到相同的登录页面-->
        <from-login login-processing-url="/static/j_spring_security_check"
            login-page="/login" 
            authentication-failure-url="/login?login_error=t"
            username-parameter="sescs_username"
            password-parameter="sescs_password">
    </http>
    
  • 在自定义的登录页面中将表单提交地址定为“/static/j_spring_security_check”(3.x默认路径,而4.x则是/login),未定义username和password参数名的话将用户名和密码输入框的name分别设置为j_username和j_password。(4.x已定义为username、password)
  • 自定义时应设表单提交路径与登录页面路径不同,应为相同的话万一前面配置了不拦截对登录页面的请求,那就相当于也不拦截白哦单提交请求了,那还怎么获取表单来验证。
  • 实现记住密码功能

    <!--key设置cookie的秘钥的值,默认是SpringSecured。后一个属性指定有效期  -->
    <remember-me key="spitterKey" token-validity-seconds="2419200"/>
    
  • 则前端页面相应要有记住密码勾选框

    <input  name="_spring_security_rember_me" type="checkbox"/>
    
  • 强制使用https:保证每次对指定url请求都会自动重定向为https请求,不管用户访问时是否加入https

    <intercept pattern="/admin/**" access="ROLE_DEMO" requires-channel="https" />
    
  • form-login属性详解

    使用SpEL
    <!--use-expressions="true":声明使用表达式-->
    <http auto-config="true" use-expressions="true"> 
        <!--需要ROLE_ADMIN权限才能访问-->
        <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')">
        <intercept-url pattern="/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
    </http>
    
  • 所支持的SpEL如下
  • 补充一个authenticated,应该和第一个一样
  • isAuthenticated():是否认证过
    jsp标签
  • Spring Security提供jsp标签库,包含三个标签

    • security:accesscontrollist :如果认证用户具有权限列表中的某一个权限,那么这个标签范围的内容将显示
    • security:authentication: 访问当前用户认证对象的属性。一般用户显示当前用户的用户名之类的。具有的用户认证信息有:
      • authorities:一组用于用户所授予的GrantedAuthority对象
      • credentials:核实用户的凭据
      • detail:认证的附加信息(IP地址,会话ID等)
      • principal:用户的主要信息对象
    • security:authorize: 如果当前用户满足特定权限,则显示标签范围的内容

      <!-- 显示用户信息, 并将信息复制给var变量,该变量的使用范围为scope的范围-->
       Hello <security:authentication property="principal.usrname" var="loginId" scope="request">
       <security:authorize access="hasRole('ROLE_ADMIN')">
          如果当前用户有ROLE_ADMIN权限,则显示这部分内容
      </security:authorize>
      
      <!--或者-->
      <security:authorize url="/admin/**">
          如果当前用户有/admin/**对应的权限,则显示这部分内容
      </security:authorize>
      
      认证用户
  • 前面提到很多用户权限的问题,那么如何获取用户的权限呢?最常用的方式是查询数据库中相应用户的权限,前面登录验证的例子使用了内存用户存储库,参考文章也介绍了内存用户存储库的配置。

    <jdbc-user-service id="userService" data-source-ref="dataSource"
    users-by-username="select username,password,true from user where username=?"
    authories-by-username-query="select username,role from user_role where username=?" />  
    <authentication-manager>
        <!--ref:直接注入bean;user-service-ref:先将bean注入一个叫DaoAuthenticationProvider的bean
        的变量userDetailsService中,再将DaoAuthenticationProvider注authentication-provider入-->
        <authentication-provider user-service-ref="userService"/>
    </authentication-manager>
    
    <!--或者-->
    <authentication-manager erase-credentials="false">
        <authentication-provider>
            <!--如果想用传统的md5或sha的话
            <password-encoder hash="sha" />
            -->
            <password-encoder ref="bcryptEncoder" />
            <jdbc-user-service data-source-ref="dataSource"
                  users-by-username-query="select username,password from user where username=?"
                  authorities-by-username-query="select username,role from user_role where username=?"/>
        </authentication-provider>
    </authentication-manager>
    <!-- 配置加密的算法,表明在登录或密码需要加密验证;
    生成的密文有60位,且每次都不同,所以数据库密码字段至少60位 -->
    <beans:bean name="bcryptEncoder"
        class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
    
  • 不写sql语句的话,Spring Security会自动生成sql语句从数据库中查找用户和权限,但一般情况下提供的查询语句并不能和我们的数据库对上,所以我们需要自己写,主要包括以下属性:
    • users-by-username-query:根据用户名查询用户名,密码以及是否可用状态
    • authorities-by-username-query:根据用户名查询用户被用户名和授权的权限。
    • group-authorities-by-username-query:根据用户名查询用户组的权限
      保护方法
  • 提供方法级别的保护,基于Spring AOP.首先需要applicationContext.xml中加入以下配置

    <!--使spring Security保护那些带相关注解的方法-->
    <global-method-security secured-annotations="enabled" />
    
  • 支持4种方法级别安全性的保护方式:

    • 使用@Secured注解方法,这是spring自带的注解方法。@Secured(””)内部的字符串不具有SpEL特性,只能是具体的权限。
    • 使用@JSR-250 @RelosAllowed注解的方法。作用和使用方法与@Secured一样,不同在于它不是spring框架的,所以可以做到和spring框架的解耦。
    • 使用Spring 方法调用前和调用后注解方法。这些方法支持SpEL.
    • 匹配一个或多个明确声明的切点方法。

      @Secured("ROLE_ADMIN")
      public void addUser(User user){
          ...  
      }
      @RolesAllowed("ROLE_ADMIN")
      public void updateUser(User user){
          ...  
      }
      
      //匹配一个或多个明确声明的切点方法
      <global-method-security secured-annotations="enabled" >
          <protect-pointcut access="ROLE_ADMIN" expression="execution(@com.securitytest.service.UserService**.*(String)"
      </global-method-security>
      
  • 处理方法调用前和调用后的Spring注解(可以使用SpEL)

    • @PreAuthorize: 在方法调用前,基于表达式计算结果来限制方法访问
    • @PostAuthorize: 允许方法调用,但是如果表达式结果为fasle则抛出异常
    • @PostFilter :允许方法调用,但必须按表达式过滤方法结果。
    • @PreFilter:允许方法调用,但必须在进入方法前过滤输入值

      @PreAuthorize("hasRole('ROLE_ADMIN')")
      public void addUser(User user){
         //如果具有权限 ROLE_ADMIN 访问该方法
          ....
      }
      
      //returnObject可以获取返回对象user,判断user属性username是否和访问该方法的用户对象的用户名一样。不一样则抛出异常。
      @PostAuthorize("returnObject.user.username==principal.username")
      public User getUser(int userId){
         //允许进入
      ...
          return user;    
      }
      
      //将结果过滤,即选出性别为男的用户
      @PostFilter("returnObject.user.sex=='男' ")
      public List<User> getUserList(){
         //允许进入
      ...
          return user;    
      }
      
      参考文章
  • Spring Security入门
  • SpringMvc整合Spring Security
  • web.xml是最好的
  • SpringMVC+Spring Security实现登录认证的简单功能
    代码实例
  • D:/ideaprojects/thz/thz-parent/thz-manager-web

    Java Configuration方式参见笔记:Spring Security之Java Configuration方式