初识SpringSecurity
学习思路
- 了解SpringSecurity是什么。
- 查看官网简介。
- 简单快速阅读官方文档。
经过一段时间的学习,我们知道构建一个SpringBoot项目只需要三步:
- 导入maven依赖。
- 配置相关文件。
- 编写测试代码。
安全框架
在Web开发中,安全一直是一个十分重要的环节。它是一种非功能性的需求,但是对于一个系统十分重要,我们一般都会使用一些组件或者框架去实现。
例如防御跨站脚本攻击,用户名密码验证这些功能,安全框架集成了一些类,可以自动帮我们做这些操作。
安全框架还为我们提供了用户授权功能,不同的用户角色可以操作的功能是不一样的,例如管理员可以拥有对数据增删查改的权限,普通用户只能查看一部分数据。
市面上比较知名的安全框架:
- Shiro,用的十分多,功能十分强大。
-
SpringSecurity,用的也很多,但是功能没Shiro那么强大,但是它可以和Spring无缝结合,十分方便,基本的功能全都有。
SpringSecurity官方文档位置
什么是SpringSecurity?
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
在SpringBoot项目中集成SpringSecurity
导入maven依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
SpringSecurity的基本原理如下图所示。
简单来说,SpringSecurity基本都是通过过滤器来完成配置的身份认证、权限认证以及登出。
SpringSecurity在Servlet的过滤链(FilterChain)中注册了一个过滤器FilterChainProxy,它会把请求代理到SpringSecurity维护的多个过滤链,每个过滤链会匹配一些URL,如果匹配则执行对应的过滤器。过滤链是有顺序的,一个请求只会执行第一条匹配的过滤链。
Spring Security 的配置本质上就是新增、删除、修改过滤器。
Http相关的配置,主要在HttpSecurity类中,在官方文档中有详细的说明。Http配置和密码验证
用户密码验证的相关方法如下图所示。
认证和授权
先来简单说说认证和授权的区别:
- 认证(authentication),是验证一个特定操作的过程,这个操作通常情况下就是“登录”,需要操作者提供一些信息,认证提供方通过这些信息判断这个操作的来源确实是真正的操作者。用三个字概括就是“你是谁?”
- 授权(authorization), 是授予某个用户、功能、节点、终端等一些特点的权限或者策略。从一个完整的权限管理系统包括操作、策略、角色、用户、用户组等。用五个字概括就是“你能干什么?”
编写代码
1、建立SpringBoot项目,导入SpringSecurity依赖。
2、导入前端资源。为了方便测试权限控制创建了三个不同等级的目录,里面分别放入了三个相同的html文件。
为了实现不同的用户拥有不同的功能,我们的首页设计如下图所示。
3、编写路由Controller。
@Controller
public class RouterController {
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "views/login";
}
//通过restful实现复用
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") int id){
return "views/level1/"+id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") int id){
return "views/level2/"+id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") int id){
return "views/level3/"+id;
}
}
4、编写SpringSecurity配置类,继承WebSecurityConfigurerAdapter并在类上添加注解@EnableWebSecurity
即可。
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
5、启动项目,发现页面自动跳转到了登录页面。SpringSecurity默认提供的用户名是user
,密码则在启动项目时在控制台中输出了。
6、在配置类中重写以下方法,配置基本的请求过滤,对不同的请求定制授权规则。
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置不同的人访问内容不同,这里可以配置过滤器、登录注销规则、安全配置、OAuth2配置
//我们平时只需要配置一些基本的规则即可
//首页允许所有人访问
http.authorizeRequests()
//定制授权规则:哪些请求,哪些人可以访问
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
}
7、配置表单登录。当用户没有登录时,自动跳转到登录页面。
//登录 /login跳转到登录页 /login?error 登录失败
//如果没有登录自动跳转到登录页面
http.formLogin()
.usernameParameter("username")//配置用户名参数
.passwordParameter("password")//配置密码参数
.loginPage("/toLogin")//登录跳转页面请求
.loginProcessingUrl("/login");//登录表单提交请求
8、我们来配置一些登录的用户进行测试。配置方法在源码中也有注释说明。
//定义用户的认证规则(角色,密码...)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//内存中定义用户信息
//我们只定义了用户的密码加密,但是没有定义用户的认证规则的加密方式
auth.inMemoryAuthentication()
//使用的加密方式要对应下面密码的加密方式
.passwordEncoder(new BCryptPasswordEncoder())
//一个人可以拥有多个角色
.withUser("coding")
//密码的加密
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip1","vip2")
.and()
.withUser("wunian")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip1","vip2","vip3")
.and()
.withUser("guest")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip1");
}
上述代码配置了三个用户的角色,并且对密码进行了加密处理,不同的角色能够访问的请求不一样,这个在步骤5中已经配置过了。
9、密码加密接口PasswordEncoder拥有很多的实现类,不同实现类对应了不同的加密方法,SpringSecurity推荐我们使用BCryptPasswordEncoder进行加密。
10、注销功能实现:
- 配置前端注销按钮,注意这里应该使用post提交,否则点击注销按钮可能会出现404错误,因为SpringSecurity默认是防止CSRF 跨站伪请求的。
<!-- 将注销请求也改成post提交即可! -->
<form th:action="@{/logout}" method="post">
<button type="submit">注销</button>
<!--<a class="item" th:href="@{/logout}">-->
<!--<i class="address card icon"></i> -->
<!--</a>-->
</form>
也可以在配置类的configure(HttpSecurity http)方法中配置如下代码禁用防止CSRF跨站伪请求,但是不建议这样做,因为这会使得我们的系统不安全。
http.csrf().disable();
实际上SpringSecurity已经帮我们将全部的用户退出的规则都定义好了,在前端输入/logout请求会弹出如下代码提示。
- 在配置类的configure(HttpSecurity http)方法中配置登出成功的页面。
//注销 开启默认注销功能
http.logout().logoutSuccessUrl("/");//注销成功后跳转至首页
- 重启项目测试登录和退出功能。
登录页定制记住我功能
功能需求
用户没登录时首页只显示导航栏,如果登录了,就显示该用户所拥有的角色权限能够看到的内容,即根据不同角色权限,前端显示不同的功能界面。
我们需要把SpringSecurity和Thymeleaf结合起来使用。
编写代码
1、导入Thymeleaf和SpringSecurity的整合包,版本一定要和SpringSecurity对应。
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
版本对应关系如下:
thymeleaf-extras-springsecurity3 for integration with Spring Security 3.x
thymeleaf-extras-springsecurity4 for integration with Spring Security 4.x
thymeleaf-extras-springsecurity5 for integration with Spring Security 5.x
2、修改前端页面。
- 导入命名空间约束,如果使用IDEA导入不生效,可以尝试重启。
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
...
</html>
- 修改前端页面。注意以下几个属性:
sec:authorize="isAuthenticated()":判断用户是否登录。
sec:authentication="principal.username":获取当前登录用户的用户名。
sec:authentication="principal.authorities":获取当前登录用户的所有角色。
sec:authorize="hasRole('xxx')":判断当前登录用户是否含有某个角色权限。
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"><!--导入springsecurity5命名空间-->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>首页</title>
<!--semantic-ui-->
<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
<link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet">
</head>
<body>
<!--主容器-->
<div class="ui container">
<div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
<div class="ui secondary menu">
<a class="item" th:href="@{/index}">首页</a>
<!--登录注销-->
<div class="right menu">
<!--核心类:Authentication-->
<!-- 如果未登录就显示登陆按钮 -->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
<!-- 如果已登录,显示用户的信息 -->
<div sec:authorize="isAuthenticated()">
<a class="item">
<!--<i class="address card icon"></i>-->
用户名:<span sec:authentication="principal.username"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
</div>
<div sec:authorize="isAuthenticated()">
<!-- 将注销请求也改成post提交即可! -->
<form th:action="@{/logout}" method="post">
<button type="submit">注销</button>
<!--<a class="item" th:href="@{/logout}">-->
<!--<i class="address card icon"></i> -->
<!--</a>-->
</form>
</div>
</div>
</div>
</div>
<div class="ui segment" style="text-align: center">
<h3>Spring Security Study by 秦疆</h3>
</div>
<div>
<br>
<div class="ui three column stackable grid">
<div sec:authorize="hasRole('vip1')">
<div class="column">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>
</div>
<div sec:authorize="hasRole('vip2')">
<div class="column">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
</div>
</div>
</div>
</div>
<div sec:authorize="hasRole('vip3')">
<div class="column">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 3</h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/qinjiang/js/semantic.min.js}"></script>
</body>
</html>
3、记住我功能实现:
- 一旦关闭浏览器,用户Session就没有了,我们需要实现关闭后用户还可以直接登录,通过在浏览器添加Cookie来实现。
- 前端配置,添加一个复选框按钮,name设置为"remember"。
<input type="checkbox" name="remember"> 记住我
- 后台配置,在配置类configure(HttpSecurity http)方法中配置rememberMe参数为"remember"。
//记住我功能
//自定义的登录页需要配置rememberMe的参数名,这样就可以绑定到前端
http.rememberMe().rememberMeParameter("remember");
4、首页定制,在配置类configure(HttpSecurity http)方法中添加如下配置即可。
http.formLogin()
.usernameParameter("username") // 前端用户名提交参数
.passwordParameter("password") // 前端密码提交参数
.loginPage("/toLogin") //登录跳转页面请求
.loginProcessingUrl("/login"); // 登陆表单提交请求