一、概述
- 在 Web 开发中,安全一直是非常重要的一个方面;
- 安全虽然属于应用的 非功能性 需求,但是应该在 应用开发的初期就考虑进来;
- 如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:
- 应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据,被攻击者窃取;
- 应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构,做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程;
- 在应用开发的第一天,就应该把安全相关的因素考虑进来,并在整个应用的开发过程中;
- 市面上,比较有名的:Shiro、Spring Security;
二、Spring Security
2.1 简介
- 官网地址
- Spring Security 是一个功能强大,且高度可定制的,身份验证和访问控制框架,它实际上是保护基于 spring 的应用程序的标准;
- Spring Security 是一个框架,侧重于为 Java 应用程序提供身份验证和授权,与所有 Spring 项目一样,Spring 安全性的真正强大之处,在于它可以轻松地扩展以满足定制需求;
- 从官网介绍中可知,这是一个 权限框架,之前做项目,没有使用框架控制权限的方式:
- 把权限细分为功能权限,访问权限,和菜单权限, 这样做,代码会非常的繁琐,冗余;
- Spring Security 就是解决之前写权限代码繁琐,冗余问题的安全框架;
- Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案;
- Web 应用的安全性包括两个部分:
- 用户认证(Authentication):
- 指的是验证某个用户,是否为系统中的合法主体,也就是说,用户能否访问该系统;
- 一般要求用户,提供用户名和密码,系统通过校验用户名和密码,来完成认证过程;
- 用户授权(Authorization):
- 指的是验证某个用户,是否有权限执行某个操作;
- 在一个系统中,不同用户所具有的权限是不同的,比如一个文件,有的用户只能进行读取,有的用户可以进行修改;
- 系统会为不同的用户,分配不同的角色,而每个角色,对应一系列的权限;
- 用户认证(Authentication):
- Spring Security 框架:
- 用户认证方面,支持主流的认证方式,包括:
- HTTP 基本认证;
- HTTP 表单验证;
- HTTP 摘要认证;
- OpenID;
- LDAP 等;
- 在用户授权方面:提供了基于角色的访问控制,和访问控制列表(Access Control List,ACL),可以对应用中的领域对象,进行细粒度的控制;
- 用户认证方面,支持主流的认证方式,包括:
2.2 搭建测试环境
-
新建 Spring Boot 项目:添加 web 和 thymeleaf 依赖;
-
导入静态资源:下载链接
-
关闭模板引擎缓存:
application.properties
# 关闭模板引擎的缓存
spring.thymeleaf.cache=false
- 新建 controller 目录,创建视图跳转的 Controller:RouterController
@Controller
public class RouterController {
@RequestMapping({"/", "/index"})
public String index() {
return "index";
}
@RequestMapping("/toLogin")
public String toLogin() {
return "views/login";
}
@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;
}
}
-
运行测试:
2.3 认识 Spring Security
- Spring Security 是针对 Spring 项目的 安全框架,也是 Spring Boot 底层安全模块,默认的技术选型,可以实现强大的 Web 安全控制;
- 对于安全控制,仅需要引入
spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理; - 相关的类:
- WebSecurityConfigurerAdapter:自定义 Security 策略(适配器模式);
- AuthenticationManagerBuilder:自定义认证策略(建造者模式);
@EnableWebSecurity:开启 WebSecurity 模式,@Enablexxx开启某个功能;
- Spring Security 的两个主要目标(访问控制):
- 认证(Authentication):
- 身份验证是关于验证凭据,如用户名、用户 ID 和密码,以验证身份。
- 身份验证,通常通过用户名和密码完成,有时与身份验证因素结合使用;
- 授权(Authorization):
- 授权发生在,系统成功验证身份后,最终会授予用户访问资源(如信息、文件、数据库、资金、位置、几乎任何内容)的完全权限;
- 认证(Authentication):
- 认证和授权的概念是通用的,而不是只在 Spring Security 中存在;
- Spring Security 采用 AOP 的方式;
2.4 认证和授权
- 导入 Spring Security 依赖:
<!--Spring Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置 Spring Security
@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.apply(customDsl())
.flag(true)
.and()
...;
}
}
- 新建目录 config,创建配置类:SecurityConfig
// AOP方式拦截器
// 开启WebSecurity模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 链式编程
// 定义授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 定制请求的授权规则,首页所有人可以访问,功能页对应权限的用户可以访问
http.authorizeRequests()
// permitAll 所有用户可以访问
.antMatchers("/").permitAll()
// hasRole:限定访问角色
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
}
}
-
运行测试:发现除了首页,其它页面,都进不去了,因为目前没有登录的角色,请求需要登录的角色,拥有对应的权限才可访问;
-
在
configure()方法中,加入以下配置,开启自动配置的登录功能:
// 开启自动配置的登录功能
// /login请求:到登录页面
// /login?error:重定向到这里表示登录失败
http.formLogin();
-
运行测试:没有权限时,会跳转到登录页面;
-
通过查看
formLogin()源码得出,没有权限,默认是自动跳转到login页面,如果认证失败,重定向到login?error; -
通过查看源码,自定义认证规则,需要重写
configure(AuthenticationManagerBuilder auth)方法:
// 定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这些数据,正常应该从数据库中读取
// auth.jdbcAuthentication():jdbc中去读取
// 在内存中读取
auth.inMemoryAuthentication()
// withUser:用户名 roles:角色,可多个
.withUser("test").password("123456").roles("vip2", "vip3")
// and:拼接
.and()
.withUser("root").password("123456").roles("vip1", "vip2", "vip3")
.and()
.withUser("guest").password("123456").roles("vip1");
}
-
重启,运行测试,使用账号登录,发现报错:
-
原因,要将前端传过来的密码,进行某种方式加密,否则就无法登录,修改代码:
// 定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这些数据,正常应该从数据库中读取
// auth.jdbcAuthentication():jdbc中去读取
// 在内存中读取
// Spring security 5.0中,新增了多种加密方式,也改变了密码的格式
// 需要将前端传过来的密码,进行某种方式加密,官方推荐:bcrypt 加密方式
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
// withUser:用户名 roles:角色,可多个
.withUser("test").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
// and:拼接
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
- 运行测试,登录成功,并且每个角色,只能访问自己认证下的规则;
2.5 权限控制和注销
- 开启自动配置的注销功能:
// 定义授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
// 开启自动配置的注销的功能
// /logout 注销请求
http.logout();
}
- 在前端页面,增加注销按钮:
index.html
<!--注销-->
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
-
运行测试:登录后,注销完成,会跳转到登录页面;
-
一般程序注销后,会跳转到首页,修改注销的代码:
// 开启自动配置的注销的功能
// /logout 注销请求
// .logoutSuccessUrl("/"):注销成功,跳转到首页
http.logout().logoutSuccessUrl("/");
增加需求:
- 用户未登录时,导航栏只显示登录按钮;
- 用户登录后,导航栏显示登录的用户信息及注销按钮;
- 按权限展示不同页面,如:只有 vip2、vip3 功能,登录后,只显示这两个功能,vip1 的功能模块不显示;
实现:
- 结合 thymeleaf 中的一些功能,显示不同的页面:
<!--是否认证登录-->
sec:authorize="isAuthenticated()"
- 导入 thymeleaf 与 Spring Security 的整合依赖:
<!--thymeleaf springsecurity5 整合-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.1.0.M1</version>
</dependency>
- 修改前端页面:导入命名空间
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
- 修改导航栏,增加认证判断:
<!--登录注销-->
<div class="right menu">
<!--未登录:显示登录-->
<!--isAuthenticated():验证是否登录-->
<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">
<!--用户名:<span th:text="${#authentication.name}"></span>-->
用户名:<span sec:authentication="name"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
</div>
<div sec:authorize="isAuthenticated()">
<!--注销-->
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
sec:authorize属性说明:
// 用户为游客则显示
sec:authorize="isAnonymous()"
// 用户通过验证则显示
sec:authorize="isAuthenticated()"
// 用户为vip角色则显示
sec:authorize="hasRole('vip')"
// 用户为ROLE_vip权限则显示
sec:authorize="hasAuthority('ROLE_vip')"
-
重启,运行测试:
跨站域请求
- 开启 csrf 跨站域防御后,需要以 post 的方式提交,Spring Security 默认是开启状态(提高网站安全性);
- 如注销登录时报错,可在配置文件中关闭 csrf:
// 关闭跨站域请求,默认为开启状态,只能以post方式提交
http.csrf().disable();
// 开启自动配置的注销的功能
// /logout 注销请求
// .logoutSuccessUrl("/"):注销成功,跳转到首页
http.logout().logoutSuccessUrl("/");
- 修改首页,按不同权限,展示不同页面:
index.html
<div class="ui three column stackable grid">
<!--根据用户角色,动态展示内容-->
<!--hasRole('vip1'):有vip1权限时显示-->
<div class="column" sec:authorize="hasRole('vip1')">
<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 class="column" sec:authorize="hasRole('vip2')">
<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 class="column" sec:authorize="hasRole('vip3')">
<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>
-
运行测试:未登录状态;
-
按权限展示:
2.6 记住我功能
- 登录后,关闭浏览器,再次进入时,需要重新登录,但很多网站,有一个记住密码的功能,可以不用重新登录;
- 在配置文件中,开启
记住我功能:
// 定义授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
// 开启记住我功能
http.rememberMe();
}
-
运行测试:
-
登录后,关闭浏览器,然后重新打开浏览器访问,发现用户依旧存在;
分析实现方式
-
查看浏览器的 cookie:
-
在 cookie 中设置了值,默认保留 14 天;
-
点击注销时,Spring Security 会自动删除这个 cookie:
-
结论:
- 登录成功后,将 cookie 发送给浏览器保存,以后登录带上这个 cookie,只要通过检查就可以实现免登录;
- 点击注销时,则会删除这个 cookie;
2.7 定制登录页
-
Spring Security 使用的登录页面是默认的,如果需要使用自定义的界面,需要进行配置;
-
在登录页配置后面,指定自定义的登录请求:
// 自定义登录请求
http.formLogin().loginPage("/toLogin");
- 前端页面,也需要指向自定义的登录请求:
index.html
<!--请求路径要和Spring Security自定义登录请求一致-->
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
-
登录时,需要将这些信息发送到哪里,也需要进行配置,提交方式必须为 post,
loginPage()源码中的注释上有写明: -
前端页面,配置提交请求及方式:
login.html
<!--<form th:action="@{/toLogin}" method="post">-->
<!--提交请求:与Spring Security自定义配置的loginProcessingUrl请求一致-->
<form th:action="@{/login}" method="post">
<div class="field">
<label>Username</label>
<div class="ui left icon input">
<input type="text" placeholder="Username" name="username">
<i class="user icon"></i>
</div>
</div>
<div class="field">
<label>Password</label>
<div class="ui left icon input">
<input type="password" name="password">
<i class="lock icon"></i>
</div>
</div>
<input type="submit" class="ui blue submit button"/>
</form>
- 这个请求提交上来,需要验证处理,查看
formLogin()源码,需要配置接收登录的用户名和密码的参数:
// 自定义登录请求
http.formLogin()
// username:默认属性名,与前端的name属性名对应,相同时可省略
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
// 登陆表单的提交请求,与上面loginPage请求相同时,可省略
.loginProcessingUrl("/login");
- 在登录页增加
记住我的多选框:login.html
<div class="field">
<input type="checkbox" name="remember"> 记住我
</div>
- 在 Spring Security 配置文件中,增加验证处理:
// rememberMe:开启记住我功能
// rememberMeParameter:配置记住我的参数,与前端对应
http.rememberMe().rememberMeParameter("remember");
- 完整配置代码:
// AOP方式拦截器
// 开启WebSecurity模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 链式编程
// 定义授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 定制请求的授权规则,首页所有人可以访问,功能页对应权限的用户可以访问
http.authorizeRequests()
// permitAll 所有用户可以访问
.antMatchers("/").permitAll()
// hasRole:限定访问角色
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
// 开启自动配置的登录功能
// /login请求:到登录页面
// /login?error:重定向到这里表示登录失败
// http.formLogin();
// 自定义登录请求
http.formLogin()
// username:默认属性名,与前端的name属性名对应,相同时可省略
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
// 登陆表单的提交请求,与上面loginPage请求相同时,可省略
.loginProcessingUrl("/login");
// 关闭跨站域请求,默认为开启状态,只能以post方式提交
http.csrf().disable();
// 开启自动配置的注销的功能
// /logout 注销请求
// .logoutSuccessUrl("/"):注销成功,跳转到首页
http.logout().logoutSuccessUrl("/");
// rememberMe:开启记住我功能
// rememberMeParameter:配置记住我的参数
http.rememberMe().rememberMeParameter("remember");
}
// 定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这些数据,正常应该从数据库中读取
// auth.jdbcAuthentication():jdbc中去读取
// 在内存中读取
// Spring security 5.0中,新增了多种加密方式,也改变了密码的格式
// 需要将前端传过来的密码,进行某种方式加密,官方推荐:bcrypt 加密方式
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
// withUser:用户名 roles:角色,可多个
.withUser("test").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
// and:拼接
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}
三、Shiro
3.1 Shiro 简介
- 官网地址
- Apache Shiro 是一个 Java 的安全(权限)框架;
- Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE环境,也可以用在 JavaEE 环 境;
- Shiro 可以完成:认证、授权、加密、会话管理、Web 集成、缓存等;
3.2 功能介绍
-
具体功能:
-
Authentication:身份认证、登录,验证用户是不是拥有相应的身份;
-
Authorization:授权,即权限验证,验证某个已认证的用户,是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户,对某个资源是否具有某个权限;
-
Session Manager:会话管理,即用户登录后,就是第一次会话,在没有退出之前,它的所有信息都在会话中,会话可以是普通的JavaSE 环境,也可以是 Web 环境;
-
Cryptography:加密,保护数据的安全性,如:密码加密存储到数据库中,而不是明文存储;
-
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
-
Caching:缓存,比如,用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率;
-
Concurrency:Shiro 支持多线程应用的并发验证,如:在一个线程中,开启另一个线程,能把权限自动的传播过去;
-
Testing:提供测试支持;
-
Run As:允许一个用户,假装为另一个用户(如果他们允许)的身份进行访问;
-
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了;
3.3 Shiro 架构
外部架构
-
从外部来看 Shiro,即从应用程序角度,来观察如何使用 shiro 完成工作:
-
Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject:
- Subject 代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西,都是 Subject,如:网络爬虫,机器人等;
- 与 Subject 的所有交互都会委托给 SecurityManager;
- Subject 其实是一个门面,SecurityManageer 才是实际的执行者;
-
SecurityManager:安全管理器,即所有与安全有关的操作,都会与 SercurityManager 交互,并且它管理着所有的 Subject:
- 它是 Shiro 的核心,负责与 Shiro 的其他组件进行交互;
- 相当于 SpringMVC 的 DispatcherServlet 的角色;
-
Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限):
- SecurityManager 要验证用户身份,需要从 Realm 获取相应的用户进行比较,来确定用户的身份是否合法;
- 也需要从 Realm 得到用户相应的角色、权限,进行验证用户的操作,是否能够进行,可以把 Realm 看成 DataSource;
内部架构
-
内架构图:
-
Subject:任何可以与应用交互的 用户;
-
Security Manager:相当于 SpringMVC 中的 DispatcherServlet:
- 是 Shiro 的心脏,所有具体的交互都通过 Security Manager 进行控制,它管理者所有的 Subject;
- 负责进行认证、授权、会话、及缓存的管理;
-
Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现:
- 可以使用认证策略(Authentication Strategy)即什么情况下,用户认证通过了;
-
Authorizer:授权器,即访问控制器,用来决定主体,是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能;
-
Realm:可以有一个或者多个的 realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用 JDBC 实现,也可以是内存实现等等,由用户提供,所以一般在应用中,都需要实现自己的 realm;
-
SessionManager:管理 Session 生命周期的组件,而 Shiro 并不仅仅可以用在 Web 环境,也可以用在普通的 JavaSE 环境中;
-
CacheManager:缓存控制器,来管理,如:用户、角色、权限等缓存的,因为这些数据基本上很少改变,放到缓存中后,可以提高访问的性能;
-
Cryptography:密码模块,Shiro 提高了一些常见的加密组件,用于密码加密,解密等;
3.4 Shiro 快速实践
- 官方文档
- 官网 Quickstart
- 创建 Maven 父工程,删掉不必要的内容;
- 创建一个普通的 Maven 子工程:
hello-shiro - 根据官方文档,导入 Shiro 所需的依赖:
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
<!-- configure logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.0-alpha7</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
创建 Shiro 配置:
log4j2.xml:复制示例文档
<Configuration name="ConfigTest" status="ERROR" monitorInterval="5">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="org.springframework" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="org.apache" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="net.sf.ehcache" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="org.apache.shiro.util.ThreadContext" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
shiro.ini:复制示例文档
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
- 创建测试类:
Quickstart复制示例文档
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple Quickstart application showing how to use Shiro's API.
*
* @since 0.9 RC2
*/
public class Quickstart {
// 获取log对象
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// 原文件代码:过时
// Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// SecurityManager securityManager = factory.getInstance();
// 更换代码:
// 获取SecurityManager对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 获取配置文件,进行初始化
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);
// 设置SecurityManager对象
SecurityUtils.setSecurityManager(securityManager);
// 重点代码:
// 1. 获取当前的用户对象:Subject
Subject currentUser = SecurityUtils.getSubject();
// 2. 获取shiro中的Session对象
Session session = currentUser.getSession();
// 设置Session的值
session.setAttribute("someKey", "aValue");
// 从session中获取值
String value = (String) session.getAttribute("someKey");
// 判断session中是否存在这个值
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
// 3. 用户认证功能
// isAuthenticated():是否认证
if (!currentUser.isAuthenticated()) {
// 将用户名和密码封装为 UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
// 记住我功能
token.setRememberMe(true);
try {
// 执行登录,可以登录成功的
currentUser.login(token);
// 如果没有指定的用户,则UnknownAccountException异常
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
// 账户存在, 密码不对异常
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
// 用户锁定异常 LockedAccountException
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// 认证异常,上面的异常都是它的子类
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
// 打印标识的主体(在本例中为用户名)
// currentUser.getPrincipal():获得当前用户的认证
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
// 4. 角色检查
// 测试用户是否拥有某角色
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
// 5. 权限检查,粗粒度
// 测试用户是否具有某一个权限,行为
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
// 6. 权限检查,细粒度
// 测试用户是否具有某一个权限,行为,比上面更加具体
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
// 用户注销
currentUser.logout();
// 退出系统
System.exit(0);
}
}
-
运行测试:显示日志消息
-
重要方法:
// 1. 获取当前的用户对象:Subject
Subject currentUser = SecurityUtils.getSubject();
// 2. 获取shiro中的Session对象
Session session = currentUser.getSession();
// 3.是否认证
currentUser.isAuthenticated()
// 4.获得当前用户的认证
currentUser.getPrincipal()
// 5.角色检查
currentUser.hasRole("schwartz")
// 6.权限检查
currentUser.isPermitted("lightsaber:wield")
// 7.用户注销
currentUser.logout();
3.5 Spring Boot 整合 Shiro(原生的整合)
环境搭建
- 在之前的 Maven 项目中,新建 Spring Boot 模块:选中 web 和 thymeleaf,会自动添加相关依赖;
- 新建 controller 目录,并创建测试类:MyController
@Controller
public class MyController {
@GetMapping({"/", "/index"})
public String toIndex(Model model) {
model.addAttribute("msg", "hello shiro");
return "index";
}
}
- 在 templates 目录下,创建静态页面:
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h2>首页</h2>
<p th:text="${msg}"></p>
</body>
</html>
-
运行测试:
整合 Shiro
- 回顾核心 API:
- Subject:用户主体;
- SecurityManager:安全管理器;
- Realm:Shiro 连接数据;
- 导入 Shiro 和 Spring 的整合依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.9.0</version>
</dependency>
- 新建 config 目录,创建自定义 Shiro 配置类:ShiroConfig
// 声明为配置类
@Configuration
public class ShiroConfig {
// 创建 ShiroFilterFactoryBean
// 创建 DefaultWebSecurityManager
// 创建 realm 对象
}
- 按照从底层创建的思路,需要先创建 realm 对象;
- 自定义一个 realm 的类,用来编写一些查询的方法,或者认证与授权的逻辑:UserRealm
// 自定义Realm:必须继承AuthorizingRealm类,实现该类的两个方法
public class UserRealm extends AuthorizingRealm {
//执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
return null;
}
//执行认证逻辑
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证逻辑AuthenticationToken");
return null;
}
}
- 将 UserRealm 注册到 Bean 中:ShiroConfig
// 声明为配置类
@Configuration
public class ShiroConfig {
// 创建 ShiroFilterFactoryBean
// 创建 DefaultWebSecurityManager
// 创建 realm 对象
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
- 创建 DefaultWebSecurityManager:ShiroConfig
// 创建 DefaultWebSecurityManager
@Bean(name = "securityManager")
// @Qualifier("userRealm"):绑定userRealm
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联Realm
securityManager.setRealm(userRealm);
return securityManager;
}
- 创建 ShiroFilterFactoryBean:ShiroConfig
// 创建 ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
页面拦截实现
- 在 templates 目录下,新建 user 目录,创建两个页面;
add.html:
<body>
<h2>add</h2>
</body>
update.html:
<body>
<h2>update</h2>
</body>
- 修改 controller,增加请求:MyController
// add
@RequestMapping("/user/add")
public String toAdd() {
return "user/add";
}
// update
@RequestMapping("/user/update")
public String toUpdate() {
return "user/update";
}
- 在 index 页面上,增加跳转链接:
<p>
<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
</p>
-
运行测试,看页面跳转是否成功;
-
添加 Shiro 的内置过滤器:
// 创建 ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
/*
添加Shiro内置过滤器,常用的有如下过滤器:
anon:无需认证就可以访问
authc:必须认证才可以访问
user:如果使用了记住我功能就可以直接访问
perms:拥有某个资源权限才可以访问
role:拥有某个角色权限才可以访问
*/
// 拦截
Map<String, String> filterMap = new LinkedHashMap<String, String>();
filterMap.put("/user/add", "authc");
filterMap.put("/user/update", "authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
-
运行测试:可以实现拦截,点击后,会跳转到
login.jsp页面; -
自定义 login 页面:
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>登录页面</h1>
<form action="">
<p>
用户名:<input type="text" name="username">
</p>
<p>
密码:<input type="text" name="password">
</p>
<p>
<input type="submit">
</p>
</form>
</body>
</html>
- 添加跳转的 controller:
// 跳转到登录页面
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
- 在 ShiroFilterFactoryBean 中配置,添加跳转页面:
// 设置登录的请求
bean.setLoginUrl("/toLogin");
- 优化代码,拦截内容,替换为通配符:
// 拦截
Map<String, String> filterMap = new LinkedHashMap<String, String>();
// filterMap.put("/user/add", "authc");
// filterMap.put("/user/update", "authc");
filterMap.put("/user/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);
// 设置登录的请求
bean.setLoginUrl("/toLogin");
-
再次运行测试:
登录认证操作
- 添加登录的 controller:
// 登录操作
@PostMapping("/login")
public String login(String username, String password, Model model) {
// 使用shiro,编写认证操作
// 1.获取Subject
Subject subject = SecurityUtils.getSubject();
// 2.封装用户的数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 3.执行登录的方法,只要没有异常就代表登录成功
try {
// 登录成功,返回首页
subject.login(token);
return "index";
// 用户名不存在
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户名不存在");
return "login";
// 密码错误
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg", "密码错误");
return "login";
}
}
- 修改前端登录页面:
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>登录页面</h1>
<!--返回后端的信息提示-->
<p style="color:red;" th:text="${msg}"></p>
<!--增加提交地址-->
<form th:action="@{/login}">
<p>
用户名:<input type="text" name="username">
</p>
<p>
密码:<input type="text" name="password">
</p>
<p>
<input type="submit">
</p>
</form>
</body>
</html>
-
再次运行测试:提交表单,经过了 UserRealm 的认证逻辑方法;
-
在 UserRealm 中,编写用户认证的判断逻辑:
// 执行认证逻辑
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证逻辑AuthenticationToken");
// 模拟数据库获取用户名和密码
String name = "root";
String password = "123456";
// 将参数token进行类型转换
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// 1.判断用户名
// 用户名不存在
if (!userToken.getUsername().equals(name)) {
// shiro底层会抛出 UnknownAccountException
return null;
}
// 2.验证密码,由Shiro去操作
// 可以使用一个AuthenticationInfo实现类SimpleAuthenticationInfo
// shiro会自动验证,重点是第二个参数,是要验证的密码
return new SimpleAuthenticationInfo("", password, "");
}
- 运行测试:成功实现,登录的认证操作;
3.6 Shiro 整合数据库
- 导入 Mybatis 等相关依赖:
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
<!--mybatis-spring-boot-starter:非spring官方,作用整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--Log4j2 日志-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.6.7</version>
</dependency>
- 创建配置文件-连接配置:
application.yaml
# MySQL - 8.0.28
spring:
datasource:
username: 数据库用户名
password: 数据库密码
# serverTimezone=UTC解决时区报错
url: jdbc:mysql://localhost:3306/数据库名?useUnicode=true&characterEncoding=utf-8&useSSL=true
driver-class-name: com.mysql.cj.jdbc.Driver
# 自定义数据源 druid
type: com.alibaba.druid.pool.DruidDataSource
# Spring Boot 默认是不注入这些属性值的,需要自定义配置类,进行绑定
# druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的 filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
# 如果运行时报错:java.lang.ClassNotFoundException: org.apache.log4j.Priority
# 则导入 log4j 依赖即可,如使用log4j2,对应导入相应依赖
filters: stat,wall,log4j2
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
- 编写 mybatis 的配置:
application.yaml
# 整合 MyBatis
mybatis:
# 对应实体类的路径
type-aliases-package: com.study.pojo
# 指定myBatis的核心配置文件与Mapper映射文件,classpath:后面不需要加 /
mapper-locations: classpath:mapper/*.xml
# 开启驼峰命名规范自动映射
configuration:
map-underscore-to-camel-case: true
- 或
application.properties
# 别名配置
mybatis.type-aliases-package=com.study.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
# 开启驼峰命名规范自动映射
mybatis.configuration.map-underscore-to-camel-case=true
- 新建 pojo 目录,创建实体类:User
// 需要引入Lombok依赖
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
- 新建 mapper 目录,创建 Mapper 接口:UserMapper
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
- 在 resources 下新建目录,创建 Mapper 配置文件:
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.study.mapper.UserMapper">
<!--查询用户-->
<select id="queryUserByName" parameterType="String" resultType="User">
select *
from `user`
where name = #{name};
</select>
</mapper>
- 创建 Service 层:新建 service 目录,创建 UserService 接口;
public interface UserService {
public User queryUserByName(String name);
}
- 创建 UserService 的实现类:UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
// 调用Mapper
@Autowired
UserMapper userMapper;
@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
}
- 在测试类中进行测试,看能否正常读取数据:
@SpringBootTest
class SpringbootShiroApplicationTests {
@Autowired
UserServiceImpl userService;
@Test
void contextLoads() {
System.out.println(userService.queryUserByName("李四"));
}
}
- 修改 UserRealm,连接到数据库,进行真实的操作:
// 自定义Realm:必须继承AuthorizingRealm类,实现该类的两个方法
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
// 执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
return null;
}
// 执行认证逻辑
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证逻辑AuthenticationToken");
// 将参数token进行类型转换
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// 连接数据库获取用户
User user = userService.queryUserByName(userToken.getUsername());
// 用户名不存在
if (user == null) {
// shiro底层会抛出 UnknownAccountException
return null;
}
// 验证密码,由Shiro去操作
// shiro会自动验证,重点是第二个参数,是要验证的密码
// 密码可以加密:MD5、MD5盐值加密
return new SimpleAuthenticationInfo("", user.getPwd(), "");
}
}
- 运行测试;
3.7 密码比对分析
-
Shiro,是怎么实现密码自动比对的,可以去 realm 的父类 AuthorizingRealm 的父类查看;
-
AuthenticatingRealm 中查找:
CredentialsMatcher获取证书匹配器; -
接口 CredentialsMatcher 有很多的实现类,MD5 盐值加密:
-
密码一般都不能使用明文保存,需要加密处理;
-
思路分析:
- 如何把一个字符串加密为 MD5;
- 替换当前的 Realm 的 CredentialsMatcher 属性,直接使用 Md5CredentialsMatcher 对象,并设置加密算法;
3.8 用户授权操作
- 使用 shiro 的过滤器,来拦截请求即可;
- 在 ShiroFilterFactoryBean 中添加一个过滤器:
// 授权过滤器:注意顺序 perms[user:add]:权限标识
filterMap.put("/user/add", "perms[user:add]");
-
运行测试,访问 add,发现以下错误,未授权错误:
-
实现权限拦截后,Shiro 会自动跳转到未授权的页面,因为没有这个页面,所以报 401 了;
-
配置未授权的提示的页面:增加一个 controller 提示;
// 未授权页面
@RequestMapping("/noauth")
@ResponseBody
public String noAuth(){
return "未经授权,不能访问此页面";
}
- 需要在 ShiroFilterFactoryBean 中,配置未授权的请求页面:
// 设置未授权的页面
bean.setUnauthorizedUrl("/noauth");
-
运行测试:
Shiro 授权
- 在 UserRealm 中添加授权的逻辑,增加授权的字符串:
// 执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
// 给资源进行授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 添加资源的授权字符串
info.addStringPermission("user:add");
// 注意:需要返回 info
return info;
}
-
再次登录测试,发现登录的用户,可以访问 add 页面,授权成功;
-
真实的业务情况:每个用户拥有自己的权限,从而进行操作,所以,权限应该在用户的数据库中;
-
正常的情况下,数据库中有一个权限表,需要联表查询;这里直接在数据库表中,增加一个字段来进行操作:
-
修改实体类,增加新的字段:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
private String perms;
}
-
在自定义的授权认证中,获取登录的用户,从而实现动态认证授权操作:
- 在用户登录授权的时候,将用户放在 Principal 中,改造下之前的代码:
// 第一个参数 user:把用户放在Principal中,在授权代码中进行调用 return new SimpleAuthenticationInfo(user, user.getPwd(), "");- 授权的方法中,获得这个用户,从而获得它的权限:
// 执行授权逻辑 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权逻辑PrincipalCollection"); // 给资源进行授权 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 添加资源的授权字符串 // info.addStringPermission("user:add"); // 获得当前对象 Subject subject = SecurityUtils.getSubject(); // 拿到User对象 User currentUser = (User) subject.getPrincipal(); // 设置权限 info.addStringPermission(currentUser.getPerms()); // 注意:需要返回 info return info; } -
给数据库中的用户,增加一些权限:
-
在过滤器中,将 update 请求,也进行权限拦截:
// 授权过滤器:注意顺序 perms[user:add]:权限标识
filterMap.put("/user/add", "perms[user:add]");
filterMap.put("/user/update", "perms[user:update]");
- 运行测试:登录不同的账户,进行测试;
3.9 Shiro 整合 Thymeleaf
- 根据权限,展示不同的前端页面;
- 导入 Maven 依赖:
<!--Shiro与Thymeleaf整合的包-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
- 配置一个 shiro 的 Dialect,在 shiro 的配置中,增加一个 Bean:
// 配置ShiroDialect:方言,用于thymeleaf和shiro标签配合使用
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
- thymeleaf 整合 shiro 的命名空间:
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
- 修改前端页面:
index.html
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">update</a>
</div>
-
运行测试:首页没有了add 和 update,因为没有登录,登录后,可以看到不同的用户,显示不同的页面;
-
修改代码:用户登录后,把信息放到 Session 中(这个 session 不是 httpSession 而是 shiro 中的),在执行认证逻辑时,加入 session;
// 用户登录后,把信息放到Session
Subject currentUser = SecurityUtils.getSubject(); currentUser.getSession().setAttribute("loginUser",user);
- 前端从 session(不是 httpSession,而是 shiro 中的) 中获取数据,然后判断,是否显示登录:
<!--用户未登录,显示登录的链接-->
<p th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}">登录</a>
</p>
Shiro 实现用户注销
- 按照 Shiro 的过滤器使用语法,注销功能,只需要在 map 集合中,加入一个 kv 键值对即可;
- 为对应的注销写一个 a 标签,来发起注销请求,并将 a 标签的 href 属性,设置为需要注销过滤器处理的 url;
- 在 controller 中,增加 logout 处理映射方法:
// 注销
@RequestMapping("/logout")
public String logout() {
return "redirect:/toLogin";
}
- 添加注销过滤器,到 map 集合中:ShiroConfig
// 注销
filterMap.put("/logout", "logout");
- 修改前端页面:
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h2>首页</h2>
<!--用户未登录,显示登录的链接-->
<p th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}">登录</a>
</p>
<p th:text="${msg}"></p>
<p>
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">update</a>
</div>
<!--注销-->
<div th:if="${session.loginUser!=null}">
<a th:href="@{/logout}">注销</a>
</div>
</p>
</body>
</html>
3.10 Spring Boot 整合 Shiro(使用 Shiro Starter)
环境搭建
-
在之前的 Maven 项目中,新建 Spring Boot 模块:选中 web、 thymeleaf、Mybatis、Mysql,会自动添加相关依赖;
-
创建成功后,添加
shiro-spring-boot-web-starter等其它依赖:
<dependencies>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-spring-boot-starter:非spring官方,作用整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Shiro 和 Spring Boot 的整合依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.9.0</version>
</dependency>
<!--Log4j2 日志-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.6.7</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<!--Shiro与Thymeleaf整合的包-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
</dependencies>
- 配置 Shiro 基本信息:
application.properties
# 是否允许将sessionId 放到 cookie 中
shiro.sessionManager.sessionIdCookieEnabled=true
# 是否允许将 sessionId 放到 Url 地址拦中
shiro.sessionManager.sessionIdUrlRewritingEnabled=true
# 访问未获授权的页面时,自定义的跳转路径
shiro.unauthorizedUrl=/noauth
# 启用 Shiro 的 Spring Web 模块
shiro.web.enabled=true
# 登录成功的跳转页面
shiro.successUrl=/index
# 登录页面
shiro.loginUrl=/login
- 设置数据库连接和其它配置:
application.yaml
# MySQL - 8.0.28
spring:
# 关闭 thymeleaf 缓存
thymeleaf:
cache: false
datasource:
username: root
password: 123456
# serverTimezone=UTC解决时区报错
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=true
driver-class-name: com.mysql.cj.jdbc.Driver
# 自定义数据源 druid
type: com.alibaba.druid.pool.DruidDataSource
# Spring Boot 默认是不注入这些属性值的,需要自定义配置类,进行绑定
# druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的 filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
# 如果运行时报错:java.lang.ClassNotFoundException: org.apache.log4j.Priority
# 则导入 log4j 依赖即可,如使用log4j2,对应导入相应依赖
filters: stat,wall,log4j2
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 整合 MyBatis
mybatis:
# 对应实体类的路径
type-aliases-package: com.study.pojo
# 指定myBatis的核心配置文件与Mapper映射文件,classpath:后面不需要加 /
mapper-locations: classpath:mapper/*.xml
# 开启驼峰命名规范自动映射
configuration:
map-underscore-to-camel-case: true
- 创建 Realm,和前面的代码相同;
- 配置 ShiroConfig:
@Configuration
public class ShiroConfig {
// 创建 realm 对象
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
// 创建 DefaultWebSecurityManager
@Bean
DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(userRealm());
return manager;
}
// ShiroFilterChainDefinition代替了ShiroFilterFactoryBean
@Bean
ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
// definition.addPathDefinition("/login", "anon");
Map<String, String> filterMap = new LinkedHashMap<String, String>();
// 授权过滤器:注意顺序 perms[user:add]:权限标识
filterMap.put("/user/add", "perms[user:add]");
filterMap.put("/user/update", "perms[user:update]");
filterMap.put("/user/*", "authc");
// 注销
filterMap.put("/logout", "logout");
definition.addPathDefinitions(filterMap);
return definition;
}
// 配置ShiroDialect:方言,用于thymeleaf和shiro标签配合使用
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
}
- 其它代码,和前面相同;下载链接