封面画师: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();
}
}