Spring Boot 安全框架

506 阅读12分钟

一、概述

  • 在 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):
      • 指的是验证某个用户,是否有权限执行某个操作;
      • 在一个系统中,不同用户所具有的权限是不同的,比如一个文件,有的用户只能进行读取,有的用户可以进行修改;
      • 系统会为不同的用户,分配不同的角色,而每个角色,对应一系列的权限;
  • 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)
      • 授权发生在,系统成功验证身份后,最终会授予用户访问资源(如信息、文件、数据库、资金、位置、几乎任何内容)的完全权限;
  • 认证和授权的概念是通用的,而不是只在 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();
    }
}