SpringSecurity安全框架(配置类版)

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>

 

 

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值