Spring Security 的介绍和简单使用
Spring Security 的介绍
简介
平时我们写 Web 项目,都需要用户登录时验证,以及权限管理之类的操作,以前使用过滤器,拦截器等进行管理,原生代码较多。
所以出现了安全框架以供我们使用,安全框架在 Web 应用的主要功能是:
- 认证
- 授权
使用的较多的安全框架有两个:shiro、Spring Security
Spring Security 是 Spring家族中的一个安全管理框架,相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
两个框架的主要功能相差不大,核心功能依旧是:认证、授权
。
Spring Security 的几个重要类:
- WebSecurityConfigurerAdapter:自定义 Sercurity 策略
- AuthenticationManagerBuilfer:自定义认证策略
- @EnableWebSecurity:开启 WebSecurity 模式
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。
快速使用
使用 Spring Boot 集成 Spring Security 结合 Web 应用。
Web 应用准备:
三个等级,不同等级访问不同的区域。
在 maven 项目中导入 Spring Security 的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
查看 maven 项目的各个依赖版本:Spring Boot:2.2.1,Spring Security:5.2.1
开始使用 Spring Security 实现授权和认证效果:
// 自定义 Spring Security 的策略
// 需要继承 WebSecurityConfigurerAdapter,作用是 --> 启用 Web 安全
@EnableWebSecurity // 该注解包含了 @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 采用的是链式编程
// 授权
// 请求授权的规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 授权请求,那些请求需要授权
.antMatchers("/").permitAll() // 规定首页所有人都能请求
.antMatchers("/level1/**").hasRole("vip1") // /level1/** 请求,只能 vip1 角色可以访问
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
// 没有权限会跳到登录页面:源码默认是 /login
http.formLogin();
}
// 认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加用户至内存中,内存中身份验证
auth.inMemoryAuthentication()
// 创建用户名为:zhangSan,密码为 123,角色是 vip1 和 vip2
.withUser("zhangSan").password("123").roles("vip1","vip2")
.and() // and() 来衔接不同的用户。
// 创建用户名为:root,密码为 root,角色是 vip1、vip2、vip3
.withUser("root").password("root").roles("vip1","vip2","vip3")
// 创建用户名为:user,密码为 123,角色是 vip1
.and()
.withUser("user").password("123").roles("vip1");
// 使用 数据库中的数据进行身份验证
// auth.jdbcAuthentication();
}
}
授权方法中 http.formLogin();
源码的注释。
用户登录界面:Spring Security 默认的登录界面,如有需要可以手动修改路径,http.loginPage("url")
登录成功,但是服务器报错:
此时需要对认证部分的代码每个用户的密码进行加密处理。
// 认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.passwordEncoder() 指定加密方式。Security 5.0+ 提供了很多加密方式
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("zhangSan").password(new BCryptPasswordEncoder().encode("123")).roles("vip1","vip2")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("root")).roles("vip1","vip2","vip3")
.and()
.withUser("user").password(new BCryptPasswordEncoder().encode("123")).roles("vip1");
}
登陆过后:就会按照上诉的定义的规则,Spring Security 帮我们进行授权和认证操作。
- zhangSan:vip1、vip2
- root:vip1、vip2、vip3
- user:vip1
每个用户只能根据自己的角色请求授权的资源(请求)。
注销
加入用户注销效果:
既然是注销,肯定是授权的一些操作了。
// 开启注销功能
http.logout().logoutSuccessUrl("/"); // 规定注销成功后,重定向到 /
上图标识了一些注意事项和例子,在前端只要发起/logout
请求即可将用户注销,在代码中还能清除 Cookie 或 Session 等后续操作。
<a href="/logout">注销</a>
权限控制
通过上面的代码,一个简单的 Web 应用授权和认证就完成了,只是没有跟网页进行联动,可能不同的角色或用户,在同一个页面看到的菜单栏显示不一样,根据角色的权限不同看到的内容也不同。
所以接下来进行对角色进行权限控制。
如果需要网页根据登录用户的角色进行动态显示内容,则需要 Security 和 thymeleaf 的整合包。
整合包与 Spring Boot 的版本有关:
Spring Boot 2.1.0+ 版本,
<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>
html 页面引入
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" <!-- 如果上面的出现不了提示,尝试使用下面这个引入 --> xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
Spring Boot 2.0.9- 版本,
<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> <version>3.0.4.RELEASE</version> </dependency>
html 页面引入
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
此处 Spring Boot 的版本是 2.2.1,导入 thymeleaf-extras-springsecurity5
依赖,在前端页面进行标签设置。
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
已有用户登录才显示注销,反之不显示
<!-- sec:authorize="isAuthenticated()" 用的是整合包中的标签,isAuthenticated 判断是否经过了身份验证,返回 boolean 值。 -->
<div sec:authorize="isAuthenticated()">
<!-- 这里的 /logout 请求的是 security 的注销功能 -->
<a href="/logout">注销</a>
</div>
<!-- 这里的 /login 请求的是 security 的登录功能 -->
<a href="/login">登录</a>
前端具体代码
<body>
<!-- 如果未登录才显示 -->
<div sec:authorize="!isAuthenticated()">
<a href="/login">登录</a>
</div>
<!-- 如果已登录才显示注销 -->
<div sec:authorize="isAuthenticated()">
<a href="/logout">注销</a>
<!-- 显示登录用户和角色 -->
<span sec:authentication="name"></span>
<span>,你好你的角色有:</span>
<span sec:authentication="principal.authorities"></span>
<div class="div1">
<!-- 如果当前用户角色是 vip1,才显示 -->
<div class="div2" sec:authorize="hasRole('vip1')">
<h3>level 1</h3>
<ul>
<li><a th:href="@{level1/1}">level1-1</a></li>
<li><a th:href="@{level1/2}">level1-2</a></li>
<li><a th:href="@{level1/3}">level1-3</a></li>
</ul>
</div>
<!-- 如果当前用户角色是 vip2,才显示 -->
<div class="div2" sec:authorize="hasRole('vip2')">
<h3>level 2</h3>
<ul>
<li><a th:href="@{level2/1}">level2-1</a></li>
<li><a th:href="@{level2/2}">level2-2</a></li>
<li><a th:href="@{level2/3}">level2-3</a></li>
</ul>
</div>
<!-- 如果当前用户角色是 vip3,才显示 -->
<div class="div2" sec:authorize="hasRole('vip3')">
<h3>level 3</h3>
<ul>
<li><a th:href="@{level3/1}">level3-1</a></li>
<li><a th:href="@{level3/2}">level3-2</a></li>
<li><a th:href="@{level3/3}">level3-3</a></li>
</ul>
</div>
</div>
</div>
看到具体的整合包部分标签:
-
sec:authorize="isAuthenticated()"
isAuthenticated 判断是否经过了身份验证,返回 boolean 值。
-
sec:authentication="name"
获取当前用户的用户名。
-
sec:authentication="principal.authorities"
获取当前用户的所有角色。
-
sec:authorize="hasRole('role')"
只有用户角色包括 role 才会显示。
效果截图:
登录 zhangSang 用户:
登录 root 用户:
登录 user 用户:
到此,根据用户的角色动态显示内容功能简单实现。
自定义登录界面和开启 RememberMe 功能
开启 RememberMe 功能。
http.rememberMe();
点击登录就有:
这样就有了 remember me 功能,默认是 14 天时间。
自定义登录界面
login.html
<form th:action="@{/toLogin}" method="post">
<input type="text" name="username" placeholder="用户名:">
<input type="password" name="password" placeholder="密码:">
<input type="checkbox" name="remember"> 记住我
<input type="submit" value="登录">
</form>
<!-- 登录连接跳转 -->
<a th:href="@{/toLogin}">登录</a>
修改登录页面的 url:
http.formLogin().loginPage("/toLogin");
登录成功。
这个项目中并没有实现对 toLogin 的登录验证实现,从头到尾都是 Spring Security 框架帮我们进行登录验证的处理,用户就是在内存中写的用户。所以我们修改了两处地方:
- 准备一个登录界面(login.html)和对于的 Controller (/toLogin),如果是 /login,则会被 Security 拦截直接走默认的 login 页面。
- 修改登录页面的 url:
http.formLogin().loginPage("/toLogin");
- 修改登录界面的表单提交的 action:
/toLogin
按照这个步骤结合我们以前写的用户验证过程,大概原理应该是 Security 根据 loginPage 指定的登录界面进行拦截处理,并且帮我们提取表单参数从而进行一系列操作。
关键是 loginPage
的 url 与 表单提交的 action 保持一致,这样 Security 才能够拦截成功,经过多次尝试,只要 Controller 中有映射,loginPage
就可以指定该映射,效果一样。
那么帮我们进行表单验证的到底是谁呢?如果没有其他设置的话,默认帮我们处理的还是之前的那个页面表单提交的 url。
这里就引出一个问题:自定义的表单提交的参数是不是需要固定?
默认情况下 Security 的表单验证的部分固定参数可以在源码中看到:
- 用户名:username
- 密码:password
- 记住我选项:remember
如果表单的 name 参数不一致,需要在 Security 配置中进行指定:
// 没有权限会跳到登录页面:源码默认是 /login
http.formLogin()
.loginPage("/to")
.loginProcessingUrl("/login")
.usernameParameter("user")
.passwordParameter("pwd");
// 开启 rememberMe 功能
http.rememberMe()
.rememberMeParameter("rem");
那么:loginProcessingUrl()
这一配置作用是什么?顾名思义 login 请求发送到指定 URL。
如果 loginPage
与表单提交action 指定的 URL
不一致,则loginProcessingUrl()
的 URL 与 action
URL
保持一致,这样就可以将表单进行拦截验证。
小结
学习了 Spring Security 的基本使用,以及和 Thymeleaf 进行整合的使用。
- 实现
WebSecurityConfigurerAdapter
类,自定义 Security 的一些安全策略 - 重写两个方法,
configure()
,一个授权,一个认证 - 整合 Thymeleaf 进行前端的设计,根据配置进行权限控制。
- 以及一些整合时的注意事项和细节
其主要通过 AOP 进行授权和认证功能的切入。
而后学习的 Shiro 也跟 Security 有相似之处。