day07【后台】SpringSecurity

day07【后台】SpringSecurity

1、权限控制框架

1.1、SpringSecurity 框架简介

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

image-20200919193504147

1.2、权限控制相关概念

1.2.1、主体

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

1.2.2、认证

英文单词: authentication,权限管理系统确认一个主体的身份, 允许主体进入系统。 简单说就是“主体” 证明自己是谁。

笼统的认为就是以前所做的登录操作。

1.2.3、授权

英文单词: authorization,将操作系统的“权力”“授予”“主体”, 这样主体就具备了操作系统中特定功能的能力。

所以简单来说, 授权就是给用户分配权限

image-20200919193748026

2、搭建SpringMVC环境

2.1、创建Maven工程

  • 新建一个Maven工程

image-20200616155417138

  • 打包方式为war

image-20200616155653920

  • 生成web.xml文件

image-20200616155757696

image-20200616155822170

2.2、加入SpringMVC所需依赖

  • 在工程的pom文件下添加所需依赖
<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.atguigu.security</groupId>
	<artifactId>SecurityLearn</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.3、SpringMVC配置文件

  • resources文件夹下添加SpringMVC的配置文件

image-20200616160022670

<?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.atguigu.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.4、配置DispatcherServlet

  • web.xml文件中配置DispatcherServlet
<?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">

	<!-- The front controller of this Spring Web application, responsible for 
		handling all application requests -->
	<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.5、添加Controller组件

  • 在工程下添加如下控制器

image-20200616160403872

  • AdminController.java代码
@Controller
public class AdminController {
	
	@GetMapping("/main.html")
	public String main(){
		return "main";
	}
	
	@RequestMapping("/to/no/auth/page.html")
	public String toNoAuthPage() {
		return "no_auth";
	}

}
  • GongfuController.java代码
@Controller
public class GongfuController {
	
	@GetMapping("/level1/{path}")
	public String leve1Page(@PathVariable("path")String path){
		return "/level1/"+path;
	}
	
	@GetMapping("/level2/{path}")
	public String leve2Page(@PathVariable("path")String path){
		return "/level2/"+path;
	}
	
	@GetMapping("/level3/{path}")
	public String leve3Page(@PathVariable("path")String path){
		return "/level3/"+path;
	}

}

2.6、加入前端页面资源

  • 加入如下前端资源

image-20200616161318668

2.7、测试环境

  • 测试成功哦~

image-20200616161219731

image-20200616161248507

3、加入SpringSecurity环境

3.1、引入依赖

  • pom文件下添加如下依赖,引入SpringSecurity所需的jar
<!-- 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>

3.2、权限控制之Filter

  • web.xml中添加用于权限认证的Filter
    • SpringSecurity使用的是过滤器Filter而不是拦截器Interceptor, 意味着SpringSecurity能够管理的不仅仅是 SpringMVC 中的 handler 请求, 还包含Web应用中所有请求,比如:
      项目中的静态资源也会被拦截, 从而进行权限控制
    • 标签中必须是springSecurityFilterChain。 因为springSecurityFilterChainIOC 容器中对应真正执行权限控制的二十几个 Filter, 只有叫这个名字才能够加载到这些 Filter
<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>

3.3、加入配置类

  • 在工程下添加关于SpringSecurity配置类,注意!这个类一定要放在自动扫描的包下,否则所有配置都不会生效!

image-20200616182742219

// 注意!这个类一定要放在自动扫描的包下,否则所有配置都不会生效!

// 将当前类标记为配置类
@Configuration

// 启用Web环境下权限控制功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {

}

3.4、拦截效果

  • 访问任何页面,都会跳转回SpringSecurity自带的登录页面

image-20200616182958363

4、实验一:放行首页和静态资源

4.1、重写configure方法

  • 父类中的configure方法代码如下
/**
	 * Override this method to configure the {@link HttpSecurity}. Typically subclasses
	 * should not invoke this method by calling super as it may override their
	 * configuration. The default configuration is:
	 *
	 * <pre>
	 * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
	 * </pre>
	 *
	 * @param http the {@link HttpSecurity} to modify
	 * @throws Exception if an error occurs
	 */
// @formatter:off
protected void configure(HttpSecurity http) throws Exception {
    logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

    http.authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .formLogin().and()
        .httpBasic();
}
  • 重写WebSecurityConfigurerAdapter父类中的configure方法
// 注意!这个类一定要放在自动扫描的包下,否则所有配置都不会生效!

// 将当前类标记为配置类
@Configuration
// 启用Web环境下权限控制功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Override
	protected void configure(HttpSecurity security) throws Exception {
		security
			.authorizeRequests()						// 对请求进行授权
			.antMatchers("/index.jsp", "/layui/**")		  // 针对/index.jsp路径进行授权
			.permitAll()								// 可以无条件访问
			.and()
			.authorizeRequests()						// 对请求进行授权
			.anyRequest()								// 任意请求
			.authenticated()							// 需要登录以后才可以访问
			;
	}
	
}

4.2、先后顺序

4.2.1、注意顺序
  • 设置授权信息时需要注意, 范围小的放在前面、 范围大的放在后面。 不然的话, 小范围的设置会被大范围设置覆盖。
4.2.2、来个死循环
  • 这是之后复习时补的笔记,所以需要用到后面的知识哟~我们将范围大的授权信息放在前面
@Override
protected void configure(HttpSecurity security) throws Exception {

    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource);		// 设置数据源
    tokenRepository.setCreateTableOnStartup(true);	// 标记为true,才会创建数据库表
    tokenRepository.initDao();						// 执行建表SQL

    security
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .antMatchers("/level1/**")					// 针对/level1/**路径设置访问要求
        .hasRole("学徒")								// 要求用户具备“学徒”角色才可以访问
        .antMatchers("/level2/**")					// 针对/level2/**路径设置访问要求
        .hasAuthority("内门弟子")						// 要求用户具备“内门弟子”权限才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
        .and()
        .logout()									// 开启退出功能
        .logoutUrl("/do/logout.html")				// 指定处理退出请求的URL地址
        .logoutSuccessUrl("/index.jsp")				// 退出成功后前往的地址
        .and()
        .exceptionHandling()								// 指定异常处理器
        //.accessDeniedPage("/to/no/auth/page.html")		// 访问被拒绝时前往的页面
        .accessDeniedHandler(new AccessDeniedHandler() {	// 自定义异常处理逻辑
            @Override
            public void handle(
                HttpServletRequest request, 
                HttpServletResponse response,
                AccessDeniedException accessDeniedException
            ) throws IOException, ServletException {
                request.setAttribute("message", "抱歉!您无法访问这个资源!☆☆☆");
                request.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(request, response);
            }
        })
        .and()
        .rememberMe()								// 开启记住我功能
        .tokenRepository(tokenRepository)			// 启用令牌仓库功能
        ;

}
4.2.3、重定向次数过多
  • 分析:
    • 首先,想要访问任意请求,则必须活已授权(登录),所以就会跳转到index.jsp页面进行登录
    • 然后嘞?浏览器就会跳转至index.jsp页面,前提嘞?访问任意请求的前提还是必须已授权(登录)
    • 然后嘞?浏览器就会跳转至index.jsp页面,前提嘞?访问任意请求的前提还是必须已授权(登录)
    • 浏览器:我特么可能不是人,但你是真的狗~
security
    .authorizeRequests()						// 对请求进行授权
    .anyRequest()								// 任意请求
    .authenticated()							// 需要登录以后才可以访问
    .and()
    .authorizeRequests()						// 对请求进行授权
    .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
    .permitAll()								// 可以无条件访问
    .antMatchers("/level1/**")					// 针对/level1/**路径设置访问要求
    .hasRole("学徒")								// 要求用户具备“学徒”角色才可以访问
    .antMatchers("/level2/**")					// 针对/level2/**路径设置访问要求
    .hasAuthority("内门弟子")						// 要求用户具备“内门弟子”权限才可以访问
    .and()
    .formLogin()								// 使用表单形式登录

    // 关于loginPage()方法的特殊说明
    // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
    // /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 退出登录
    .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

    // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
    .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址

image-20200617185747304

4.3、实验效果

  • 可以正常访问index.jsp页面和静态资源(因为index.jsp页面需要用到静态资源,所以能正常访问index.jsp页面,就说明静态资源也能正常访问)

image-20200616202400603

  • 其他资源就不行咯~

image-20200616202417865

5、实验二:未认证跳转至登录页面

5.1、指定登录页面地址

  • configure方法中指定登录页面地址,以及登录表单提交的地址
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        ;

}

5.2、实验效果

  • 访问没有授权的资源,则会自动跳转回index.jsp页面

image-20200616203652193

6、实验三:设置登录系统的账号、 密码

6.1、思路

image-20200919194138970

6.2、页面设置

  • 修改页面表单的提交地址

image-20200616205524887

  • CSRF

image-20200616212313513

  • 表单中用户名输入框和密码输入框的name属性要和之后的配置一致

image-20200616211136482

  • 表单代码如下:
<p>${SPRING_SECURITY_LAST_EXCEPTION.message}</p>
<form action="${pageContext.request.contextPath }/do/login.html" method="post">
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
	<div class="layadmin-user-login-main">
		<div class="layadmin-user-login-box layadmin-user-login-header">
			<h2>layuiAdmin</h2>
			<p>layui 官方出品的单页面后台管理模板系统</p>
		</div>
		<div
			class="layadmin-user-login-box layadmin-user-login-body layui-form">	
				<div class="layui-form-item">						
					<label
						class="layadmin-user-login-icon layui-icon layui-icon-username"
						for="LAY-user-login-username"></label> 		
					<!-- input的name属性值必须符合SpringSecurity规则,除非专门进行了定制,否则用户名必须使用username,密码必须使用password -->
					<input type="text"
						name="loginAcct" id="LAY-user-login-username" lay-verify="required"
						placeholder="用户名" class="layui-input">
				</div>
				<div class="layui-form-item">
					<label
						class="layadmin-user-login-icon layui-icon layui-icon-password"
						for="LAY-user-login-password"></label> <input type="text"
						name="userPswd" id="LAY-user-login-password" lay-verify="required"
						placeholder="密码" class="layui-input">
				</div>
				<div class="layui-form-item">
					<div class="layui-row">
						<div class="layui-col-xs7">
							<label
								class="layadmin-user-login-icon layui-icon layui-icon-vercode"
								for="LAY-user-login-vercode"></label>
								<input type="text"
								name="vercode" id="LAY-user-login-vercode" lay-verify="required"
								placeholder="图形验证码" class="layui-input">
						</div>
						<div class="layui-col-xs5">
							<div style="margin-left: 10px;">
								<img src="https://www.oschina.net/action/user/captcha"
									class="layadmin-user-login-codeimg" id="LAY-user-get-vercode">
							</div>
						</div>
					</div>
				</div>
				<div class="layui-form-item" style="margin-bottom: 20px;">
					<input type="checkbox" name="remember-me" lay-skin="primary"
						title="记住我"> <a href="forget.html"
						class="layadmin-user-jump-change layadmin-link"
						style="margin-top: 7px;">忘记密码?</a>
				</div>
				<div class="layui-form-item">
					<button type="submit" class="layui-btn layui-btn-fluid" lay-submit
						lay-filter="LAY-user-login-submit">登 入</button>
				</div>
				<div class="layui-trans layui-form-item layadmin-user-login-other">
					<label>社交账号登入</label> <a href="javascript:;"><i
						class="layui-icon layui-icon-login-qq"></i></a> <a href="javascript:;"><i
						class="layui-icon layui-icon-login-wechat"></i></a> <a
						href="javascript:;"><i
						class="layui-icon layui-icon-login-weibo"></i></a> <a href="reg.html"
						class="layadmin-user-jump-change layadmin-link">注册帐号</a>
				</div>
		</div>
	</div>
</form>

6.3、重写configure方法

  • protected void configure(AuthenticationManagerBuilder builder) throws Exception {方法;登录页面中,表单提交的用户名字段名必须是loginAcct,表单提交的密码字段名必须是userPswd
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
        ;

}
  • protected void configure(HttpSecurity security) throws Exception {方法
    • 分配Admin角色给tom,密码为123123
    • 分配UPDATE权限给jerry,密码为123123
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {

    builder
        .inMemoryAuthentication()	// 在内存中完成账号、密码的检查
        .withUser("tom")			// 指定账号
        .password("123123")			// 指定密码
        .roles("ADMIN")				// 指定当前用户的角色
        .and()
        .withUser("jerry")			// 指定账号
        .password("123123")			// 指定密码
        .authorities("UPDATE")		// 指定当前用户的权限
        ;

}

6.4、CSRF介绍

  • 了解跨站请求伪造:Cross-site request forgery 跨站请求伪造,发送登录请求时没有携带_csrf 值, 则返回下面错误:

    image-20200919194351364

  • 从钓鱼网站的页面提交的请求无法携带正确、 被承认的令牌

image-20200919194429144

6.5、实验效果

  • 带上CSRF,则可以成功登陆

image-20200616211928648

  • 删除CSRF,则无法登陆

image-20200616212351240

7、实验四:用户注销

7.1、禁用CSRF版本

7.1.1、重写configure方法
  • 重写configure方法
    • 禁用CSRF功能
    • 开启注销功能
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
        .and()
        .csrf()
        .disable()									// 禁用CSRF功能
        .logout()									// 开启退出功能
        .logoutUrl("/do/logout.html")				// 指定处理退出请求的URL地址
        .logoutSuccessUrl("/index.jsp")				// 退出成功后前往的地址
        ;

}
7.1.2、提交注销请求
  • 在上方navbar中,指定注销请求的地址

image-20200616213303514

<!-- 禁用CSRF功能的前提下,最简单的退出操作 -->
<a href="${pageContext.request.contextPath }/do/logout.html">退出</a>

7.2、启用CSRF版本

7.2.1、重写configure方法
  • 重写configure方法
    • 启用CSRF功能
    • 开启注销功能
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
//		.and()
//		.csrf()
//		.disable()									// 禁用CSRF功能
        .and()
        .logout()									// 开启退出功能
        .logoutUrl("/do/logout.html")				// 指定处理退出请求的URL地址
        .logoutSuccessUrl("/index.jsp")				// 退出成功后前往的地址
        ;

}
7.2.2、提交注销请求
  • navbar.jsp页面中,提交注销请求的同时,附带上CSRF的值

image-20200616213303514

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<div class="layui-header">
	<div class="layui-logo" onclick="location.href='${PATH }/main.html'">武林秘籍管理系统</div>
	<!-- 头部区域(可配合layui已有的水平导航) -->
	<ul class="layui-nav layui-layout-left">
		<li class="layui-nav-item"><a href="">控制台</a></li>
		<li class="layui-nav-item"><a href="">商品管理</a></li>
		<li class="layui-nav-item"><a href="">用户</a></li>
		<li class="layui-nav-item"><a href="javascript:;">其它系统</a>
			<dl class="layui-nav-child">
				<dd>
					<a href="">邮件管理</a>
				</dd>
				<dd>
					<a href="">消息管理</a>
				</dd>
				<dd>
					<a href="">授权管理</a>
				</dd>
			</dl></li>
	</ul>
	<ul class="layui-nav layui-layout-right">
		<li class="layui-nav-item"><a href="javascript:;"> <img
				src="http://t.cn/RCzsdCq" class="layui-nav-img"> 张无忌
		</a>
			<dl class="layui-nav-child">
				<dd>
					<a href="">基本资料</a>
				</dd>
				<dd>
					<a href="">安全设置</a>
				</dd>
			</dl></li>
		<li class="layui-nav-item">
			<!-- 禁用CSRF功能的前提下,最简单的退出操作 -->
			<%-- <a href="${pageContext.request.contextPath }/do/logout.html">退出</a> --%>
			<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() {
					
					// 给超链接的DOM对象绑定单击响应函数
					document.getElementById("logoutAnchor").onclick = function() {
						
						// 提交包含csrf参数的表单
						document.getElementById("logoutForm").submit();
						
						// 取消超链接的默认行为
						return false;
						
					};
					
				};
			</script>
		</li>
	</ul>
</div>

7.3、实验效果

image-20200616214446237

8、实验五:基于角色或权限访问

8.1、重写configure方法

  • 设置角色或权限与资源的关联关系
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .antMatchers("/level1/**")					// 针对/level1/**路径设置访问要求
        .hasRole("学徒")								// 要求用户具备“学徒”角色才可以访问
        .antMatchers("/level2/**")					// 针对/level2/**路径设置访问要求
        .hasAuthority("内门弟子")						// 要求用户具备“内门弟子”权限才可以访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
        .and()
        .logout()									// 开启退出功能
        .logoutUrl("/do/logout.html")				// 指定处理退出请求的URL地址
        .logoutSuccessUrl("/index.jsp")				// 退出成功后前往的地址
        ;

}
  • 设置用户与角色或权限的关联关系
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {

    builder
        .inMemoryAuthentication()			// 在内存中完成账号、密码的检查
        .withUser("tom")					// 指定账号
        .password("123123")					// 指定密码
        .roles("ADMIN","学徒")				// 指定当前用户的角色
        .and()
        .withUser("jerry")					// 指定账号
        .password("123123")					// 指定密码
        .authorities("UPDATE","内门弟子")		// 指定当前用户的权限
        ;

}

8.2、实验结果

image-20200616224044692

8.3、源码分析

8.3.1、添加角色
  • Debug运行至如下代码处

image-20200616224822083

  • Step into进入.hasRole("学徒") 方法
public ExpressionInterceptUrlRegistry hasRole(String role) {
    return access(ExpressionUrlAuthorizationConfigurer.hasRole(role));
}
  • Step into进入ExpressionUrlAuthorizationConfigurer.hasRole(role)方法
private static String hasRole(String role) {
    Assert.notNull(role, "role cannot be null");
    if (role.startsWith("ROLE_")) {
        throw new IllegalArgumentException(
            "role should not start with 'ROLE_' since it is automatically inserted. Got '"
            + role + "'");
    }
    return "hasRole('ROLE_" + role + "')";
}
  • 结论:添加角色时,SpringSecurity会在角色名称前面添加ROLE_字符串,结论:我们自定义的角色名称不能以ROLE_开头(it is automatically added),不然就会抛异常
8.3.2、分配角色
  • Debug至如下代码处

image-20200616225116867

  • Step into进入.roles("ADMIN","学徒")方法
public UserDetailsBuilder roles(String... roles) {
    this.user.roles(roles);
    return this;
}
  • Step into进入this.user.roles(roles)方法
public UserBuilder roles(String... roles) {
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(
        roles.length);
    for (String role : roles) {
        Assert.isTrue(!role.startsWith("ROLE_"), role
                      + " cannot start with ROLE_ (it is automatically added)");
        authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
    }
    return authorities(authorities);
}
  • Step into进入authorities(authorities)方法
public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
    this.authorities = new ArrayList<GrantedAuthority>(authorities);
    return this;
}
8.3.3、添加权限
  • Debug至如下代码处

image-20200616225405781

  • Step into进入.hasAuthority("内门弟子")方法
public ExpressionInterceptUrlRegistry hasAuthority(String authority) {
    return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));
}
  • Step into进入ExpressionUrlAuthorizationConfigurer.hasAuthority(authority)方法
private static String hasAuthority(String authority) {
    return "hasAuthority('" + authority + "')";
}
  • 结论:不像添加角色那样,添加权限时,SpringSecurity并没有为权限添加前缀
8.3.4、分配权限
  • Debug至如下代码处

image-20200616230054839

  • Step into进入.authorities("UPDATE","内门弟子")方法
public UserDetailsBuilder authorities(String... authorities) {
    this.user.authorities(authorities);
    return this;
}
  • Step into进入this.user.authorities(authorities)方法
public UserBuilder authorities(String... authorities) {
    return authorities(AuthorityUtils.createAuthorityList(authorities));
}
  • Step into进入AuthorityUtils.createAuthorityList(authorities)方法
public static List<GrantedAuthority> createAuthorityList(String... roles) {
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(roles.length);

    for (String role : roles) {
        authorities.add(new SimpleGrantedAuthority(role));
    }

    return authorities;
}
  • Step into进入authorities(authorities)方法
public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
    this.authorities = new ArrayList<GrantedAuthority>(authorities);
    return this;
}

8.4、“ROLE_”的坑

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

9、实验六:自定义 403 错误页面

9.1、添加403错误页面

  • 403错误页面:no_auth.jsp

image-20200617142727551

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%
	pageContext.setAttribute("PATH", request.getContextPath());
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
	content="width=device-width, initial-scale=1, maximum-scale=1">
<title>武林秘籍管理系统</title>
<link rel="stylesheet" href="${PATH }/layui/css/layui.css">
</head> 
<body class="layui-layout-body">
	<div class="layui-layout layui-layout-admin">
		<!-- 顶部导航 -->
		<%@include file="/WEB-INF/include/navbar.jsp" %>
		
		<!-- 侧边栏 -->
		<%@include file="/WEB-INF/include/sidebar.jsp" %>
		
		
		<div class="layui-body">
			<!-- 内容主体区域 -->
			<div style="padding: 15px;">
				<h1>非常抱歉!您没有访问这个功能的权限!(回家照照镜子)</h1>
				<h2>${message }</h2>
			</div>
		</div>
		<div class="layui-footer"></div>
	</div>
	<script src="${PATH }/layui/layui.js"></script>
	<script>
		//JavaScript代码区域
		layui.use('element', function() {
			var element = layui.element;

		});
	</script>
</body>
</html>

9.2、来到403错误页面

  • 重写configure方法:指定访问被拒绝时,跳转的页面
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .antMatchers("/level1/**")					// 针对/level1/**路径设置访问要求
        .hasRole("学徒")								// 要求用户具备“学徒”角色才可以访问
        .antMatchers("/level2/**")					// 针对/level2/**路径设置访问要求
        .hasAuthority("内门弟子")						// 要求用户具备“内门弟子”权限才可以访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
        .and()
        .logout()									// 开启退出功能
        .logoutUrl("/do/logout.html")				// 指定处理退出请求的URL地址
        .logoutSuccessUrl("/index.jsp")				// 退出成功后前往的地址
        .and()
        .exceptionHandling()						// 指定异常处理器
        .accessDeniedPage("/to/no/auth/page.html")	// 访问被拒绝时前往的页面
        ;

}
  • 实验效果

image-20200617143234531

9.3、携带异常信息

  • 重写configure方法:访问被拒绝时,设置异常信息,并转发至指定页面
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .antMatchers("/level1/**")					// 针对/level1/**路径设置访问要求
        .hasRole("学徒")								// 要求用户具备“学徒”角色才可以访问
        .antMatchers("/level2/**")					// 针对/level2/**路径设置访问要求
        .hasAuthority("内门弟子")						// 要求用户具备“内门弟子”权限才可以访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
        .and()
        .logout()									// 开启退出功能
        .logoutUrl("/do/logout.html")				// 指定处理退出请求的URL地址
        .logoutSuccessUrl("/index.jsp")				// 退出成功后前往的地址
        .and()
        .exceptionHandling()								// 指定异常处理器
        //.accessDeniedPage("/to/no/auth/page.html")		// 访问被拒绝时前往的页面
        .accessDeniedHandler(new AccessDeniedHandler() {	// 自定义异常处理逻辑
            @Override
            public void handle(
                HttpServletRequest request, 
                HttpServletResponse response,
                AccessDeniedException accessDeniedException
            ) throws IOException, ServletException {
                request.setAttribute("message", "抱歉!您无法访问这个资源!☆☆☆");
                request.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(request, response);
            }
        })
        ;

}
  • 实验效果

image-20200617143318907

10、实验七:记住我-内存版(不重要)

10.1、开启记住我功能

  • configure方法中开启记住我功能
@Override
protected void configure(HttpSecurity security) throws Exception {

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .antMatchers("/level1/**")					// 针对/level1/**路径设置访问要求
        .hasRole("学徒")								// 要求用户具备“学徒”角色才可以访问
        .antMatchers("/level2/**")					// 针对/level2/**路径设置访问要求
        .hasAuthority("内门弟子")						// 要求用户具备“内门弟子”权限才可以访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
        .and()
        .logout()									// 开启退出功能
        .logoutUrl("/do/logout.html")				// 指定处理退出请求的URL地址
        .logoutSuccessUrl("/index.jsp")				// 退出成功后前往的地址
        .and()
        .exceptionHandling()								// 指定异常处理器
        //.accessDeniedPage("/to/no/auth/page.html")		// 访问被拒绝时前往的页面
        .accessDeniedHandler(new AccessDeniedHandler() {	// 自定义异常处理逻辑
            @Override
            public void handle(
                HttpServletRequest request, 
                HttpServletResponse response,
                AccessDeniedException accessDeniedException
            ) throws IOException, ServletException {
                request.setAttribute("message", "抱歉!您无法访问这个资源!☆☆☆");
                request.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(request, response);
            }
        })
        .and()
        .rememberMe()								// 开启记住我功能
        ;

}
  • 注意:如果不能使用“remember-me”作为请求参数名称,可以使用rememberMeParameter()方法定制

10.2、页面CheckBox

  • 页面中CheckBoxname属性的值一定要设置为"remember-me"`
<div class="layui-form-item" style="margin-bottom: 20px;">
	<input type="checkbox" name="remember-me" lay-skin="primary"
		title="记住我"> <a href="forget.html"
		class="layadmin-user-jump-change layadmin-link"
		style="margin-top: 7px;">忘记密码?</a>
</div>

10.3、记住我原理

  • 未登录之前只有JSESSIONID

image-20200617144826082

  • 点击记住我,登陆之后,多了一个name=remember-mecookie,并且过期时间为7.1,这说明我们关闭浏览器再打开页面,也能直接访问武林秘籍

image-20200617144920082

image-20200919194907410

11、实验八:记住我-数据库版(不重要)

11.1、加入依赖

  • 在工程的pom文件中添加数据库所需的依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<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>

11.2、配置数据源

  • SpringMVC配置文件中配置数据源,以及将数据源交由JdbcTemplate管理
<!-- 配置数据源 -->
<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>

11.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
);

11.4、如何修改源码

  • 包名、类名均需与目标源码相同,然后将目标源码拷贝过来,做修改即可

image-20200617150806910

/*
 * Copyright 2002-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.web.authentication.rememberme;

import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;

/**
 * JDBC based persistent login token repository implementation.
 *
 * @author Luke Taylor
 * @since 2.0
 */
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
		PersistentTokenRepository {
	// ~ Static fields/initializers
	// =====================================================================================

	/** Default SQL for creating the database table to store the tokens */
	public static final String CREATE_TABLE_SQL = "create table if not exists persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
			+ "token varchar(64) not null, last_used timestamp not null)";
	/** The default SQL used by the <tt>getTokenBySeries</tt> query */
	public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
	/** The default SQL used by <tt>createNewToken</tt> */
	public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
	/** The default SQL used by <tt>updateToken</tt> */
	public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
	/** The default SQL used by <tt>removeUserTokens</tt> */
	public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";

	// ~ Instance fields
	// ================================================================================================

	private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL;
	private String insertTokenSql = DEF_INSERT_TOKEN_SQL;
	private String updateTokenSql = DEF_UPDATE_TOKEN_SQL;
	private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL;
	private boolean createTableOnStartup;

	public void initDao() {
		if (createTableOnStartup) {
			getJdbcTemplate().execute(CREATE_TABLE_SQL);
		}
	}

	public void createNewToken(PersistentRememberMeToken token) {
		getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(),
				token.getTokenValue(), token.getDate());
	}

	public void updateToken(String series, String tokenValue, Date lastUsed) {
		getJdbcTemplate().update(updateTokenSql, tokenValue, lastUsed, series);
	}

	/**
	 * Loads the token data for the supplied series identifier.
	 *
	 * If an error occurs, it will be reported and null will be returned (since the result
	 * should just be a failed persistent login).
	 *
	 * @param seriesId
	 * @return the token matching the series, or null if no match found or an exception
	 * occurred.
	 */
	public PersistentRememberMeToken getTokenForSeries(String seriesId) {
		try {
			return getJdbcTemplate().queryForObject(tokensBySeriesSql,
					new RowMapper<PersistentRememberMeToken>() {
						public PersistentRememberMeToken mapRow(ResultSet rs, int rowNum)
								throws SQLException {
							return new PersistentRememberMeToken(rs.getString(1), rs
									.getString(2), rs.getString(3), rs.getTimestamp(4));
						}
					}, seriesId);
		}
		catch (EmptyResultDataAccessException zeroResults) {
			if (logger.isDebugEnabled()) {
				logger.debug("Querying token for series '" + seriesId
						+ "' returned no results.", zeroResults);
			}
		}
		catch (IncorrectResultSizeDataAccessException moreThanOne) {
			logger.error("Querying token for series '" + seriesId
					+ "' returned more than one value. Series" + " should be unique");
		}
		catch (DataAccessException e) {
			logger.error("Failed to load token for series " + seriesId, e);
		}

		return null;
	}

	public void removeUserTokens(String username) {
		getJdbcTemplate().update(removeUserTokensSql, username);
	}

	/**
	 * Intended for convenience in debugging. Will create the persistent_tokens database
	 * table when the class is initialized during the initDao method.
	 *
	 * @param createTableOnStartup set to true to execute the
	 */
	public void setCreateTableOnStartup(boolean createTableOnStartup) {
		this.createTableOnStartup = createTableOnStartup;
	}
}
  • 重要代码:创建数据库表
/** Default SQL for creating the database table to store the tokens */
public static final String CREATE_TABLE_SQL = "create table if not exists persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
    + "token varchar(64) not null, last_used timestamp not null)";

private boolean createTableOnStartup;

public void initDao() {
    if (createTableOnStartup) {
        getJdbcTemplate().execute(CREATE_TABLE_SQL);
    }
}
  • 为什么会加载我们的建立的类?类加载器的就近原则

11.5、配置Token

  • 重写configure方法
    • 注入数据源
    • 创建数据库表
    • 启用令牌仓库功能
@Override
protected void configure(HttpSecurity security) throws Exception {

    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource);		// 设置数据源
    tokenRepository.setCreateTableOnStartup(true);	// 标记为true,才会创建数据库表
    tokenRepository.initDao();						// 执行建表SQL

    security
        .authorizeRequests()						// 对请求进行授权
        .antMatchers("/index.jsp", "/layui/**")		// 针对/index.jsp路径进行授权
        .permitAll()								// 可以无条件访问
        .antMatchers("/level1/**")					// 针对/level1/**路径设置访问要求
        .hasRole("学徒")								// 要求用户具备“学徒”角色才可以访问
        .antMatchers("/level2/**")					// 针对/level2/**路径设置访问要求
        .hasAuthority("内门弟子")						// 要求用户具备“内门弟子”权限才可以访问
        .and()
        .authorizeRequests()						// 对请求进行授权
        .anyRequest()								// 任意请求
        .authenticated()							// 需要登录以后才可以访问
        .and()
        .formLogin()								// 使用表单形式登录

        // 关于loginPage()方法的特殊说明
        // 指定登录页的同时会影响到:“提交登录表单的地址”、“退出登录地址”、“登录失败地址”
        // /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 退出登录
        .loginPage("/index.jsp")					// 指定登录页面(如果没有指定会访问SpringSecurity自带的登录页)

        // loginProcessingUrl()方法指定了登录地址,就会覆盖loginPage()方法中设置的默认值/index.jsp POST
        .loginProcessingUrl("/do/login.html")		// 指定提交登录表单的地址
        .usernameParameter("loginAcct")				// 定制登录账号的请求参数名
        .passwordParameter("userPswd")				// 定制登录密码的请求参数名
        .defaultSuccessUrl("/main.html")			// 登录成功后前往的地址
        .and()
        .logout()									// 开启退出功能
        .logoutUrl("/do/logout.html")				// 指定处理退出请求的URL地址
        .logoutSuccessUrl("/index.jsp")				// 退出成功后前往的地址
        .and()
        .exceptionHandling()								// 指定异常处理器
        //.accessDeniedPage("/to/no/auth/page.html")		// 访问被拒绝时前往的页面
        .accessDeniedHandler(new AccessDeniedHandler() {	// 自定义异常处理逻辑
            @Override
            public void handle(
                HttpServletRequest request, 
                HttpServletResponse response,
                AccessDeniedException accessDeniedException
            ) throws IOException, ServletException {
                request.setAttribute("message", "抱歉!您无法访问这个资源!☆☆☆");
                request.getRequestDispatcher("/WEB-INF/views/no_auth.jsp").forward(request, response);
            }
        })
        .and()
        .rememberMe()								// 开启记住我功能
        .tokenRepository(tokenRepository)			// 启用令牌仓库功能
        ;

}

11.6、实验效果

  • 启动web应用程序,则会创建数据库表

image-20200617152348459

  • 用户登录之后,数据库表中便会插入一条Token

image-20200617152437502

  • 用户注销之后,相应的Token便会被删除

image-20200617152456703

11.7、修改源码建议

  • 尽量不要修改,因为会打乱原本框架的代码执行逻辑

12、实验九:查询数据库完成认证

12.1、创建数据库表

  • 创建t_admin
use security;
drop table if exists t_admin;
create table t_admin
(
  id int not null auto_increment, 		# 主键
  loginacct varchar(255) not null, 	# 登录账号
  userpswd char(32) not null, 			# 登录密码
  username varchar(255) not null, 		# 昵称
  email varchar(255) not null, 			# 邮件地址
  createtime char(19), 				# 创建时间
  primary key (id)						# 逐渐
);
  • 插入测试数据

image-20200617175926330

12.2、创建实体类

  • 创建Admin实体类

image-20200617180118184

public class Admin {

	private Integer id;
	private String loginacct;
	private String userpswd;
	private String username;
	private String email;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getLoginacct() {
		return loginacct;
	}

	public void setLoginacct(String loginacct) {
		this.loginacct = loginacct;
	}

	public String getUserpswd() {
		return userpswd;
	}

	public void setUserpswd(String userpswd) {
		this.userpswd = userpswd;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

}

12.3、自定义数据库查询方式

12.3.1、UserDetailsService接口
  • UserDetailsService接口定义的规范:根据用户名查询用户信息,并为其分配角色或权限
public interface UserDetailsService {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Locates the user based on the username. In the actual implementation, the search
	 * may possibly be case sensitive, or case insensitive depending on how the
	 * implementation instance is configured. In this case, the <code>UserDetails</code>
	 * object that comes back may have a username that is of a different case than what
	 * was actually requested..
	 *
	 * @param username the username identifying the user whose data is required.
	 *
	 * @return a fully populated user record (never <code>null</code>)
	 *
	 * @throws UsernameNotFoundException if the user could not be found or the user has no
	 * GrantedAuthority
	 */
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
12.3.2、创建UserDetailsService类
  • UserDetailsService类的作用:
    • 根据表单提交的数据查询Admin对象
    • 设置Admin对象的角色或权限信息
    • Admin对象的信息封装置UserDetails中(User类实现了UserDetails接口)
    • 注意:如果要分配角色,需要以ROLE_开头哦

image-20200617180311944

@Component
public class MyUserDetailsService implements UserDetailsService {
	
	@Autowired
	private JdbcTemplate jdbcTemplate;

	// 总目标:根据表单提交的用户名查询User对象,并装配角色、权限等信息
	@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<>();
		
		authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
		authorities.add(new SimpleGrantedAuthority("UPDATE"));
		
		// 3.把admin对象和authorities封装到UserDetails中
		
		String userpswd = admin.getUserpswd();
		
		return new User(username, userpswd, authorities);
        
	}

}

12.4、ROLE_ 前缀问题

在自定义的 UserDetailsService 中创建权限列表时,使用org.springframework.security.core.authority.AuthorityUtils.createAuthorityList(String...)工具方法获取创建 SimpleGrantedAuthority 对象添加角色时需要手动在角色名称前加ROLE_前缀

image-20200919195219791

12.5、重写configure方法

  • 在配置类中启用我们自定义的数据库查询方式

image-20200617180924922

@Autowired
private MyUserDetailsService myUserDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    /*
		builder
			.inMemoryAuthentication()			// 在内存中完成账号、密码的检查
			.withUser("tom")					// 指定账号
			.password("123123")					// 指定密码
			.roles("ADMIN","学徒")				// 指定当前用户的角色
			.and()
			.withUser("jerry")					// 指定账号
			.password("123123")					// 指定密码
			.authorities("UPDATE","内门弟子")		// 指定当前用户的权限
			;
		*/

    // 装配userDetailsService对象
    builder
        .userDetailsService(userDetailsService);

}

12.6、实验效果

  • 同样OK

image-20200617181652251

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值