day07【后台】SpringSecurity
1、权限控制框架
1.1、SpringSecurity 框架简介
用户登录系统时我们协助 SpringSecurity 把用户对应的角色、 权限组装好, 同时把各个资源所要求的权限信息设定好, 剩下的“ 登录验证”、 “ 权限验证” 等等工作都交给 SpringSecurity
1.2、权限控制相关概念
1.2.1、主体
英文单词: principal,使用系统的用户或设备或从其他系统远程登录的用户等等。 简单说就是谁使用系统,谁就是主体。
1.2.2、认证
英文单词: authentication,权限管理系统确认一个主体的身份, 允许主体进入系统。 简单说就是“主体” 证明自己是谁。
笼统的认为就是以前所做的登录操作。
1.2.3、授权
英文单词: authorization,将操作系统的“权力”“授予”“主体”, 这样主体就具备了操作系统中特定功能的能力。
所以简单来说, 授权就是给用户分配权限
2、搭建SpringMVC环境
2.1、创建Maven工程
- 新建一个
Maven
工程
- 打包方式为
war
- 生成
web.xml
文件
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
的配置文件
<?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组件
- 在工程下添加如下控制器
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、加入前端页面资源
- 加入如下前端资源
2.7、测试环境
- 测试成功哦~
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
。 因为springSecurityFilterChain
在IOC
容器中对应真正执行权限控制的二十几个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
配置类,注意!这个类一定要放在自动扫描的包下,否则所有配置都不会生效!
// 注意!这个类一定要放在自动扫描的包下,否则所有配置都不会生效!
// 将当前类标记为配置类
@Configuration
// 启用Web环境下权限控制功能
@EnableWebSecurity
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter {
}
3.4、拦截效果
- 访问任何页面,都会跳转回
SpringSecurity
自带的登录页面
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") // 指定提交登录表单的地址
4.3、实验效果
- 可以正常访问
index.jsp
页面和静态资源(因为index.jsp
页面需要用到静态资源,所以能正常访问index.jsp
页面,就说明静态资源也能正常访问)
- 其他资源就不行咯~
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
页面
6、实验三:设置登录系统的账号、 密码
6.1、思路
6.2、页面设置
- 修改页面表单的提交地址
CSRF
- 表单中用户名输入框和密码输入框的
name
属性要和之后的配置一致
- 表单代码如下:
<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
值, 则返回下面错误: -
从钓鱼网站的页面提交的请求无法携带正确、 被承认的令牌
6.5、实验效果
- 带上
CSRF
,则可以成功登陆
- 删除
CSRF
,则无法登陆
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
中,指定注销请求的地址
<!-- 禁用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
的值
<%@ 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、实验效果
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、实验结果
8.3、源码分析
8.3.1、添加角色
Debug
运行至如下代码处
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
至如下代码处
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
至如下代码处
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
至如下代码处
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
<%@ 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") // 访问被拒绝时前往的页面
;
}
- 实验效果
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);
}
})
;
}
- 实验效果
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
- 页面中
CheckBox
的name
属性的值一定要设置为"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
- 点击记住我,登陆之后,多了一个
name=remember-me
的cookie
,并且过期时间为7.1
,这说明我们关闭浏览器再打开页面,也能直接访问武林秘籍
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、如何修改源码
- 包名、类名均需与目标源码相同,然后将目标源码拷贝过来,做修改即可
/*
* 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
应用程序,则会创建数据库表
- 用户登录之后,数据库表中便会插入一条
Token
- 用户注销之后,相应的
Token
便会被删除
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) # 逐渐
);
- 插入测试数据
12.2、创建实体类
- 创建
Admin
实体类
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_
开头哦
- 根据表单提交的数据查询
@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_
前缀
12.5、重写configure方法
- 在配置类中启用我们自定义的数据库查询方式
@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
啦