SpringSecurity安全框架(配置类版)

论坛 期权论坛 脚本     
已经匿名di用户   2022-3-21 23:58   2052   0

1. 前言

1.1 SpringSecurity 框架用法简介

用户登录系统时我们协助 SpringSecurity 把用户对应的角色、权限组装好,同时把各个 资源所要求的权限信息设定好,剩下的“登录验证”、“权限验证”等等工作都交给 SpringSecurity

1.2 权限管理过程中的相关概念

1.2.1 主体

英文单词:principal

使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统 谁就是主体

1.2.2 认证

英文单词:authentication

权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明 自己是谁。 笼统的认为就是以前所做的登录操作。

1.2.3 授权

英文单词:authorization

将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的 能力。所以简单来说,授权就是给用户分配权限。

1.3 security的特点

Spring 技术栈的组成部分,通过提供完整可扩展的认证和授权支持保护你的应用程序。官网地址:

https://spring.io/projects/spring-security

SpringSecurity 特点:

1)和 Spring 无缝整合。

2) 全面的权限控制。

3)专门为 Web 开发而设计。

4)旧版本不能脱离 Web 环境使用。

5)新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独 引入核心模块就可以脱离 Web 环境。

6)重量级

2. 环境准备

2.1 新建工程

2.1.1 引入SpringMVC依赖

<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>com.bjc</groupId>
 <artifactId>springsecuryti-web</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>war</packaging>

 <dependencies>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>4.3.20.RELEASE</version>
  </dependency> 
  <!-- 引入 Servlet 容器中相关依赖 -->
  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>servlet-api</artifactId>
   <version>2.5</version>
   <scope>provided</scope>
  </dependency> 
  <!-- JSP 页面使用的依赖 -->
  <dependency>
   <groupId>javax.servlet.jsp</groupId>
   <artifactId>jsp-api</artifactId>
   <version>2.1.3-b06</version>
   <scope>provided</scope>
  </dependency>

 </dependencies>

</project>

2.1.2 MVC的xml配置

<?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"
 xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

 <context:component-scan
  base-package="com.bjc.security"></context:component-scan>

 <bean
  class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="prefix" value="/WEB-INF/views/"></property>
  <property name="suffix" value=".jsp"></property>
 </bean>

 <mvc:annotation-driven></mvc:annotation-driven>
 <mvc:default-servlet-handler />

</beans>

2.1.3 web.xml

<?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_2_5.xsd"
 version="2.5">

 <servlet>
  <servlet-name>springDispatcherServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
   <param-name>contextConfigLocation</param-name>
   <param-value>classpath:spring-mvc.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
 </servlet> 
 <!-- Map all requests to the DispatcherServlet for handling -->
 <servlet-mapping>
  <servlet-name>springDispatcherServlet</servlet-name>
  <url-pattern>/</url-pattern>
 </servlet-mapping>

</web-app>

然后引入静态资源等文件。访问,如图:

2.2 引入security框架

2.2.1 引入security依赖

<!-- 添加security依赖 -->
<!-- SpringSecurity 对 Web 应用进行权限管理 -->
<dependency>
 <groupId>org.springframework.security</groupId>
 <artifactId>spring-security-web</artifactId>
 <version>4.2.10.RELEASE</version>
</dependency> 
<!-- SpringSecurity 配置 -->
<dependency>
 <groupId>org.springframework.security</groupId>
 <artifactId>spring-security-config</artifactId>
 <version>4.2.10.RELEASE</version>
</dependency> 
<!-- SpringSecurity 标签库 -->
<dependency>
 <groupId>org.springframework.security</groupId>
 <artifactId>spring-security-taglibs</artifactId>
 <version>4.2.10.RELEASE</version>
</dependency>

2.2.2 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.2.3 security配置类

package com.bjc.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity  // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
 
}

然后启动项目,访问,如图:

发现,需要输入用户名和密码才能进入首页了,这说明security已经生效了。

3. 放行首页和静态资源

我们只需要在配置类中重写父类的configure方法即可,例如:

@Configuration
@EnableWebSecurity  // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{

 @Override
 protected void configure(HttpSecurity security) throws Exception {
  security
   .authorizeRequests()   // 对请求进行授权
   .antMatchers("/index.jsp")  // 针对 /index.jsp路径进行授权
   .permitAll()     // 可以无条件访问
   .antMatchers("/layui/**")  // 针对 /layui目录下的静态资源进行授权
   .permitAll()     // 设置其可以无条件访问
   .and()
   .authorizeRequests()   // 对请求进行授权
   .anyRequest()     // 任意的请求
   .authenticated()    // 都需要登录之后才能访问
   ;
 }
 
}

再次访问系统,如图:

可以正常访问了。

4. 指定登录页面

4.1 无权限跳转到自带的登录页

使用formLogin

访问无权限的野蛮,如图:

4.2 无权限跳转到指定的登录页

4.2.1 loginPage

通过loginPage方法指定登录页面(如果没有指定会访问security自带的登录页面),其可以修改security默认的登录请求地址

指定登录页前后 SpringSecurity 登录地址变化:

指定前
/login GET - the login form
/login POST - process the credentials and if valid authenticate the user
/login?error GET - redirect here for failed authentication attempts
/login?logout GET - redirect here after successfully logging out
指定后
/index.jsp GET - the login form
/index.jsp POST - process the credentials and if valid authenticate the user
/index.jsp?error GET - redirect here for failed authentication attempts
/index.jsp?logout GET - redirect here after successfully logging out

例如:

4.2.2 loginProcessingUrl

通过调用 loginProcessingUrl()方法指定登录地址。如果指定了loginProcessingUrl,那么其值就会覆盖loginPage方法中设置的默认值(index.jsp POST)

例如:

4.3 设置登录用户名和密码

实现思路,如图:

通过上面的分析,我们知道,我们要实现登录,我们需要form表单的用户名和密码的name属性是一个约定好的值。如果是采用默认的,就是

SpringSecurity 默认账号的请求参数名:username 
SpringSecurity 默认密码的请求参数名:password

如果需要定制的话,我们需要配置一下。

4.3.1 定制登录用户名密码

1) 我们可以在配置类中指定参数,并指定登录成功跳转的路径例如:

注意:loginProcessingUrl后面的permitAll可以省略不要。

2)在jsp中,指定登录用户名和密码的参数名,如图:

注意:标签

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

需要加上,用于跨域验证的。在配置版中有过csrf的介绍

3)配置授权信息

在配置类中,重写另一个configure方法,并设置用户名和密码以及对应的角色和权限,例如:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth
  .inMemoryAuthentication()  // 内存授权
  .withUser("tom")    // 设置账号
  .password("123456")    // 设置密码
  .roles("ADMIN")     // 设置角色
  .and()
  .withUser("marry")    // 设置另一个用户
  .password("123456")    // 密码
  .authorities("SAVE","EDIT")  // 设置权限
  ;
}

使用账号tom/123456访问,可以成功的进入系统,如图:

目前为止完整的配置类如下:

package com.bjc.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity  // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
 
 // 授权
 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth
   .inMemoryAuthentication()  // 内存授权
   .withUser("tom")    // 设置账号
   .password("123456")    // 设置密码
   .roles("ADMIN")     // 设置角色
   .and()
   .withUser("marry")    // 设置另一个用户
   .password("123456")    // 密码
   .authorities("SAVE","EDIT")  // 设置权限
   ;
 }

 // 认证
 @Override
 protected void configure(HttpSecurity security) throws Exception {
  security
   .authorizeRequests()   // 对请求进行授权
   .antMatchers("/index.jsp")  // 针对 /index.jsp路径进行授权
   .permitAll()     // 可以无条件访问
   .antMatchers("/layui/**")  // 针对 /layui目录下的静态资源进行授权
   .permitAll()     // 设置其可以无条件访问
   .and()
   .authorizeRequests()   // 对请求进行授权
   .anyRequest()     // 任意的请求
   .authenticated()    // 都需要登录之后才能访问
   .and()
   .formLogin()     // 无权限跳转到自带的登录页
   /**
      *  关于loginPage方法的特殊说明:
    *  指定登录页的同时会影响到:提交表单的地址、退出登录地址、登录失败的地址
      *        如果指定loginPage的值为/index.jsp,那么,
    *  1)去登录页:/index.jsp  Get
    *  2)提交登录表单:/index.jsp  POST
    *  3)登录失败:/index.jsp?error
    *  4)退出登录:/index.jsp?logout
    * */
   .loginPage("/index.jsp")  // 指定登录页面(如果没有指定会访问security自带的登录页面)
   /*
    * loginProcessingUrl的说明:
    *   如果指定了loginProcessingUrl,那么其值就会覆盖loginPage方法中设置的默认值(index.jsp POST)
    * */
   .loginProcessingUrl("/do/login.html")   // 指定提交登录表单的地址
   .permitAll()       // 允许登录地址访问(可以不要)
   .usernameParameter("loginAcct")   // 指定登录账号的请求参数名
   .passwordParameter("loginPwd")   // 指定登录密码号的请求参数名
   .defaultSuccessUrl("/main.html")  // 设置登录成功去到的页面
   ;
 }
 
}

4.4 退出登录

4.4.1 禁用csrf的退出登录

csrf是security默认开启的,我们需要禁用掉它。如果 CSRF 功能没有禁用,那么退出请求必须是 POST 方式。如果禁用了 CSRF 功能则任何请求方式都可以。所以,我们可以直接使用a标签并指定退出url即可实现退出功能了。

我们可以使用logout()方法开启注销功能,使用logoutUrl()方法来自定义注销功能的 URL 地址。

1)配置

2)页面

<a id="logoutAnchor" href="${pageContext.request.contextPath }/do/logout.html">退出</a>

4.4.2 启用csrf功能退出登录

如果 CSRF 功能没有禁用,那么退出请求必须是 POST 方式。

1)配置

2)form表单

<li class="layui-nav-item">
 <form id="logoutForm" action="${pageContext.request.contextPath }/do/logout.html" method="post">
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 </form>
 <a id="logoutAnchor" href="">退出</a>
 <script type="text/javascript">
  window.onload = function() {
   // 点击a标签,提交表单
   document.getElementById("logoutAnchor").onclick = function() {
    
    document.getElementById("logoutForm").submit();
    
    // 阻止a标签的默认执行动作
    return false;
    
   };
   
  };
 </script>
</li>

同时,我们可以自定义退出登录的处理器

1)addLogoutHandler()方法:添加退出处理器

2)logoutSuccessHandler()方法:退出成功处理器

5. 基于角色或权限访问控制

我们可以给特定资源指定特定的权限或者角色才能访问。例如:

5.1 指定特定的资源需要哪些角色或者权限

注意:要先设置小范围的授权在设置大范围的,即anyRequest()放在后面,如上图。

5.2 指定用户具备的权限或者角色

所以,用户ton可以访问level1目录的内容,marry可以访问level2目录下的内容

如图:

1)tom用户

访问level1下的内容

访问level2下的内容

2)marry用户

访问lever1目录下的内容

访问level2目录下的内容

注意:SpringSecurity 会在角色字符串前面加“ROLE_”前缀,查看security源码,如图:

之所以要强调这个事情,是因为将来从数据库查询得到的用户信息、角色信息、权 限信息需要我们自己手动组装。手动组装时需要我们自己给角色字符串前面加“ROLE_” 前缀。

5.3 自定义403页面

当我们访问的资源没有权限的时候,security默认给我们跳转到一个403的错误页面,这个体验极度不好,所以,我们需要自定义该页面。

5.3.1 访问被拒绝之后直接跳转到页面

controller

@RequestMapping("/to/no/auth/page.html")
public String toNoAuthPage() {
 return "no_auth";
}

效果:

5.3.2 访问被拒绝后走处理器

该方式可以带一些数据到页面,当然,上一种也可以,只是带的数据是通用的数据,该方式可以根据不同的异常携带不同的数据,我们可以通过accessDeniedHandler方法来定制。

例如:

访问没权限的资源,如:

6. 记住我功能(不重要)

6.1 内存版本

HttpSecurity 对象调用 rememberMe()方法,登录表单携带名为 remember-me 的请求参数即可。例如:

页面

这时候,在登录页,会有一个记住我的选择框,勾选之后,发起请求,就会在浏览器写入一个名为remember-me的cookie,再次在登录的时候,就根据这个 Cookie 的 value 在服务器端找到以前登录的 User,而且这个 Cookie 被设置为存储 2 个星期。

当我们关闭浏览器,再次访问的时候,就可以直接进入系统了,不需要在输入用户名和密码。

6.2 数据库版(不重要)

为了让服务器重启也不影响记住登录状态,将用户登录状态信息存入数据库

6.2.1 引入数据库相关依赖

<!-- 引入数据库相关依赖 -->
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid</artifactId>
 <version>1.1.12</version>
</dependency> 
<!-- mysql 驱动 --> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.47</version>
</dependency>
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-orm</artifactId>
 <version>4.3.20.RELEASE</version>
</dependency>

6.2.2 配置数据源

<!-- 配置数据源 -->
<bean id="dataSource"
 class="com.alibaba.druid.pool.DruidDataSource">
 <property name="username" value="root"></property>
 <property name="password" value="root"></property>
 <property name="url" value="jdbc:mysql://localhost:3306/security?useSSL=false"></property>
 <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean> 
<!-- jdbcTemplate -->
<bean id="jdbcTemplate"
 class="org.springframework.jdbc.core.JdbcTemplate">
 <property name="dataSource" ref="dataSource"></property>
</bean>

6.2.3 创建数据库

CREATE DATABASE `security` CHARACTER SET utf8;

CREATE TABLE persistent_logins (
  username VARCHAR (64) NOT NULL,
  series VARCHAR (64) PRIMARY KEY,
  token VARCHAR (64) NOT NULL,
  last_used TIMESTAMP NOT NULL
) ;

6.2.4 配置类

1)在 WebAppSecurityConfig 类中注入数据源

@Autowired
private DataSource dataSource;

2)启用令牌仓库功能

repository是JdbcTokenRepositoryImpl对象

JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl(); 
repository.setDataSource(dataSource);

然后,重启服务器,登录成功,查看数据库,如图:

数据已经正确存入数据库,再次重启服务器,访问,一样可以不用登录就可以进入系统。

注意:表来自于JdbcTokenRepositoryImpl类,如图:

既然该类已经有了建表语句,为什么我们还需要手动建表了,我们看源码,如图:

有一个init方法,可以自动创建表,但是,问题来了,该方法用protected修饰的,该类是框架提供的,我们只能继承该类,才能调用到该方法,但是,这样做比较不划算,所以,我们还不如自己创建一个表算了。

完整的配置类:

package com.bjc.security.config;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

@Configuration
@EnableWebSecurity  // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
 
 @Autowired
 private DataSource dataSource;
 
 // 授权  确定用户有什么权限和角色
 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth
   .inMemoryAuthentication()  // 内存授权
   .withUser("tom")    // 设置账号
   .password("123456")    // 设置密码
   .roles("ADMIN","门徒")   // 设置角色
   .and()
   .withUser("marry")    // 设置另一个用户
   .password("123456")    // 密码
   .authorities("SAVE","UPDATE") // 设置权限
   ;
 }
 
 // 认证  确定资源需要什么权限和角色才能访问
 @Override
 protected void configure(HttpSecurity security) throws Exception {
  
  JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl(); 
  repository.setDataSource(dataSource);
  
  security
   .authorizeRequests()   // 对请求进行授权
   .antMatchers("/index.jsp")  // 针对 /index.jsp路径进行授权
   .permitAll()     // 可以无条件访问
   .antMatchers("/layui/**")  // 针对 /layui目录下的静态资源进行授权
   .permitAll()     // 设置其可以无条件访问
   // .and()
   // .authorizeRequests()   // 对请求进行授权
   .antMatchers("/level1/**")  // 访问level1下的资源
   .hasRole("门徒")     // 需要角色是门徒的才能访问
   .antMatchers("/level2/**")  // 访问level2下的资源
   .hasAuthority("UPDATE")   // 需要用户具备UPDATE权限才能访问
   .anyRequest()     // 其他未设置的全部请求
   .authenticated()    // 都需要登录之后才能访问
   .and()
   .formLogin()     // 无权限跳转到自带的登录页
   /**
      *  关于loginPage方法的特殊说明:
    *  指定登录页的同时会影响到:提交表单的地址、退出登录地址、登录失败的地址
      *        如果指定loginPage的值为/index.jsp,那么,
    *  1)去登录页:/index.jsp  Get
    *  2)提交登录表单:/index.jsp  POST
    *  3)登录失败:/index.jsp?error
    *  4)退出登录:/index.jsp?logout
    * */
   .loginPage("/index.jsp")  // 指定登录页面(如果没有指定会访问security自带的登录页面)
   /*
    * loginProcessingUrl的说明:
    *   如果指定了loginProcessingUrl,那么其值就会覆盖loginPage方法中设置的默认值(index.jsp POST)
    * */
   .loginProcessingUrl("/do/login.html")   // 指定提交登录表单的地址
   .permitAll()       // 允许登录地址访问(可以不要)
   .usernameParameter("loginAcct")   // 指定登录账号的请求参数名
   .passwordParameter("loginPwd")   // 指定登录密码号的请求参数名
   .defaultSuccessUrl("/main.html")  // 设置登录成功去到的页面
   .and()
   //.csrf()
   //.disable()        // 禁用csrf功能
   .logout()        // 开启退出功能
   .logoutUrl("/do/logout.html")   // 指定退出url
   .logoutSuccessUrl("/index.jsp")   // 指定退出成功之后去到的url
   .and()
   .exceptionHandling()      // 指定异常处理器
   // .accessDeniedPage("/to/no/auth/page.html") // 访问被拒绝的时候,前往的页面
   .accessDeniedHandler(new AccessDeniedHandler() {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException, ServletException {
     // 携带数据到页面,页面用${message}接收数据
     request.setAttribute("message", "抱歉,您没有权限访问该页面,请找管理员配置合适的权限在访问。。。");
     // 转发
     request.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(request, response);
    }
   })
   .and()
   .rememberMe()  // 记住我
   .tokenRepository(repository);
   ;
 }
 
}

7. 数据库登录

前面我们的登录是采用的内存登录方式,适用于系统指定用户且用户量较少的系统适用,现在我们需要查询数据库,适用数据库中的用户来进行登录。

7.1 默认实现(了解)

SpringSecurity 的默认实现已经将 SQL 语句硬编码在了 JdbcDaoImpl 类中,所以,默认方法显得比较刻板,源码如下:

可以知道,默认实现表结构等都是规定好的,相当不方便。这种 情况下,我们有下面三种选择

1)按照 JdbcDaoImpl 类中 SQL 语句设计表结构。

2)修改 JdbcDaoImpl 类的源码。

3)不使用 jdbcAuthentication()。

auth.jdbcAuthentication().usersByUsernameQuery("tom")

7.2 自定义数据库查询方式

自定义数据库查询方式也很简单,只需要执行auth.userDetailsService(userDetailsService);即可,其中userDetailsService需要自定义实现UserDetailsService接口的类并自动装配。

7.2.1 自定义userDetailsService

package com.bjc.security.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import com.bjc.security.entity.Admin;

@Component
public class MyUserDetailServiceImpl implements UserDetailsService{
 @Autowired
 private JdbcTemplate jdbcTemplate;
 
 // 总目标:根据表单提交的用户名查询User对象,并装配角色、权限等信息
 // username  表单提交的用户名
 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  // 1.从数据库查询Admin对象
  String sql = "SELECT id,loginacct,userpswd,username,email FROM t_admin WHERE loginacct=?";
  
  List<Admin> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Admin.class), username);
  
  Admin admin = list.get(0);
  
  // 2.给Admin设置角色权限信息
  List<GrantedAuthority> authorities = new ArrayList<>();
  
  // 角色前面需要添加ROLE_  这里应该查询对应的角色表的
  authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
  // 权限前面不需要添加前缀
  authorities.add(new SimpleGrantedAuthority("UPDATE"));
  
  // 3.把admin对象和authorities封装到UserDetails中
  
  String userpswd = admin.getUserpswd();
  
  // User是UserDetails的实现类
  return new User(username, userpswd, authorities);
 }

}

7.2.2 设置

访问,使用数据库中的用户名和密码,可以成功访问。

8. 密码加密

加密我们需要通过passwordEncoder方法指定加密算法。

8.1 普通MD5加密

8.1.1 加密算法类

package com.bjc.security.config;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 *加密类
 *
 */
@Component
public class MyPasswordEncoder implements PasswordEncoder {

 /**
  *   加密方法:采用MD5加密
  */
 @Override
 public String encode(CharSequence rawPassword) {
  return generatePwd(rawPassword);
 }

 /**
  *   校验方法
  *   1. rawPassword :明文
  *   2. encodedPassword:数据库查询出来的密文
  */
 @Override
 public boolean matches(CharSequence rawPassword, String encodedPassword) {
  return encodedPassword.equals(generatePwd(rawPassword));
 }
 
 // 加密方法
 private String generatePwd(CharSequence rawPassword) {
  String pwd = (String)rawPassword;
  try {
   // 获取MD5加密类
   MessageDigest instance = MessageDigest.getInstance("MD5");
   // 得到加密后的密码字节数组
   byte[] afterPwd = instance.digest(pwd.getBytes());
   // 转换成16位
   return new BigInteger(1,afterPwd).toString(16);
  } catch (NoSuchAlgorithmException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  return null;
 }

}

8.1.2 配置

1)引入加密算法

2)设置加密算法

8.2 带盐值的加密

SpringSecutiry中,给我们提供了一个类bCryptPasswordEncoder,我们可以很方便的对密码进行加密,其比Md5更安全,采用的是动态“盐”机制。

使用起来很简单,直接将我们自定义的那个加密类替换成bCryptPasswordEncoder即可。

例如:

注意:我们需要将该类注入到spring容器中

也可以直接调用getBCryptPasswordEncoder()传参。

配置类最终版

package com.bjc.security.config;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

@Configuration
@EnableWebSecurity  // 启动web环境下的权限控制功能
public class WebApplicationSecurityConfig extends WebSecurityConfigurerAdapter{
 
 @Autowired
 private DataSource dataSource;
 
 @Autowired
 private MyUserDetailServiceImpl userDetailsService;
 
 /*
  * @Autowired private MyPasswordEncoder passwordEncoder;
  */
 
 @Autowired
 private BCryptPasswordEncoder bCryptPasswordEncoder;
 
 @Bean
 public BCryptPasswordEncoder getBCryptPasswordEncoder() {
  return new BCryptPasswordEncoder();
 }
 
 // 授权  确定用户有什么权限和角色
 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  /*auth
   .inMemoryAuthentication()  // 内存授权
   .withUser("tom")    // 设置账号
   .password("123456")    // 设置密码
   .roles("ADMIN","门徒")   // 设置角色
   .and()
   .withUser("marry")    // 设置另一个用户
   .password("123456")    // 密码
   .authorities("SAVE","UPDATE") // 设置权限
   ;*/
  
  // 装配userDetailsService
  auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
 }
 
 // 认证  确定资源需要什么权限和角色才能访问
 @Override
 protected void configure(HttpSecurity security) throws Exception {
  
  JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl(); 
  repository.setDataSource(dataSource);
  
  security
   .authorizeRequests()   // 对请求进行授权
   .antMatchers("/index.jsp")  // 针对 /index.jsp路径进行授权
   .permitAll()     // 可以无条件访问
   .antMatchers("/layui/**")  // 针对 /layui目录下的静态资源进行授权
   .permitAll()     // 设置其可以无条件访问
   // .and()
   // .authorizeRequests()   // 对请求进行授权
   .antMatchers("/level1/**")  // 访问level1下的资源
   .hasRole("门徒")     // 需要角色是门徒的才能访问
   .antMatchers("/level2/**")  // 访问level2下的资源
   .hasAuthority("UPDATE")   // 需要用户具备UPDATE权限才能访问
   .anyRequest()     // 其他未设置的全部请求
   .authenticated()    // 都需要登录之后才能访问
   .and()
   .formLogin()     // 无权限跳转到自带的登录页
   /**
      *  关于loginPage方法的特殊说明:
    *  指定登录页的同时会影响到:提交表单的地址、退出登录地址、登录失败的地址
      *        如果指定loginPage的值为/index.jsp,那么,
    *  1)去登录页:/index.jsp  Get
    *  2)提交登录表单:/index.jsp  POST
    *  3)登录失败:/index.jsp?error
    *  4)退出登录:/index.jsp?logout
    * */
   .loginPage("/index.jsp")  // 指定登录页面(如果没有指定会访问security自带的登录页面)
   /*
    * loginProcessingUrl的说明:
    *   如果指定了loginProcessingUrl,那么其值就会覆盖loginPage方法中设置的默认值(index.jsp POST)
    * */
   .loginProcessingUrl("/do/login.html")   // 指定提交登录表单的地址
   .permitAll()       // 允许登录地址访问(可以不要)
   .usernameParameter("loginAcct")   // 指定登录账号的请求参数名
   .passwordParameter("loginPwd")   // 指定登录密码号的请求参数名
   .defaultSuccessUrl("/main.html")  // 设置登录成功去到的页面
   .and()
   //.csrf()
   //.disable()        // 禁用csrf功能
   .logout()        // 开启退出功能
   .logoutUrl("/do/logout.html")   // 指定退出url
   .logoutSuccessUrl("/index.jsp")   // 指定退出成功之后去到的url
   .and()
   .exceptionHandling()      // 指定异常处理器
   // .accessDeniedPage("/to/no/auth/page.html") // 访问被拒绝的时候,前往的页面
   .accessDeniedHandler(new AccessDeniedHandler() {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException, ServletException {
     // 携带数据到页面,页面用${message}接收数据
     request.setAttribute("message", "抱歉,您没有权限访问该页面,请找管理员配置合适的权限在访问。。。");
     // 转发
     request.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(request, response);
    }
   })
   .and()
   .rememberMe()  // 记住我
   .tokenRepository(repository);
   ;
 }
 
}

9. security标签

我们需要在页面中显示security提供的信息,可以使用security提供的标签库来显示相应的信息,例如,显示登录用户的名称

9.1 导入标签库

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

9.2 使用标签

如图,security给我们提供了如下几个标签

以security:authentication为例,标签写法如下:

<security:authentication property="xxx"/>

property属性为表达式,这个表达式怎么写了,我们暂时用xxx代替,看页面显示什么,启动项目,如图:

页面报错了,我们看最下面的错误信息,如图:

可以知道,该标签获取的属性是类UsernamePasswordAuthenticationToken中的属性,打开该类,如图,发现了2个get方法

我们在页面中获取一下principal(主体),看看它是何方神圣,如图:

SecurityAdmin是我们定义的一个继承了User的类,由此可以得出一个结论:

SpringSecurity处理完登录操作之后把登录成功的User对象以principal属性名存入了UsernamePasswordAuthenticationToken对象

知道了这里的主体是什么了,表达式就好写了,例如:

显示用户昵称:

<security:authentication property="principal.originalAdmin.userName"/>

注意:这里没有使用credentials,是因为credentials是密码擦除,最后程序将其置为了null,所以页面访问会报错。

10 权限控制

10.1 使用角色控制

10.1.1 配置方式

指定某个连接必须具有某个角色或者权限才能访问

10.1.2 注解方式

为了方便,我们往往在控制层(Controller)使用注解来进行权限控制,值得注意的是,使用注解方式,需要在配置类中开启注解权限功能

1) 开启注解权限功能

在配置类上加上如下注解

@EnableGlobalMethodSecurity(prePostEnabled = true)

例如:

2) 在需要控制的地方加上注解

例如:我们希望访问/role/getPageInfo.json需要有部长的角色才能访问

我们只需要在方法上添加如下注解

@PreAuthorize("hasRole('部长')")

例如:

10.2 使用权限进行权限控制

同使用角色,只是单词不同,例如:

1)配置类方式

.antMatchers("/admin/getByPageInfo.html")
.hasAuthority("user:get")

2)注解方式

@PreAuthorize("hasAuthority('user:get')")

10.3 使用角色与权限进行权限控制

在配置中,我们可以access来使用角色与权限联合权限控制

例如:只有是经理角色,并具有user:get的用户才能访问

.access("hasRole('经理') OR hasAuthority('user:get')")

11. 其他注解

11.1 @PostAuthorize

先执行方法然后根据方法返回值判断是否具备权限

例如:查询一个 Admin 对象,在@PostAuthorize 注解中和当前登录的 Admin 对象 进行比较,如果不一致,则判断为不能访问。实现“只能查自己”效果。

@PostAuthorize("returnObject.data.loginAcct == principal.username")

注意:使用 returnObject 获取到方法返回值,使用 principal 获取到当前登录用户的主体对 象

11.2 @PreFilter

在方法执行前对传入的参数进行过滤。只能对集合类型的数据进行过滤。

@PreFilter(value="filterObject%2==0") 
@ResponseBody 
@RequestMapping("/admin/test/pre/filter") 
public ResultEntity<List<Integer>> saveList(@RequestBody List<Integer> valueList) { 
 return ResultEntity.successWithData(valueList); 
}

11.3 @PostFilter

在方法执行后对方法返回值进行过滤。只能对集合类型的数据进行过滤。

12. 页面元素的权限控制

我们可以使用标签根据用户不同的权限来显示不同的内容,当然,前提是我们需要引入标签库

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

使用标签<security:authorize>来实现,例如:

<!-- 有经理角色的才能看到 -->
<security:authorize access="hasRole('经理')">
    <div class="col-xs-6 col-sm-3 placeholder">
      <img data-src="holder.js/200x200/auto/sky" class="img-responsive" alt="Generic placeholder thumbnail">
      <h4>Label</h4>
      <span class="text-muted">Something else</span>
    </div>
</security:authorize>
<!-- 有部长角色的才能看到 -->
<security:authorize access="hasRole('部长')">
    <div class="col-xs-6 col-sm-3 placeholder">
      <img data-src="holder.js/200x200/auto/vine" class="img-responsive" alt="Generic placeholder thumbnail">
      <h4>Label</h4>
      <span class="text-muted">Something else</span>
    </div>
</security:authorize>
<!-- 有user:get权限的才能看到 -->
<security:authorize access="hasAuthority('user:get')">
    <div class="col-xs-6 col-sm-3 placeholder">
      <img data-src="holder.js/200x200/auto/sky" class="img-responsive" alt="Generic placeholder thumbnail">
      <h4>Label</h4>
      <span class="text-muted">Something else</span>
   </div>
</security:authorize>

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:81
帖子:4969
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP