当一个系统建立之后,通常需要适当地做一些权限控制,使得不同用户具有不同的权限操作系统。
用户自定义访问控制
- 实际生产中,网站访问多是基于HTTP请求的,我们可以通过重写WebSecurityConfigurerAdapter类的configure(HTTPSecurity http)方法来对基于Http的请求访问进行控制。
- configure(HttpSecurity http)方法的参数类型是HttpSecurity类,该类提供了Http请求的限制以及权限、Session管理配置、CSRF跨站请求问题。
authorizeRequest() | 开启基于HttpServletRequest请求访问的控制 |
---|
formLgoin() | 开启基于表单的用户登录 |
httpBasic() | 开启基于HTTP请求的Basic认证登录 |
logout() | 开启退出登录的支持 |
sessionManagement() | 开启Session管理配置 |
rememberMe() | 开启记住我功能 |
csrf() | 配置CSRF跨站请求伪造防护功能 |
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.and()
.formLogin();
}
- 测试,登录首页,进入common和vip页面会提示登录,根据权限控制访问部分。
自定义用户登录
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户登录界面</title>
<link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/login/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" th:action="@{/userLogin}" th:method="post" >
<img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px">
<h1 class="h3 mb-3 font-weight-normal">请登录</h1>
<div th:if="${param.error}" style="color: red;height: 40px;text-align: left;font-size: 1.1em">
<img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!
</div>
<input type="text" name="name" class="form-control" placeholder="用户名" required="" autofocus="">
<input type="password" name="pwd" class="form-control" placeholder="密码" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="rememberme"> 记住我
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button>
<p class="mt-5 mb-3 text-muted">Copyright© 2019-2020</p>
</form>
</body>
</html>
@GetMapping("/userLogin")
public String toLoginPage(){
return "login";
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("login").permitAll()
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated();
// 自定义用户登录控制
http.formLogin()
//指定想自定义登录页跳转的请求路径并放行
.loginPage("/userLogin").permitAll()
//接受登录的账号密码
.usernameParameter("name").passwordParameter("pwd")
// 默认成功界面
.defaultSuccessUrl("/")
// 失败页面
.failureUrl("/userLogin?error");
}
- 效果测试,发现登录页面已经被Security拦截并跳转到自定义的用户登录页面login.html.
自定义用户退出
- 自定义用户退出主要考虑退出后的会话如何管理以及跳转到哪个页面。HttpSecurity类的logout()方法用来处理用户退出,它默认处理路径为"/logout"的Post类型请求,同时也会清除Session和“Remember Me”等任何默认用户配置。
- 添加自定义用户退出链接
- 需要注意的是,springboot引入spring security框架后会自动开启CSRF防护功能(跨站请求伪造防护),用户退出时必须使用POST请求;如果关闭了CSRF防护功能,那么就可以使用任意方式的HTTP请求进行用户注销。
<form th:action="@{/mylogout}" method="post">
<input th:type="submit" th:value="注销">
</form>
- 自定义用户退出控制
- 在页面中定义好用户退出链接时,不需要再Controller控制层中额外定义用户退出方法,可以直接在Security中定制logout()方法实现用户退出。
http.logout()
.logoutUrl("/mylogout")
.logoutSuccessUrl("/");
登录用户信息获取
- 传统项目中进行用户登录处理时,通常会查询用户是否存在,如果存在则登录成功,同时将当前用户放在Session中。
- 两种方式获取登录后的用户信息:HttpSession和SecurityContextHolder方式
-
HttpSession方式
- 对于该方式,测试时登录用户后,再使用同一浏览器访问getUserBySession可以在控制台看到输出信息
@GetMapping("/getUserBySession")
@ResponseBody
public void getUser(HttpSession session){
Enumeration<String> names = session.getAttributeNames();
while(names.hasMoreElements()){
String element = names.nextElement();
SecurityContextImpl attribute =(SecurityContextImpl)session.getAttribute(element);
System.out.println("element:"+element);
System.out.println("attribute:"+attribute);
Authentication authentication = attribute.getAuthentication();
UserDetails principal = (UserDetails)authentication.getPrincipal();
System.out.println(principal);
System.out.println("username:"+principal.getUsername());
}
}
-
使用SecurityContextHolder获取用户信息
- Spring Security针对拦截的登录用户专门提供了一个SecurityContextHolder类,来获取应用上下文SecurityContex,进而获取封装的用户信息。
- 测试方式如上Session方式
- HttpSession方式获取用户信息相对比较传统,而且必须引入HttpSession对象;而Security提供的SecurityContextHolder相对简便,也是Security项目相对推荐的使用方式。
@GetMapping("getUserByContext")
@ResponseBody
public void getUser2(){
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("userDetails:"+context);
Authentication authentication = context.getAuthentication();
UserDetails principal = (UserDetails)authentication.getPrincipal();
System.out.println(principal);
System.out.println("username:"+principal.getUsername());
}
记住我功能
- SpringSecurity针对记住我功能提供了两种实现:
- 一种是简单地使用加密来保证基于Cookie中Token的安全
- 一种是通过数据库或其他持久化机制来保存生成的Token
-
基于简单加密Token的方式
- 当用户选择记住我并成功登录后,SpringSecurity会生成一个Cookie并发送给客户端浏览器。
- Cookie值的组合加密方式:
base64(username+":"+expirationTime+":"+md5Hex(username+":"+expirationTime+":"+password+":"+key)
- 安全隐患:任何人获取到该记住我功能的Token后,都可以在该Token值过期之前进行自动登录,只有当用户觉察到Token呗盗用后,才会对自己的登录密码进行修改来立即使其原有的记住我Token失效
- 效果测试:勾选了记住我选项后,在有效期内再次访问不需要重新登录验证。
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200);
-
基于持久化Token的方式
- 区别:基于简单加密Token的方式,生成的Token将在客户端保存一段时间,如果用户不退出登录,或者不修改密码,那么在Cookie失效之前,任何人都可以无限制地使用该Token进行自动登录。
- 基于持久化Token的方式采用如下逻辑:
- 用户选择记住我成功登录后,Security会把username、随机产生的序列号、生成的Token进行持久化存储,同时将它们的组合生成一个Cookie发送给客户端浏览器。
- 当用户再次访问系统时,首先检查客户端携带的Cookie,如果定义保存的一致,则通过验证并自动登录,同时系统将重新生成一个新的Token替换旧的Token,并把新的Token再次发送给客户端
- 如果Cookie中的Token不匹配,就需要重新登录,生成新的Token和Cookie,此时有可能Cookie被盗用了。
- 如果用户访问系统时没有携带Cookie,或者包含的username和序列号与数据库中保存的不一致,则会引导用户到登录页面。
- 持久化Token的方式比简单加密Token的方式相对更加安全。
- 创建存储Cookie信息的用户表
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201123150546452.png#pic_center)
- 编写持久化存储的代码
...
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200)
.tokenRepository(tokenRepository());
}
@Bean
public JdbcTokenRepositoryImpl tokenRepository(){
JdbcTokenRepositoryImpl jr = new JdbcTokenRepositoryImpl();
jr.setDataSource(dataSource);
return jr;
}
- 测试———在登录时选中记住我后,每次登录数据表中token都不相同,在点击注销后,表会清空
如果用户是在Token有效期后自动退出的,那么数据表中的持久化用户信息不会随之删除,当用户再次进行访问登录时,则是在表中新增一条持久化信息。
CSRF防护功能
- CSRF(跨站请求伪造),也叫一键攻击或者会话控制,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。
- CSRF攻击是黑客借助受害者的Cookie骗取服务器的信任,但是黑客并不能获取Cookie,也看不到Cookie的具体内容,即黑客能够做的就是伪造正常用户给服务器发送请求,执行请求中的命令,在服务端直接改变数据的值,而非窃取服务器中的数据。
因此,针对CSRF攻击要保护的对象是哪些可以直接产生数据变化的服务,而对于读取数据的服务,可以不进行CSRF保护。 - 对于CSRF的防御:验证HTTP Referer字段/在请求地址中添加Token并验证/在HTTP头中自定义属性并验证
-
CSRF防护功能关闭
- SpringBoot整合SpringSecurity默认开启了CSRF防御功能,并要求数据修改的请求都需要Security的安全认证后才可以正常访问
- 创建数据修改界面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户修改</title>
</head>
<body>
<div align="center">
<form method="post" action="@/updateUser">
用户名: <input type="text" name="username" /><br />
密 码: <input type="password" name="password" /><br />
<button type="submit">修改</button>
</form>
</div>
</body>
</html>
@Controller
public class CSRFController {
@GetMapping("/toUpdate")
public String toUpdate(){
return "csrf/csrfTest";
}
@ResponseBody
@PostMapping(value = "/updateUser")
public String updateUser(@RequestParam String username, @RequestParam String password, HttpServletRequest request){
System.out.println(username);
System.out.println(password);
String csrf_token = request.getParameter("_csrf");
System.out.println(csrf_token);
return "ok";
}
}
- 效果测试——修改后报错403,因为已经默认开始CSRF功能,所有涉及数据修改方式的请求都会被拦截
- 可以关闭默认开启的CSRF防御功能/也可以配置Security需要的CSRF Token
- 在configure中加入
http.csrf().disable();
关闭服务 ——强行关闭会面临被攻击的危险
-
针对Form表单数据修改的CSRF Token配置
- Security支持在Form表单中提供一个携带CSRF Token信息的隐藏域,与其他修改数据一起提交
- 上面的测试页面已经注释:
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
,也可以使用Thymeleaf支持的th:action属性th:action="@{/updateUser}"
,该请求会默认携带CSRF Token信息,无须手动添加。
Security管理前端页面
- 除了通过Spring Security对后台增加权限控制,还可以使用Security与Thymeleaf整合实现前端页面管理
-
添加thymeleaf-extras-springsecurity5依赖启动器
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>影视直播厅</title>
</head>
<body>
<h1 align="center">欢迎进入电影网站首页</h1>
<div sec:authorize="isAnonymous()">
<h2 align="center">游客您好,如果想查看电影<a th:href="@{/userLogin}">请登录</a> </h2>
</div>
<div sec:authorize="isAuthenticated()">
<h2 align="center"><span sec:authentication="name" style="color:#007bff"></span>
您好您的用户权限为<span sec:authentication="principal.authorities" style="color:darkkhaki"></span>,您有权看以下电影 </h2>
<form th:action="@{/mylogout}" method="post">
<input th:type="submit" th:value="注销">
</form>
</div>
<hr>
<div sec:authorize="hasRole('common')">
<h3>普通电影</h3>
<ul>
<li><a th:href="@{/detail/common/1}">飞驰人生</a></li>
<li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
</ul>
</div>
<div sec:authorize="hasAuthority('ROLE_vip')">
<h3>VIP专享</h3>
<ul>
<li><a th:href="@{/detail/vip/1}">速度与激情</a></li>
<li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
</ul>
</div>
</body>
</html>
- 测试结果——登录index.html会发现功能太强大了…
至此完成的一个小demo还蛮有成就感