SpringSecurity权限控制框架(七)

441 阅读15分钟

SpringSecurity权限控制框架

一、权限控制

1.认证和授权的概念

  • 认证:系统提供的用于识别用户身份的功能,通常提供用户名和密码进行登录其实就是在进行认证,认证的目的是让系统知道你是谁。
  • 授权:用户认证成功后,需要为用户授权,其实就是指定当前用户可以操作哪些功能。

解决了哪些问题?

  • 必须登录后台管理系统才能开放功能
  • 对角色分配权限,对权限不完整的用户只开放部分功能

2.权限模块的数据模型

  • 用户表t_user
  • 角色表t_role
  • 权限表t_permission
  • 角色权限关系表t_role_permission

​ RBAC(基于角色的访问控制),就是用户通过角色与权限进行关联。简单来说就是,一个用户有若干个角色,每一个角色拥有若干权限。这样,就能构成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般都是多对多的关系。

​ 我们把基于角色的权限控制叫做RABC。

角色是什么?

​ 可以理解为一定数量的权限的集合,权限的载体。

在应用系统中,权限表现成什么?

​ 对功能模块的操作,对上传文件的删改,菜单的访问,甚至页面上某个按钮,某个图片的可见性控制,都属于权限的范畴。

认证过程

​ 只需要用户表就可以了,在用户登陆时可以查询用户表t_user进行校验,判断用户输入的用户名和密码是否正确。

授权过程

​ 用户必须完成认证之后才可以进行授权,首先可以根据用户查询它对应的角色t_role。再根据角色查询对应的权限t_permission以及资源。

3.表之间的关系

  • 角色和角色是多对多的关系
  • 角色和权限是多对多的关系
  • 权限和菜单是多对多的关系

二、Spring Security

1.简介

Spring Security是 Spring提供的安全认证服务的框架。 使用Spring Security可以帮助我们来简化认证和授权的过程。

官网:spring.io/projects/sp…

中文官网:www.w3cschool.cn/springsecur…

2.坐标

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-web</artifactId>
  <version>5.0.5.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-config</artifactId>
  <version>5.0.5.RELEASE</version>
</dependency>

3.总结

  1. SpringSecurity是Spring家族的一个安全框架,简化我们开发里面的认证和授权。
  2. SpringSecurity内部封装了Filter,只需要在web.xml容器中配置一个过滤器——代理过滤器,真实的过滤器在spring的容器中配置
  3. 常见安全框架
    • Spring 的 SpringSecurity
    • Apache的Shirohttp://shiro.apache.org/

4.示例一

需求

使用Spring Security 进行控制:网站(一些页面)需要登录才能访问(认证)

步骤

  1. 创建Maven工程spring_security_demo导入依赖

     <packaging>war</packaging>
        <properties>
            <spring.version>5.0.5.RELEASE</spring.version>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context-support</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>${spring.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-web</artifactId>
                <version>5.0.5.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-config</artifactId>
                <version>5.0.5.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>servlet-api</artifactId>
                <version>2.5</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.maven</groupId>
                    <artifactId>tomcat7-maven-plugin</artifactId>
                    <configuration>
                        <!-- 指定端口 -->
                        <port>85</port>
                        <!-- 请求路径 -->
                        <path>/</path>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
  2. 配置web.xml(前端控制器,SpringSecurity相关的过滤器)

    • DelegatingFilterProxy用于整合第三方框架(代理过滤器,非真正的过滤器,真正的过滤器要在spring的配置文件)
    • springmvc的核心控制器,在web.xml中主要配置SpringMVC的DispathcerServlet和用于整合第三方框架的DelegatingFilterProxy代理过滤器,真正的过滤器在spring的配置文件,用于整合Spring Security。
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns="http://java.sun.com/xml/ns/javaee"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
             id="WebApp_ID" version="3.0">
    
        <filter>
            <!--
             1:DelegatingFilterProxy用于整合第三方框架(代理过滤器,非真正的过滤器,真正的过滤器需要在spring的配置文件)
              整合Spring Security时过滤器的名称必须为springSecurityFilterChain,
              否则会抛出NoSuchBeanDefinitionException异常
            -->
            <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:springmvc的核心控制器-->
        <servlet>
            <servlet-name>springmvc</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <!-- 指定加载的配置文件 ,通过参数contextConfigLocation加载 -->
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:spring-security.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>springmvc</servlet-name>
            <url-pattern>*.do</url-pattern>
        </servlet-mapping>
    </web-app>
    
  3. 创建spring-security.xml(核心)

    • 定义哪些连接可以放行
    • 定义哪些连接不可以放行,即需要有角色、权限才可以放行
    • 认证管理,定义登录账号名和密码,并授予访问的角色、权限

    在spring-security.xml中主要配置Spring Security的拦截规则和认证管理器。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:security="http://www.springframework.org/schema/security"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                      http://www.springframework.org/schema/beans/spring-beans.xsd
                      http://www.springframework.org/schema/mvc
                      http://www.springframework.org/schema/mvc/spring-mvc.xsd
                      http://www.springframework.org/schema/context
                      http://www.springframework.org/schema/context/spring-context.xsd
                              http://www.springframework.org/schema/security
                              http://www.springframework.org/schema/security/spring-security.xsd">
    
         <!--
           ① 配置哪些链接可以放行(没有认证通过也可以访问的资源)
           security="none":没有权限
           pattern="/login.html":没有任何权限,可以访问login.html
         -->
        <!--<security:http security="none" pattern="/login.html"></security:http>-->
    
        <!--
        ② 定义哪些链接不可以放行(必须通过认证才能访问的资源),及需要有角色,有权限才可以放行访问资源
        <security:http auto-config="true" use-expressions="true">
              auto-config="true":开启自动配置 由springsecurity提供登录页面,提供登录的url地址,退出的url地址
              use-expressions="true":使用表达式的方式控制权限
                 security:intercept-url:定义哪些链接不可以放行,需要当前角色和权限才能放行
                    pattern="/**":要求系统中的所有资源,都必须通过角色和权限才能访问
                    access:指定角色和权限
                       如果使用表达式use-expressions="true"
                           access="hasRole('ROLE_ADMIN'):表示具有ROLE_ADMIN的角色才能访问系统的资源
                       如果不使用表达式use-expressions="false"
                           access="ROLE_ADMIN:表示具有ROLE_ADMIN的角色才能访问系统的资源
        -->
        <security:http auto-config="true" use-expressions="true">
            <security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"></security:intercept-url>
        </security:http>
    
        <!--
          ③ 认证管理:定义登录账号和密码,并授予当前用户访问的角色或权限
            (1):将用户名和密码:当前用户具有的角色,写死到配置文件中(现在:入门)
                    security:user name="admin" :登录名
                    authorities="ROLE_ADMIN"   :角色(ROLE_ADMIN),权限
                    password="admin"          :密码
             (2):用户名和密码,当前用户具有的角色,从数据库中查询(后续)
        -->
        <security:authentication-manager>
            <security:authentication-provider>
                <security:user-service>
                    <security:user name="admin" authorities="ROLE_ADMIN" password="admin"></security:user>
                </security:user-service>
            </security:authentication-provider>
        </security:authentication-manager>
    </beans>
    

    请求 url 地址:http://localhost:85/

    会自动调整到登陆页面(springSecurity自动提供的)

    登录需要输入争取企鹅的用户名和密码(admin/admin),因为Spring security提供了一套安全机制,登录的时候进行了拦截,参考系统源码PasswordEncoderFactories

问题

500:There is no PasswordEncoder mapped for the id "null"

解决方案

需要修改配置文件

<security:user name="admin" authorities="ROLE_ADMIN" password="{noop}admin"></security:user>

输入正确用户名密码。

发现跳转到404错误页面信息是 /favicon.ico未找到,这是Spring Security自动指向的图标。但是这个有没有是无所谓的。说明另一个问题:

此时没有登录成功的页面。

{noop}:表示当前使用的密码为明文。表示当前密码不需要加密PasswordEncoderFactories【这是一个加密算法工厂类,可以从中得到各种加密器对象】。

在webapp文件夹下面,新建index.html,登录后就可以正常访问到index.html了。

注意:

  1. 在web.xml里面配置的权限相关的过滤器,名字不能改(springSecurityFilterChain)

    <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. 刚刚案例中没有指定没密码加密方式,需要在配置密码的时候添加{noop}

    <security:user-service>
       <security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN"/>
    </security:user-service>
    
    

5.示例二

需求进阶

​ 刚刚的案例还是不够贴合真实生产环境,有以下的一些问题:

  1. 项目中我们将所有的资源(所有请求URL)都保护起来了,实际环境下往往有一些资源不需要认证也可以访问,也就是可以匿名访问。【资源放行,免登陆访问】
  2. 登录页面是由框架生成的,而我们的项目往往会使用自己的登录页面。【自定义登录页面】
  3. 直接将用户名和密码配置在了配置文件中,而真实生产环境下的用户名往往保存在数据库中。【使用数据库数据进行登录验证】
  4. 在配置文件中配置的密码使用明文,这非常不安全,而真实生产环境下密码需要进行加密。【密码加密和验证】

步骤

  1. 配置可匿名访问的资源(不需要登录权限和角色,就可以访问的资源)

    • 在项目中创建js、css目录并在两个目录下提供任意一些测试文件

    • 在spring-security.xml文件中配置,指定哪些资源可以匿名访问。

      <!--
        http:用于定义相关权限控制
        指定哪些资源不需要进行权限校验,可以使用通配符
      -->
      <security:http security="none" pattern="/js/**" />
      <security:http security="none" pattern="/css/**" />
      
  2. 使用指定的登录页面(login.html)

    • 在webapp文件夹下面,提供login.html作为项目的登录页面

      <!DOCTYPE html>
      <html>
      <head>
      <meta charset="UTF-8">
          <title>登录</title>
      </head>
      <body>
      <form action="/login.do" method="post">
          username:<input type="text" name="username"><br>
          password:<input type="password" name="password"><br>
          <input type="submit" value="submit">
      </form>
      </body>
      </html>
      
      
    • 修改spring-security.xml文件,指定login.html页面可以匿名访问

    • 修改spring-security.xml文件,加入表单登录信息的配置

    • 修改spring-security.xml文件,关闭csrfFilter过滤器

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:security="http://www.springframework.org/schema/security"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                      http://www.springframework.org/schema/beans/spring-beans.xsd
                      http://www.springframework.org/schema/mvc
                      http://www.springframework.org/schema/mvc/spring-mvc.xsd
                      http://www.springframework.org/schema/context
                      http://www.springframework.org/schema/context/spring-context.xsd
                              http://www.springframework.org/schema/security
                              http://www.springframework.org/schema/security/spring-security.xsd">
         <!--
          ① 配置哪些链接可以放行(没有认证通过也可以访问的资源)
          security="none":没有权限
          pattern="/login.html":没有任何权限,可以访问login.html
        -->
        <!--<security:http security="none" pattern="/login.html"></security:http>-->
        <security:http security="none" pattern="/login.html" />
        <!--
            http:用于定义相关权限控制
            指定哪些资源不需要进行权限校验,可以使用通配符
        -->
        <security:http security="none" pattern="/js/**" />
        <security:http security="none" pattern="/css/**" />
        <!--
                form-login:定义表单登录信息
                login-page="/login.html":表示指定登录页面
                username-parameter="username":使用登录名的名称,默认值是username
                password-parameter="password":使用登录名的密码,默认值是password
                login-processing-url="/login.do":表示登录的url地址
                default-target-url="/index.html":登录成功后的url地址
                authentication-failure-url="/login.html":认证失败后跳转的url地址,失败后指定/login.html
                always-use-default-target="true":登录成功后,始终跳转到default-target-url指定的地址,即登录成功的默认地址
        -->
        <security:http auto-config="true" use-expressions="true">
            <security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"></security:intercept-url>
            <security:form-login login-page="/login.html"
                                 username-parameter="username"
                                 password-parameter="password"
                                 login-processing-url="/login.do"
                                 default-target-url="/index.html"
                                 authentication-failure-url="/login.html"
                                 always-use-default-target="true"/>
            <!--
                csrf:对应CsrfFilter过滤器
                disabled:是否启用CsrfFilter过滤器,如果使用自定义登录页面需要关闭此项,否则登录操作会被禁用(403)
            -->
          <security:csrf disabled="true"></security:csrf>
        </security:http>
    

    注意:

    ​ 此时测试,如果用户名和密码输入正确。抛出异常:

    403-forbidden

    分析原因:

    Spring-security采用盗链机制,其中csrf使用token表示和随机字符,每次访问页面都会随机生成,然后和服务器进行比较,成功可以访问,不成功不能访问。

    解决方案:

    关闭盗链安全请求

    <!--关闭盗链安全请求-->
    <security:csrf disabled="true" />
    
    

    什么是csrf?

    csrf又称为跨域请求伪造,攻击方通过伪造用户请求访问受信任站点。

    解决方法基本上都是增加攻击网站无法获取到的一些表单信息,比如增加图片验证码,可以杜绝csrf攻击,但是除了登录注册之外,其他的地方都不适合放验证码,因为降低了网站易用性。

  3. 从数据库查询用户信息

    • 定义UserService类,实现UserDetailsService接口。

      public class User {
          private String username;
          private String password;
          private String telephone;
          // 生成set get 和 tostring 方法
      }
      

      此处使用Map集合模拟从集合中取出的数据。

      @Component
      public class UserService implements UserDetailsService {
          //模拟数据库中的用户数据
         static Map<String,com.dyy.pojo.User> map =   new HashMap<String,com.dyy.pojo.User>();
      
         static {
             com.dyy.pojo.User user1 =  new com.dyy.pojo.User();
             user1.setUsername("admin");
             user1.setPassword("admin");
             user1.setTelephone("123");
      
             com.dyy.pojo.User user2 =  new com.dyy.pojo.User();
             user2.setUsername("zhangsan");
             user2.setPassword("123");
             user2.setTelephone("321");
      
             map.put(user1.getUsername(),user1);
             map.put(user2.getUsername(),user2);
         }
      
          /**
           * 根据用户名加载用户信息
           * @param username
           * @return
           * @throws UsernameNotFoundException
           */
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              System.out.println("username"+username);
              //模拟根据用户名查询数据库
              com.dyy.pojo.User userInDb = map.get(username);
      
              if (userInDb==null){
                  //根据用户名没有查询到用户,抛出异常,表示登录名输入有误
                  return  null;
              }
              //模拟数据库中的密码,后期需要查询数据库
              String passwordInDb ="{noop}" + userInDb.getPassword();
              //授权,后期需要改为查询数据库动态获得用户拥有的权限和角色
              List<GrantedAuthority> lists = new ArrayList<>();
              lists.add(new SimpleGrantedAuthority("add"));
              lists.add(new SimpleGrantedAuthority("delete"));
              lists.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
      
              //public User(String username, String password, Collection<? extends GrantedAuthority > authorities)
              //返回User,
              //参数一:存放登录名,
              //参数二:存放数据库查询的密码(数据库获取的密码,默认会和页面获取的密码进行比对,成功跳转到成功页面,失败回到登录页面,并抛出异常表示失败)
              //参数三:存放当前用户具有的权限集合
              return new User(username,passwordInDb,lists);//注意:框架提供的User类:org.springframework.security.core.userdetails.User
          }
      }
      
    • 修改spring-security.xml配置(注入UserService)

      <!--
          三:认证管理,定义登录账号名和密码,并授予访问的角色、权限
          authentication-manager:认证管理器,用于处理认证操作
      -->
      <security:authentication-manager>
          <!-- authentication-provider:认证提供者,执行具体的认证逻辑 -->
          <security:authentication-provider user-service-ref="userService">
          </security:authentication-provider>
      </security:authentication-manager>
      <context:component-scan base-package="com.dyy"/>
      

      在spring配置文件中注册UserService,指定其作为认证过程中根据用户名查询用户信息的处理类。当我们进行登录操作时,spring security框架会调用UserService的loadUserByUsername查询用户信息,并根据此方法中提供的数据库中的密码和用户页面输入的表单密码进行比对来实现认证操作。

  4. 对密码进行加密

    常见的密码加密方式有:

    • 3DES、AES、DES:使用对称加密算法,可以通过解密来还原出原始密码

    • MD5、SHA1:使用单项HASH算法,无法通过计算还原出原始密码,但是可以建立彩虹表进行查表破解。

    • MD5可进行加盐加密,保证安全。同样的密码值,盐值不同,加密的结果不同。

    bcrypt:将salt随机混入最终加密后的密码,验证时也无需单独提供之前的salt,从而不需要单独处理salt问题。

    spring security中的BcryptPasswordEncoder方法采用SHA-256+随机盐+密钥对称密码进行加密。SHA系列是Hash算法,不是加密算法,使用解密算法意味着可以解密(这个和编码/解码一样),但是采用的是Hash处理,过程是不可逆的。

    (1)加密:注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。

    (2)密码匹配:用户登录,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash值比较。如果两者相同,说明用户输入的密码正确。

    **这就是为什么处理密码时要使用hash算法,而不使用加密算法。因为这样处理即使数据库泄露,黑客也很难破解密码。**两个相同的密码处理后也可能不同,没法去一一对照。

    加密后格式一般是

    $2a$10$/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa
    

    加密后字符串的长度为固定的60位。其中

    $是分割符,无意义;

    2a是bcrypt加密版本号;

    10是cost的值;

    而后的前22位是salt值;

    再然后的字符串就是密码的密文了。

    ①在spring-security.xml文件中指定密码加密对象

    <!--
        三:认证管理,定义登录账号名和密码,并授予访问的角色、权限
        authentication-manager:认证管理器,用于处理认证操作
    -->
    <security:authentication-manager>
        <!-- authentication-provider:认证提供者,执行具体的认证逻辑  -->
        <security:authentication-provider user-service-ref="userService">
            <!--指定密码加密策略-->
            <security:password-encoder ref="passwordEncoder"></security:password-encoder>
        </security:authentication-provider>
    </security:authentication-manager>
    
    <!--配置密码加密对象-->
    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
    
    

    ②修改UserService实现类,将密码设置为加密后的密文。

  5. 配置多种校验规则(对访问的页面做权限控制)

    ①修改spring-security.xml文件

    前提:<security:http auto-config=“true” use-expressions=“true”>

    <security:http auto-config="true" use-expressions="true">
       <!--<security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"></security:intercept-url>-->
        <!--只要认证通过就可以访问-->
        <security:intercept-url pattern="/index.html"  access="isAuthenticated()" />
        <security:intercept-url pattern="/a.html"  access="isAuthenticated()" />
    
        <!--拥有add权限就可以访问b.html页面-->
        <security:intercept-url pattern="/b.html"  access="hasAuthority('add')" />
    
        <!--拥有ROLE_ADMIN角色就可以访问c.html页面,
            注意:此处虽然写的是ADMIN角色,框架会自动加上前缀ROLE_-->
        <security:intercept-url pattern="/c.html"  access="hasRole('ROLE_ADMIN')" />
    
        <!--拥有ROLE_ADMIN角色就可以访问d.html页面-->
        <security:intercept-url pattern="/d.html"  access="hasRole('ABC')" />
    </security:http>
    
  6. 注解方式权限控制(对访问的Controller类做权限控制)

    Spring Security除了可以在配置文件中配置权限校验规则,还可以使用注解方式控制类中方法的调用。例如:Controller中的某个方法要求必须具有某个权限才可以访问,此时就可以使用Spring Security框架提供的注解方式进行控制。

    ①在spring-security.xml文件中配置组件扫描和mvc的注解驱动,用于扫描Controller

    <context:component-scan base-package="com.dyy"/>
    <mvc:annotation-driven />
    
    

    ②在spring-security.xml文件中开启权限注解支持

    <!--开启注解方式权限控制-->
    <security:global-method-security pre-post-annotations="enabled" />
    

    ③创建Controller类并在Controller的方法上加入注解(@PreAuthorize)进行权限控制

    @RestController
    @RequestMapping("/hello")
    public class HelloController {
    
        @RequestMapping("/add")
        @PreAuthorize("hasAuthority('add')")//表示用户必须拥有add权限才能调用当前方法
        public String add(){
            System.out.println("add...");
            return "success";
        }
    
        @RequestMapping("/update")
        @PreAuthorize("hasRole('ROLE_ADMIN')")//表示用户必须拥有ROLE_ADMIN角色才能调用当前方法
        public String update(){
            System.out.println("update...");
            return "success";
        }
    
        @RequestMapping("/delete")
        @PreAuthorize("hasRole('ABC')")//表示用户必须拥有ABC角色才能调用当前方法
        public String delete(){
            System.out.println("delete...");
            return "success";
        }
    }
    
    
  7. 退出登录

    ①index.html定义退出登录连接

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        登录成功!<br>
        <a href="/logout.do">退出登录</a>
    </body>
    </html>
    
    

    ②在spring-security.xml

    <!--
      logout:退出登录
      logout-url:退出登录操作对应的请求路径
      logout-success-url:退出登录后的跳转页面
      invalidate-session="true" 默认为true,用户在退出后Http session失效
    -->
    <security:logout logout-url="/logout.do" logout-success-url="/login.html" invalidate-session="true"/>
    
    

    通过上面的配置可以发现,如果用户要退出登录,只需要请求/logout.do这个URL地址就可以,同时会将当前session失效,最后页面会跳转到login.html页面

三、集成权限框架

集成步骤

1.导入依赖

spring和spring security

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-web</artifactId>
  <version>${spring.security.version}</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-config</artifactId>
  <version>${spring.security.version}</version>
</dependency>

2.添加过滤器

在web.xml中配置用于整合Spring Security框架的过滤器DelegatingFilterProxy

<filter>
    <!--
      DelegatingFilterProxy用于整合第三方框架(代理过滤器,非真正的过滤器,真正的过滤器需要在spring的配置文件)
      整合Spring Security时过滤器的名称必须为springSecurityFilterChain,
      否则会抛出NoSuchBeanDefinitionException异常
    -->
    <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>

3.准备sql语句

  1. 使用登录名查询用户星系
  2. 传递用户id查询角色集合
  3. 传递角色id查询权限集合

4.准备Service、Dao接口、Mapper映射文件

/**
 * 用户服务接口
 */
public interface UserService {
    User findUserByUsername(String username);
}

@Service(interfaceClass = UserService.class)
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    public User findUserByUsername(String username) {
        User user = userDao.findUserByUsername(username);
        return user;
    }
}

/**
 * 持久层Dao接口
 */
@Repository
public interface UserDao {
    User findUserByUsername(String username);
}


/**
 * 持久层Dao接口
 */
@Repository
public interface RoleDao {
    Set<Role> findRolesByUserId(Integer userId);
}

/**
 * 持久层Dao接口
 */
@Repository
public interface PermissionDao {
    Set<Permission> findPermissionsByRoleId(Integer roleId);
}

5.实现UserDetailsService接口

@Component
public class SpringSecurityUserService implements UserDetailsService {

    @Reference //注意:此处要通过dubbo远程调用用户服务
    private UserService userService;

    //根据用户名查询用户信息
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //远程调用用户服务,根据用户名查询用户信息
        com.dyy.pojo.User user = userService.findUserByUsername(username);
        if(user == null){
            //用户名不存在,抛出异常UsernameNotFoundException 
            return null;
        }
        List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
        Set<Role> roles = user.getRoles();
        for(Role role : roles){
            Set<Permission> permissions = role.getPermissions();
            for(Permission permission : permissions){
                //授权
                list.add(new SimpleGrantedAuthority(permission.getKeyword()));
            }
        }
        /**
         * User()
         * 1:指定用户名
         * 2:指定密码(SpringSecurity会自动对密码进行校验)
         * 3:传递授予的角色和权限
         */
        UserDetails userDetails = new User(username,user.getPassword(),list);
        return userDetails;
    }
}

6.springmvc.xml

由于UserDetailsService不在之前的包下,所以需要修改批量扫描的包.因为在SpringSecurityUserService的loadUserByUsername方法中需要通过dubbo远程调用名称为UserService的服务。

<!--批量扫描-->
<dubbo:annotation package="com.dyy" />

7.创建spring-security.xml

  1. 定义哪些连接可以放行
  2. 定义哪些连接不可以放行,即需要有角色、权限才可以放行。
  3. 认证管理,定义登录账号名和密码,并授予访问的角色,权限。
  4. 设置在页面上可以通过iframe【引用其他页面】访问受保护的页面,默认为不允许访问,需要添加security:frame-options policy="SAMEORIGIN"
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                  http://www.springframework.org/schema/beans/spring-beans.xsd
                  http://www.springframework.org/schema/security
                  http://www.springframework.org/schema/security/spring-security.xsd">
    <!--一:定义哪些链接可以放行-->
    <!--
      http:用于定义相关权限控制
      指定哪些资源不需要进行权限校验,可以使用通配符
    -->
    <security:http security="none" pattern="/js/**" />
    <security:http security="none" pattern="/css/**" />
    <security:http security="none" pattern="/img/**" />
    <security:http security="none" pattern="/plugins/**" />
    <security:http security="none" pattern="/login.html" />
    <!--开启注解方式权限控制-->
    <security:global-method-security pre-post-annotations="enabled" />
    <!--
        二:定义哪些链接不可以放行,即需要有角色、权限才可以放行
        http:用于定义相关权限控制
        auto-config:是否自动配置
                        设置为true时框架会提供默认的一些配置,例如提供默认的登录页面、登出处理等
                        设置为false时需要显示提供登录表单配置,否则会报错
        use-expressions:用于指定intercept-url中的access属性是否使用表达式
    -->
    <security:http auto-config="true" use-expressions="true">

	<security:headers>
    <!--设置在页面可以通过iframe访问受保护的页面,默认为不允许访问-->
    <security:frame-options policy="SAMEORIGIN"></security:frame-options>
</security:headers>

        <!--只要认证通过就可以访问-->
        <!--
            intercept-url:定义一个拦截规则
            pattern:对哪些url进行权限控制
            access:在请求对应的URL时需要什么权限,默认配置时它应该是一个以逗号分隔的角色列表,
              请求的用户只需拥有其中的一个角色就能成功访问对应的URL
            isAuthenticated():需要经过认证后才能访问(不是匿名用户)
        -->
        <security:intercept-url pattern="/pages/**"  access="isAuthenticated()" />

        <!--
          form-login:定义表单登录信息
        -->
        <security:form-login login-page="/login.html"
                             username-parameter="username"
                             password-parameter="password"
                             login-processing-url="/login.do"
                             default-target-url="/pages/main.html"
                             authentication-failure-url="/login.html"
                             always-use-default-target="true"
        />

        <!--
          csrf:对应CsrfFilter过滤器
          disabled:是否启用CsrfFilter过滤器,如果使用自定义登录页面需要关闭此项,否则登录操作会被禁用(403)
        -->
        <security:csrf disabled="true"></security:csrf>

        <!--
          logout:退出登录
          logout-url:退出登录操作对应的请求路径
          logout-success-url:退出登录后的跳转页面
        -->
        <security:logout logout-url="/logout.do" logout-success-url="/login.html" invalidate-session="true"/>
    </security:http>


    <!--配置密码加密对象-->
    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />


    <!--
        三:认证管理,定义登录账号名和密码,并授予访问的角色、权限
        authentication-manager:认证管理器,用于处理认证操作
    -->
    <security:authentication-manager>
        <!--
            authentication-provider:认证提供者,执行具体的认证逻辑
        -->
        <security:authentication-provider user-service-ref="springSecurityUserService">
            <!--指定密码加密策略-->
            <security:password-encoder ref="passwordEncoder"></security:password-encoder>
        </security:authentication-provider>
    </security:authentication-manager>

</beans>

X-Frame-Options响应头:

X-Frame-Options HTTP响应头是用来给浏览器指示允许一个页面可否在<frame></frame>或者<object>中展现的标记。网站可以使用此功能,来确保自己网站的内容没有被嵌入到别人的网站中去,也就从而避免了点击劫持(clickjacking)的攻击。

属性值:

  1. DENY

    表示该页面不允许在frame中展示,即使是在相同域名的页面中嵌套也不允许。

  2. SAMEORIGIN

    表示该页面可以在相同域名页面的frame中展示。

  3. ALLOW-FROM uri

    表示该页面可以在指定来源的frame中展示。

8.springmvc.xml引入spring-security.xml文件

<import resource="classpath:spring-security.xml"></import>

9.给Controller的方法上加上权限控制注解

@RestController
@RequestMapping("/travelItem")
public class TravelItemController {

    @Reference
    private TravelItemService travelItemService;

    @RequestMapping("/findAll")
    public Result findAll(){
        List<TravelItem> lists =  travelItemService.findAll();
        return new Result(true,MessageConstant.QUERY_TRAVELITEM_SUCCESS,lists);
    }


    @RequestMapping("/edit")
    @PreAuthorize("hasAuthority('TRAVELITEM_EDIT')")//权限校验
    public Result edit(@RequestBody TravelItem travelItem){
        travelItemService.edit(travelItem);
        return new Result(true,MessageConstant.EDIT_TRAVELITEM_SUCCESS);
    }

    @RequestMapping("/findById")
    public Result findById(Integer id){
        TravelItem travelItem = travelItemService.findById(id);
        return new Result(true,MessageConstant.QUERY_TRAVELITEM_SUCCESS,travelItem);
    }

    @RequestMapping("/delete")
    @PreAuthorize("hasAuthority('TRAVELITEM_DELETE')")//权限校验,使用TRAVELITEM_DELETE123测试
    public Result delete(Integer id){
        try {
            travelItemService.delete(id);
            return new Result(true,MessageConstant.DELETE_TRAVELITEM_SUCCESS);
        }catch (RuntimeException e){
            return new Result(false,MessageConstant.DELETE_TRAVELITEM_FAIL);
        }catch (Exception e) {
            e.printStackTrace();
        }
        return new Result(false,MessageConstant.DELETE_TRAVELITEM_FAIL);
    }

    @RequestMapping("/findPage")
    @PreAuthorize("hasAuthority('TRAVELITEM_QUERY')")//权限校验
    public PageResult findPage(@RequestBody QueryPageBean queryPageBean){
        PageResult pageResult =  travelItemService.findPage(queryPageBean.getCurrentPage(),
                queryPageBean.getPageSize(),queryPageBean.getQueryString());
        return pageResult;
    }


    //@RequestBody:表示对象和json数据进行互转
    @RequestMapping("/add")
    @PreAuthorize("hasAuthority('TRAVELITEM_ADD')")//权限校验
    public Result add(@RequestBody TravelItem travelItem){
        try {
            // 增加,更新,删除 ,就不需要往data里面添加数据
            // 查询:list 对象
            travelItemService.add(travelItem);
            Result result = new Result(true, MessageConstant.ADD_TRAVELITEM_SUCCESS);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new Result(false, MessageConstant.ADD_TRAVELITEM_SUCCESS);
    }

}

10.添加权限提示信息

  1. <security:http>标签中增加<security:access-denied-handler>

    <!--自定义异常处理-->
    <security:access-denied-handler ref="customAccessDeniedHandler"/>
    <!--引用customAccessDeniedHandler类来处理异常,这个类是我们自己定义的-->
    
  2. 增加自定义处理类【继承AccessDeniedHandler接口】

    /**
     * 无权访问处理类
     */
    @Component
    public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    
        @Override
        public void handle(HttpServletRequest request,HttpServletResponse response,
                           AccessDeniedException accessDeniedException) throws IOException,ServletException {
            if (isAjaxRequest(request)) {// AJAX请求,使用response发送403
                Result result = new Result(false, "无权访问","403");
                String json = JSON.toJSONString(result);
                response.getWriter().print(json);
            } else{// 同步请求处理
                request.getRequestDispatcher("/pages/error/403.html").forward(request,response);
            }
        }
    
        /**
         * 判断是否为ajax请求
         */
        public static boolean isAjaxRequest(HttpServletRequest request) {
            if (request.getHeader("accept").indexOf("application/json") > -1
                    || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").equalsIgnoreCase("XMLHttpRequest"))) {
                return true;
            }
            return false;
        }
    }
    

11.指定自定义的login.html作为默认登录页面

放行login.html,定义login.html作为登录页面login-page="/login.html"。

<security:http security="none" pattern="/login.html" />
<!--
          form-login:定义表单登录信息
        -->
        <security:form-login login-page="/login.html"
                             username-parameter="username"
                             password-parameter="password"
                             login-processing-url="/login.do"
                             default-target-url="/pages/main.html"
                             authentication-failure-url="/login.html"
                             always-use-default-target="true"
        />

12.登录后显示用户名

登录成功跳转的页面,需要引入vue、axios,支持json的发送接收

<script src="../js/axios-0.18.0.js"></script>

定义username属性

使用钩子函数,调用ajax

<script>
    new Vue({
        el: '#app',
        data:{
            menuList:[],
            username:null  // 显示用户名
        },
	    //发送请求获取当前登录用户的用户名
        created:function () {           
            //发送请求获取当前登录用户的用户名
            // 返回Result(flag,message,data),data放置User对象
            axios.get("/user/getUsername.do").then((response)=>{
                if(response.data.flag){
                    this.username = response.data.data.username;
                }
            }).catch((error)=>{
                this.$message.error("获取用户名失败");
            })
        }
    });
    $(function() {
        var wd = 200;
        $(".el-main").css('width', $('body').width() - wd + 'px');
    });
</script>

修改登陆成功跳转的页面index.html

<div class="avatar-wrapper">
    <img src="../img/user2-160x160.jpg" class="user-avatar">
    <!--显示用户名-->
    {{username}}
</div>

创建UserController并提供getUsername方法,Spring Security使用了一个Authentication对象来描述当前用户的相关信息。这个Authentication对象不需要我们自己去创建,在与系统交互的过程中,Spring Security会自动为我们创建相应的Authentication对象,然后赋值给当前的SecurityContext。通过**Authentication.getPrincipal()**可以获取到代表当前用户的信息。

package com.dyy.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.dyy.constant.MessageConstant;
import com.dyy.entity.PageResult;
import com.dyy.entity.QueryPageBean;
import com.dyy.entity.Result;
import com.dyy.service.UserService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {
    @Reference
    private UserService userService;

    //获取当前登录用户的用户名
    @RequestMapping("/getUsername")
    public Result getUsername()throws Exception{
        try{
            User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            return new Result(true, MessageConstant.GET_USERNAME_SUCCESS,user);
        }catch (Exception e){
            return new Result(false, MessageConstant.GET_USERNAME_FAIL);
        }
    }
}

13.用户退出

在index.html页面上加入退出登录的超链接

<el-dropdown-item divided>
        <span style="display:block;"><a href="/logout.do">退出</a></span>
</el-dropdown-item>

在在spring-security.xml文件中配置

<!--
  logout:退出登录
  logout-url:退出登录操作对应的请求路径
  logout-success-url:退出登录后的跳转页面
  invalidate-session="true" 销毁session
-->
<security:logout logout-url="/logout.do" logout-success-url="/login.html" invalidate-session="true"/>