封面画师:adsuger     封面ID:74171912

1. SpringSecurity

在Web开发中,安全一直都是一个非常重要的方面,如果开发的应用出现安全漏洞,极大概率会造成隐私泄露。虽然它属于应用的非功能性需求,但我们应该在应用设计初期就将其考虑进来,如果不在设计之初就考虑清楚,架构一旦确定再进行更改会对架构产生较大的改动。虽然使用过滤器和拦截器也可以达到安全处理,但市面上也有集成的安全框架。

在市面上有两种比较出名的安全框架,一种是SpringSecurity,一种是Shiro,它们主要用来做两件事:认证、授权。

权限分类:功能权限、访问权限、菜单权限,我们可以使用大量的原生代码编写拦截器或过滤器来实现这些权限。

我们先对SpringSecurity进行简单的介绍。

先来一手官网:SpringSecurity官网​ 😝

1.1 环境搭建

创建项目时,勾选web与thymeleaf,向项目中添加web与thymeleaf的启动器

导入静态资源(目录结构如下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
index.html
|views
    |level1
        1.html
        2.html
        3.html
    |level2
        1.html
        2.html
        3.html
    |level3
        1.html
        2.html
        3.html
    login.html

为了便于测试,我们可以关闭模板引擎的缓存:

1
spring.thymeleaf.cache=false

编写Controller,实现基本页面跳转,也是相当于测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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;
    }
}

编写好代码后,我们可以运行项目、测试环境!

1.2 初识SpringSecurity

PS:回忆并比较我们学习过的Spring AOP:横切与配置类。

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,它可以实现强大的Web安全控制,对于安全控制,我们仅需要引入spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住我们常用的几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

“认证”(Authentication):

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization):

授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

这个概念是通用的,而不是只在Spring Security 中存在。

1.3 认证和授权

导入SpringSecurity的启动器:

1
2
3
4
5
<!--security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

根据官方文档,编写 SpringSecurity 配置类,配置类固定的结构:

1
2
3
4
5
6
7
8
9
@EnableWebSecurity // 开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

在配置类中设计权限规则:

1
2
3
4
5
6
7
8
9
10
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有页可以访问,但是功能页需要访问权限
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasAnyRole("vip1")
        .antMatchers("/level2/**").hasAnyRole("vip2")
        .antMatchers("/level3/**").hasAnyRole("vip3");
}

然后,我们可以进行简单的测试。启动程序,进入首页,点击超链接,我们发现除了首页,其他界面都无法进入。报 403 状态,Access Denied。

因为我们没有进行登录,所以当我们进行需要登录的操作时,应该将页面跳转至登录界面,而不是报错。开启登录界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有页可以访问,但是功能页需要访问权限
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasAnyRole("vip1")
        .antMatchers("/level2/**").hasAnyRole("vip2")
        .antMatchers("/level3/**").hasAnyRole("vip3");
    // 开启自动配置的登录功能
    // /login 请求来到登录页
    // /login?error 重定向到这里表示登录失败
    http.formLogin();
}

然后我们可以进行测试:没有登录但进行相关操作时,跳转至登录界面。

SpringSecurity登录界面

但是我们要怎么为用户设置权限呢?我们可以定义认证规则,重写configure()方法。SpringSecurity提供了两种方式,一种是在内存中定义,一种是从数据库取得。我们在此演示在内存中定义。

在配置类中定义认证规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
//认证
//密码编码:PasswordEncoder
//在Spring Security 5.x中新增了多种加密方式,也改变了代码的书写方式
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //在内存中定义,真实场景从数据库读取
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
        .withUser("yang").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
        .and()
        .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
        .and()
        .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}

最后,进行测试!发现登陆成功,每个角色拥有自己的访问权限,且只能进行自己权限下的操作。😉

1.4 注销及权限控制

注销

开启注销功能:

1
2
3
4
5
6
7
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
    //....
    //开启自动配置的注销的功能
    http.logout();
}

添加注销超链接:

1
2
3
<a class="item" th:href="@{/logout}">
   <i class="address card icon"></i> 注销
</a>

然后进行测试:登陆成功后点击注销,跳转至登录页面。

但是,如果我们想要注销后跳转至首页,应该怎么做呢?

1
2
// 在注销功能后添加一个方法即可
http.logout().logoutSuccessUrl("/");

然后,我们再进行 测试,发现符合我们的预期。

权限控制

现在导航条上有两个按钮(或者说超链接),但是这显然不符合实际情况。我们需要在用户没有登录的时候,导航栏上只显示登录按钮;用户登录成功后,显示用户名、用户角色和注销按钮,同时页面中只显示用户拥有相信权限的内容。

因为我们使用的Thymeleaf模板引擎,因此我们需要结合Thymeleaf中的一些功能。

导入相关依赖:

1
2
3
4
5
6
<!-- thymeleaf-extras- springsecurity4 -->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

修改前端界面:

首先导入命名空间:

1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org"
        xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">

修改导航栏,添加认证判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!--登录注销-->
<div class="right menu">

    <!--如果未登录-->
    <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 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>

</div>

PS:在这里我们用到了sec:authorize="isAuthenticated()",用来验证是否登录,进而显示不同的界面。

然后我们重启项目,登录系统,查看是否修改成功。


如果我们登录后进行注销产生了 404 错误,因为Security默认防止 CSRF(跨站请求伪造)。Security认为会产生安全问题,因此产生 404 错误,我们可以将请求改为post表单提交,或者在Spring Security中关闭 CSRF功能以消除 404 错误。

我们可以在配置类中增加 http.csrf().disable();以关闭 Security的防止CSRF功能。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void configure(HttpSecurity http) throws Exception {

    // ... 

    //防止网站攻击
    http.csrf().disable(); //关闭csrf
    //开启注销,并跳转至首页
    http.logout ().logoutSuccessUrl("/");


}

我们还没有写完,因为还没有实现根据用户角色实现界面显示对应的内容。继续修改界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<div>
    <br>
    <div class="ui three column stackable grid">


        <!--内容用户权限动态显示-->
        <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>
</div>

然后,我们就可以进行测试验证功能是否实现了。

1.5 记住我及首页定制

记住我

所谓“记住我”,就是用户登录后关掉浏览器,再次打开浏览器时不用再次登录。

直接去配置类中开启记住我:

1
2
3
4
5
6
7
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
    // ... 
    //记住我
    http.rememberMe();
}

然后我们进行测试,发现登录页多了一个记住我功能,我们登陆成功后关闭浏览器,再重新打开浏览器,用户依旧存在,不用再次登录。

那么这个功能是怎么实现的呢?

其实很简单,就是用户登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了(cookie默认保留14天)。如果点击注销,则会删除这个cookie。

注销删除Cookie

首页定制

我们现在使用的登录页面时Security自带的,不是很美观,我们想要使用我们自己编写的界面,那么应该怎么做呢?

在登录页配置后面加上loginPage("/toLogin")即可。即:http.formLogin().loginPage("/toLogin");

这时候的前端页面也应当指向我们自定义的login请求:

1
2
3
<a class="item" th:href="@{/toLogin}">
   <i class="address card icon"></i> 登录
</a>

假设我们的前端的请求不是/toLogin,而是/login,那么应该怎么写呢?默认是/login方式,但是默认的已经被我们自定义的取消的。我们可以这么写:

1
2
3
4
5
6
7
8
//权限不足默认到登录页,并开启登录页面
//login
//定制登录页 loginPage("/toLogin") 参数表示登录页请求地址
// loginProcessingUrl("/login") 参数表示前端请求地址
//可加usernameParameter("username")
//    .passwordParameter("password") 绑定前端输入框名,防止输入框name与Security默认的不一致
http.formLogin().loginPage("/toLogin").usernameParameter("username")
    .passwordParameter("password").loginProcessingUrl("/login");

由于使用了自定义的登录界面,“记住我”功能也消失了,我们需要自定义,在配置类中编写:

1
2
//开启记住我功能 cookies,自定义接受前端参数
http.rememberMe().rememberMeParameter("remember");

同时在登录页添加记住我的组件:

1
<input type="checkbox" name="remember"> 记住我

然后进行测试即可!

1.6 代码节选

前端页面设计采用了SemanticUI进行编写。

完整配置代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首页所有页可以访问,但是功能页需要访问权限
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasAnyRole("vip1")
                .antMatchers("/level2/**").hasAnyRole("vip2")
                .antMatchers("/level3/**").hasAnyRole("vip3");


        //权限不足默认到登录页,并开启登录页面
        //login
        //定制登录页 loginPage("/toLogin")
        //可加usernameParameter("username")
        //        .passwordParameter("password")绑定前端输入框名
        http.formLogin().loginPage("/toLogin").usernameParameter("username")
                .passwordParameter("password").loginProcessingUrl("/login");

        //防止网站攻击
        http.csrf().disable(); //关闭csrf
        //开启注销,并跳转至首页
        http.logout ().logoutSuccessUrl("/");

        //开启记住我功能 cookies,自定义接受前端参数
        http.rememberMe().rememberMeParameter("remember");
    }

    //认证
    //密码编码:PasswordEncoder
    //在Spring Security 5.x中新增了多种加密方式
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //真实场景从数据库读取
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("yang").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
                .and()
                .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
    }
}

前端登录界面节选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div class="ui placeholder segment">
    <div class="ui column very relaxed stackable grid">
        <div class="column">
            <div class="ui form">
                <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>
                    <div class="field">
                        <input type="checkbox" name="remember">记住我
                    </div>
                    <input type="submit" class="ui blue submit button"/>
                </form>
            </div>
        </div>
    </div>
</div>

2. Shiro

2.1 什么是Shiro

  • Shiro是一个Java安全(权限)框架
  • Shiro可以非常容易的开发出足够好的应用,不仅可以用于JavaSE环境,还可以用于JavaEE环境
  • Shiro可以完成认证、授权、加密、会话管理、Web集成、缓存等
  • 官网:Shiro

2.2 有哪些功能

Shiro功能图

  • Authentication:身份认证、登录
  • Authorization:授权验证
  • Session Manage:会话管理,即用户登录后就是第一次会话,在没退出之前,他的所有信息都在会话中
  • Cryptography:加密
  • Web Support:Web支持
  • Caching:缓存
  • Concurrency:Shiro支持多线程应用的并发验证,即:在一个线程中开启另外一个线程,能把权限自动传播过去
  • Testing:提供测试支持
  • Run As:如果允许,可以使一个用户伪装成另一个用户登录
  • Remember Me:记住我

2.3 Shiro 快速开始

官方GitHub快速开始代码:ShiroQuickStart

官方快速开始文档:ShiroQuickStartDoc


根据官方文档,我们先创建一个Maven项目,然后倒入以下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.1</version>
</dependency>
<!-- Shiro uses SLF4J for logging.  We'll use the 'simple' binding
             in this example app.  See http://www.slf4j.org for more info. -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.21</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.21</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

将官方的 log4j.properties 文件拷贝至我们项目的resources目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

将官方的 shiro.ini 文件拷贝至我们项目的resources目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[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.java 文件拷贝至我们项目的java目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.lang.util.Factory;
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 {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);

    public static void main(String[] args) {

        // The easiest way to create a Shiro SecurityManager with configured
        // realms, users, roles and permissions is to use the simple INI config.
        // We'll do that by using a factory that can ingest a .ini file and
        // return a SecurityManager instance:

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();

        // for this simple example quickstart, make the SecurityManager
        // accessible as a JVM singleton.  Most applications wouldn't do this
        // and instead rely on their container configuration or web.xml for
        // webapps.  That is outside the scope of this simple quickstart, so
        // we'll just do the bare minimum so you can continue to get a feel
        // for things.
        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true);
            try {
                currentUser.login(token);
            } 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!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                         "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        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.");
        }

        //a (very powerful) Instance Level permission:
        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!");
        }

        //all done - log out!
        currentUser.logout();

        System.exit(0);
    }
}

然后我们就可以运行并在控制台查看输出日志了。


如果我们直接拷贝官方的依赖,运行会报错,这时我们需要导入common-logging依赖(Shiro默认使用的日志依赖),然后再次运行;运行后我们又发现控制台并没有按照我们的期望输出日志,这是因为我们的依赖拷贝的依赖中含有<scope>test</scope>,我们需要将<scope>test</scope>删除就可以输出期望的日志了。

2.4 Shiro的Subject分析

我们来分析一下快速开始中的Java代码。👊

首先,使用工厂模式解析配置文件 shiro.ini ,并创建 SecurityManager 对象:

1
2
3
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

获取当前的用户对象Subject:

1
Subject currentUser = SecurityUtils.getSubject();

通过当前用户获取Session(Shiro的Session,非Web中的Session):

1
2
3
4
5
6
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
    log.info("Retrieved the correct value! [" + value + "]");
}

判断当前的用户是否被认证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (!currentUser.isAuthenticated()) {
    // Token: 令牌
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
    token.setRememberMe(true);    // 设置记住我
    try {
        currentUser.login(token);    // 执行登录操作
    } 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!");
    } catch (LockedAccountException lae) { // “用户被锁定”异常
        log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                 "Please contact your administrator to unlock it.");
    }
    // ... catch more exceptions here (maybe custom ones specific to your application?
    catch (AuthenticationException ae) {    // 认证异常
        //unexpected condition?  error?
    }
}

//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

角色检查:

1
2
3
4
5
if (currentUser.hasRole("schwartz")) {
    log.info("May the Schwartz be with you!");
} else {
    log.info("Hello, mere mortal.");
}

权限检查,粗粒度:

1
2
3
4
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.");
}

权限检查(比第一次检查得更具体),细粒度:

1
2
3
4
5
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!");
}

账户注销:

1
currentUser.logout();

退出系统:

1
System.exit(0);

常用代码:

1
2
3
4
5
6
7
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
currentUser.isAuthenticated();
currentUser.getPrincipal();
currentUser.hasRole("schwartz");
currentUser.isPermitted("lightsaber:wield");
currentUser.logout();

2.5 SpringBoot 集成 Shiro

2.5.1 项目准备

创建SpringBoot项目,选择导入Web和Thymeleaf的启动器:

1
2
3
4
5
6
7
8
<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>

编写一个简单的首页,用于测试环境是否搭建成功:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org/">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>首页</h1>
        <p th:text="${msg}"></p>
    </body>
</html>

再编写一个简单的Controller,用于界面跳转:

1
2
3
4
5
6
7
8
9
@Controller
public class MyController {

    @RequestMapping({"/","index"})
    public String toIndex(Model model){
        model.addAttribute("msg","hello,shiro!");
        return "index";
    }
}

最后,启动项目,访问:http://localhost:8080/检查环境是否搭建成功!


补充:

Shiro三大对象回顾:

1
2
3
4
5
<!--
    Subject 用户
    SecurityManager 管理所有用户
    Realm 连接数据
-->

2.5.2 创建三大对象

导入整合所需依赖:

1
2
3
4
5
6
7
<!--shiro整合spring-->
<!--shiro-spring -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.0</version>
</dependency>

创建包config,在包下创建ShiroConfig.java

1
2
3
4
5
6
@Configuration
public class ShiroConfig {
        //3. 创建 ShiroFilterFactoryBean
        //2. 创建 DefaultWebSecurityManager
        //1. 创建 realm 对象
}

我们需要倒着来进行创建,首先创建 realm 对象。在config包下创建自定义realm类UserRealm.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//自定义UserRealm  extends AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=>授权doGetAuthorizationInfo");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");
        return null;
    }
}

PS: Shiro会先执行认证,再执行授权!

将自定义的UserRealm类添加到Spring容器中(在ShiroConfig类中编写):

1
2
3
4
5
//创建realm对象, 自定义类 :1
@Bean(name = "userRealm")
public UserRealm userRealm(){
    return new UserRealm();
}

创建 DefaultWebSecurityManager

1
2
3
4
5
6
7
8
9
//DefaultWebSecurityManager :2
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    //关联UserRealm
    // 也可以不使用@Qualifier,直接securityManager.setRealm(userRealm());
    securityManager.setRealm(userRealm);
    return securityManager;
}

创建 ShiroFilterFactoryBean

1
2
3
4
5
6
7
8
//ShiroFilterFactoryBean :3
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(defaultWebSecurityManager);
    return bean;
}

2.5.3 页面拦截

编写两个前端页面:

add.html:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>ADD</h1>
</body>
</html>

update.html:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>UPDATE</h1>
</body>
</html>

在控制器中编写跳转代码:

1
2
3
4
5
6
7
8
9
@RequestMapping("/user/add")
public String add(){
    return "user/add";
}

@RequestMapping("/user/update")
public String update(){
    return "user/update";
}

在index.html中编写用于点击的超链接:

1
2
3
<a th:href="@{/user/add}">add</a>

<a th:href="@{/user/update}">update</a>

在配置类中添加Shiro过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//ShiroFilterFactoryBean :3
@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<>();
    filterMap.put("/user/add","authc");
    filterMap.put("/user/update","authc");
    bean.setFilterChainDefinitionMap(filterMap);
    return bean;
}

然后启动项目,运行测试,进入index界面后,我们点击界面上的add或update超链接,这个时候界面不会跳转至对应的界面,而是报错。这是因为我们进行的拦截设置,我们当前并没有权限。

但实际场景应该是:没有权限跳转至登录界面,而不是给用户报错。因此我们需要编写一个登录界面login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org/">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>登录</h1>
        <hr>
        <!-- 消息提示 -->
        <p th:text="${msg}" style="color: red"></p>
        <form th:action="@{/login}">
            <p>用户名:<input type="text" name="username"></p>
            <p>密码:<input type="password" name="password"></p>
            <p><input type="submit" value="提交"></p>
        </form>

    </body>
</html>

添加跳转至登录界面的控制器:

1
2
3
4
@RequestMapping("/toLogin")
public String toLogin(){
    return "login";
}

在ShiroConfig配置类中设置登录的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){
    // ... 
    //        filterMap.put("/user/add","authc");
    //        filterMap.put("/user/update","authc");
    // 将上述代码二合一,支持通配符
    //授权,未被授权会跳转到未授权界面
    filterMap.put("/user/*","authc");
    bean.setFilterChainDefinitionMap(filterMap);

    //设置登录请求
    bean.setLoginUrl("/toLogin");

    return bean;
}

然后,我们可以进行测试,发现未登录时会跳转至登录界面。

2.5.4 用户认证

在控制器中编写登录逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping("/login")
public String login(String username,String password,Model model){
    //获取当前的用户
    Subject subject = SecurityUtils.getSubject();
    //封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);

    try {
        subject.login(token);//执行登录的方法,如果没有异常就登录成功
        return "index";
    } catch (UnknownAccountException e) {   //用户名不存在
        model.addAttribute("msg","用户名不存在!");
        return "login";
    }catch (IncorrectCredentialsException e){
        model.addAttribute("msg","密码不正确!");
        return "login";
    }
}

UserRealm 类中编写用户认证逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行了=>认证doGetAuthenticationInfo");

    // 模拟登录数据
    String username = "root";
    String password = "123456";
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;

    if (!userToken.getUsername.equals(username)){ //账户不存在
        return null;    //UnknownAccountException
    }

    return new SimpleAuthenticationInfo("",password,"");
}

2.5.5 整合数据库

导入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<!--MySQL驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--log4j-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>
<!--mybatis springboot-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

编写全局配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
spring:
  datasource:
    username: root
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    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 依赖即可,Maven 地址: https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

在配置文件中配置MyBatis:

1
2
mybatis.type-aliases-package=com.yang.pojo
mybatis.mapper-locations=classpath:mapper/*.xml

创建POJO(Account.java):

1
2
3
4
5
6
7
8
9
10
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private int id;
    private String name;
    private float money;
    private String password;
}

编写mapper接口:

1
2
3
4
5
6
@Repository
@Mapper
public interface AccountMapper {

    public Account queryAccountByName(String name);
}

编写MyBatis映射文件:

1
2
3
4
5
6
7
8
9
10
11
<?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.yang.mapper.AccountMapper">

    <select id="queryAccountByName" parameterType="String" resultType="Account">
        select * from account where name = #{name}
    </select>

</mapper>

编写Service层的接口和实现类:

1
2
3
public interface AccountService {
    public Account queryAccountByName(String name);
}
1
2
3
4
5
6
7
8
9
10
11
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    AccountMapper accountMapper;

    @Override
    public Account queryAccountByName(String name) {
        return accountMapper.queryAccountByName(name);
    }
}

改造UserRealm 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//自定义UserRealm  extends AuthorizingRealm
public class UserRealm extends AuthorizingRealm {

    @Autowired
    AccountService accountService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=>授权doGetAuthorizationInfo");

        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");

        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        Account account = accountService.queryAccountByName(userToken.getUsername());

        if (account == null){ //账户不存在
            return null;    //UnknownAccountException
        }

        //可以加密 MD5、MD5盐值加密
        //密码认证,shiro自行验证 密码加密
        return new SimpleAuthenticationInfo("",account.getPassword(),"");
    }
}

2.5.6 MD5盐值加密密码

在上述案例中,我们只使用的简单的认证,即使是明文密码也可以通过认证,我们还可以设置其他的加密方法。

查看有哪些加密方法:

可以去 UserRealm 的父类 AuthorizingRealm 的父类 AuthenticatingRealm 中找一个方法。这个方法可以设置证书匹配器setCredentialsMatcher()。我们进入接口CredentialsMatcher,使用快捷键Ctrl + Alt + B查看接口实现类:

CredentialsMatcher实现类

我们看到有许多的实现类,这些实现类就提供了加密的方法。

❓那如果我想要更换加密方式,应该怎么做呢?

在配置类中创建一个bean:

1
2
3
4
5
6
7
8
9
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    // 使用md5 算法进行加密
    hashedCredentialsMatcher.setHashAlgorithmName("md5");
    // 设置散列次数: 意为加密几次
    hashedCredentialsMatcher.setHashIterations(1);
    return hashedCredentialsMatcher;
}

将创建的bean注册到realm中:

1
2
3
4
5
6
7
//创建realm对象, 自定义类 :1
@Bean(name = "userRealm")
public UserRealm userRealm(){
    UserRealm userRealm = new UserRealm();
    userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
    return userRealm;
}

修改自定义的realm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    // ... 

    // 设置盐值
    ByteSource salt = ByteSource.Util.bytes(account.getName());
    // 密码使用MD5盐值加密
    SimpleHash simpleHash = new SimpleHash("MD5", account.getPassword(), salt);
    // 打印解密后的密码
    System.out.println(simpleHash.toString());
    // 返回SimpleAuthenticationInfo实例,格式为(用户,用户密码,盐,当前Realm的类名)
    return new SimpleAuthenticationInfo(account,simpleHash,salt,"");
}

然后我们可以重新启动项目,进行登录。测试后,可以登录成功,控制台也打印出加密后的密码:

1
dbb1c112a931eeb16299d9de1f30161d

我们可以使用MD5在线穷举破解( MD5不可逆 ,这只是穷举法破解出来的),得到解密后的密码:

1
root123456

我们发现,使用了盐值后,密码变成了 盐值 + 实际密码 的格式。😎

❓那么我们确定是真的被加密了呢?我们可以在下方位置打一个断点:

ShiroMD5盐值加密断点

然后使用Debug模式启动项目,进入首页,点击登录,输入用户名和密码。在控制台中找到this,点开它;找到credentialsMatcher,点开它。我们可以发现,我们前面进行的配置都在这里显示了。如果使用最开始的方式,不进行加密,这里会是空的。💪

Debug-credentialsMatcher


PS:

MD5在线加密:MD5在线加密

MD5在线穷举破解:MD5在线解密破解

2.5.7 用户拦截与授权

经过上面的测试,我们多次在控制台上看到以下信息:

1
2
执行了=>认证doGetAuthenticationInfo
执行了=>授权doGetAuthorizationInfo

表明 Shiro会先执行认证,再执行授权!


拦截

ShiroConfig类的ShiroFilterFactoryBean中添加以下代码:

1
2
3
4
5
6
7
8
9
10
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){
    // ...
    //授权,未被授权会跳转到未授权界面
    filterMap.put("/user/add","perms[account:add]");
    filterMap.put("/user/update","perms[account:update]");
    filterMap.put("/user/*","authc");
    // ... 
    return bean;
}

然后我们运行程序,并登陆,点击页面中的 addupdate 链接,这时候页面会跳转至错误页面。状态码是401,显示未授权。

但是真实业务中,不应该跳转至错误页面,应该跳转至权限不足页面。因此,我们需要编写Controller(模拟跳转至错误页面):

1
2
3
4
5
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
    return "当前用户权限不足!";
}

同时,需要在ShiroConfig类的ShiroFilterFactoryBean中设置权限不足界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){
   
    // ...

    //授权,未被授权会跳转到未授权界面
    filterMap.put("/user/add","perms[account:add]");
    filterMap.put("/user/update","perms[account:update]");
    filterMap.put("/user/*","authc");
    
    // ...
    
    //设置权限不足界面
    bean.setUnauthorizedUrl("/noauth");

    return bean;
}

然后,我们可以再进行测试。

运行程序,并登陆,点击页面中的 addupdate 链接,这时候页面会跳转至我们设定的未授权界面。

授权

我们已经让某些界面需要权限才能够访问,那么应该怎么给用户赋予一些权限呢?

UserRealm类中授权方法中添加权限赋予代码:

1
2
3
4
5
6
7
8
9
10
11
12
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了=>授权doGetAuthorizationInfo");

    //给资源进行授权
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //授予所有用户add权限
    info.addStringPermission("account:add");

    return info;
}

运行项目,选择任一用户登录系统。这时候所有的用户都被赋予 add 权限,相当于系统的默认权限。同时,在实际的业务操作中,用户的权限信息是储存在数据库中的,并且不同用于拥有不同的权限。为了达到这个目的,我们需要在现有数据库的用户表中添加perms字段,表示用户所拥有的对应权限。

数据表增加perms字段

然后,我们需要给我们的实体类增加一个属性,用来表示用户的权限:

1
2
3
4
5
6
7
8
9
10
11
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private int id;
    private String name;
    private float money;
    private String password;
    private String perms;
}

更改UserRealm类中认证方法的返回值,使授权方法可以获取到当前用户:

1
return new SimpleAuthenticationInfo(account,account.getPassword(),"");

更改UserRealm类中授权方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了=>授权doGetAuthorizationInfo");

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //授予所有用户add权限
    info.addStringPermission("account:add");

    //获取当前登录的对象
    Subject subject = SecurityUtils.getSubject();
    /*获取认证返回的用户信息
         new SimpleAuthenticationInfo(account,simpleHash,salt,"")
         即:获取account
        */
    Account currentAccount = (Account) subject.getPrincipal();       //获取Account对象

    //设置当前用户的权限
    info.addStringPermission(currentAccount.getPerms());

    return info;
}

最后,进行测试即可。

2.5.8 整合Thymeleaf

经过上面的编写与测试,我们发现用户如果没有 update 权限的情况下,进入首页也会显示 update 的链接。我们希望的应该是没有对应的权限,就不要显示对应的链接。这时候,就需要进行 Shiro 和 Thymeleaf 的整合!

首先,我们需要导入Shiro与Thymeleaf整合的依赖:

1
2
3
4
5
6
<!--shiro thymeleaf-->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

然后在配置类ShiroConfig中编写getShiroDialect()并向容器中添加一个bean:

1
2
3
4
5
//整合ShiroDialect:用于整合shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
    return new ShiroDialect();
}

修改index.html前端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org/"
      xmlns:shiro="https://www.thymeleaf.org/thymeleaf-extras-shiro">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>首页</h1>
        <!--判断用户是否登录-->
        <div shiro:notAuthenticated>
            <a th:href="@{/toLogin}">登录</a>
        </div>
        <hr>
        <p th:text="${msg}"></p>

        <div shiro:hasPermission="account:add">
            <a th:href="@{/user/add}">add</a>
        </div>
        <div shiro:hasPermission="account:update">
            <a th:href="@{/user/update}">update</a>
        </div>

    </body>
</html>

最后,启动项目,测试即可。


在进行判断用户是否登录时,还可以从Session中进行取值判断。

更改前端代码:

1
2
3
4
<!--从session中判断值-->
<div th:if="${session.loginAccount==null}">
    <a th:href="@{/toLogin}">登录</a>
</div>

UserRealm类的认证方法中设置Session(这里的Session是Shiro的,不是Servlet的):

1
2
3
4
5
6
7
8
9
10
11
12
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行了=>认证doGetAuthenticationInfo");
    // ...

    Subject currentSubject = SecurityUtils.getSubject();
    Session session = currentSubject.getSession();
    session.setAttribute("loginAccount",account);
    
    // ...
}

同样可以启动项目并测试,会得到相同的结果。

2.6 重要代码

自定义realm,使用了MD5盐值加密。整合Thymeleaf时,采用从Session中获取值判断有无用户登录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//自定义UserRealm  extends AuthorizingRealm
public class UserRealm extends AuthorizingRealm {

    @Autowired
    AccountService accountService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=>授权doGetAuthorizationInfo");

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //授予所有用户add权限
        info.addStringPermission("account:add");

        //获取当前登录的对象
        Subject subject = SecurityUtils.getSubject();
        /*获取认证返回的用户信息
         new SimpleAuthenticationInfo(account,simpleHash,salt,"")
         即:获取account
        */
        Account currentAccount = (Account) subject.getPrincipal();       //获取Account对象

        //设置当前用户的权限
        info.addStringPermission(currentAccount.getPerms());

        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=>认证doGetAuthenticationInfo");

        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        Account account = accountService.queryAccountByName(userToken.getUsername());

        if (account == null){ //账户不存在
            return null;    //UnknownAccountException
        }

        Subject currentSubject = SecurityUtils.getSubject();
        Session session = currentSubject.getSession();
        session.setAttribute("loginAccount",account);

        // 设置盐值
        ByteSource salt = ByteSource.Util.bytes(account.getName());
        // 密码使用MD5盐值加密
        SimpleHash simpleHash = new SimpleHash("MD5", account.getPassword(), salt);
        // 打印解密后的密码
        System.out.println(simpleHash.toString());
        return new SimpleAuthenticationInfo(account,simpleHash,salt,"");

    }
}

Shiro配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Configuration
public class ShiroConfig {

    //ShiroFilterFactoryBean :3
    @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<>();
        //        filterMap.put("/user/add","authc");
        //        filterMap.put("/user/update","authc");

        //授权,未被授权会跳转到权限不足界面
        filterMap.put("/user/add","perms[account:add]");
        filterMap.put("/user/update","perms[account:update]");
        filterMap.put("/user/*","authc");

        bean.setFilterChainDefinitionMap(filterMap);

        //设置登录请求
        bean.setLoginUrl("/toLogin");
        //设置权限不足界面
        bean.setUnauthorizedUrl("/noauth");

        return bean;
    }



    //DefaultWebSecurityManager :2
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联UserRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    //创建realm对象, 自定义类 :1
    @Bean(name = "userRealm")
    public UserRealm userRealm(){
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return userRealm;
    }

    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 使用md5 算法进行加密
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 设置散列次数: 意为加密几次
        hashedCredentialsMatcher.setHashIterations(1);
        return hashedCredentialsMatcher;
    }

    //整合ShiroDialect:用于整合shiro thymeleaf
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }
}