写在前面

Shiro是一个安全框架,除了提供前面介绍的认证和授权之外,还可以对用户密码进行加密,那么本篇就来学习Shiro如何对密码进行加密,并介绍如何在SpringBoot中使用Shiro框架。

MD5加密

前面我们无论是读取配置文件中的密码还是模拟数据库中的密码,使用的都是明文密码,显然这是不安全的,因此很多情况下我们都会采用加密算法,尤其是采用非对称加密,其实就是不可逆加密,常用的MD5加密算法就是这样的一种算法。

举个例子,下面一段代码演示了如何利用MD5加密算法对密码1234进行加密:

1
2
3
4
5
public static void main(String[] args){
String password = "1234";
String encodePassword = new Md5Hash(password).toString();
System.out.println(encodePassword);
};

执行结果如下所示:

1
81dc9bdb52d04dc20036dbd8313ed055

由于这是非对称加密,因此开发者无法通过计算来将上述得到的加密字符串还原为之前的1234这一密码。这一开发者只需将这个加密的字符串保存在数据库中,等到下次用户登录的时候,继续使用MD5算法对密码进行加密,之后将生成的加密字符串与数据库中取出的字符串进行比较,这样就能知道密码是否正确,很明显这种方式既保留了密码验证功能又提升了安全性。

但是这种方式有一个致命的问题:开发者无法直接通过计算来反推密码,但是可以通过计算一些简单密码加密后的MD5,并与之前的值进行比较,这样就可以推算出原来的密码。说白了就是MD5算法对每个字符串生成的加密值是固定的,这样只要是密码1234,那么它通过MD5加密之后的值就是之前的那一串字符串。

加盐

通过前面的学习,我们知道MD5算法对每个字符串生成的加密值是固定的,因此密码相同的用户得到的MD5加密值是一样的。为了提高它的安全性,我们可以给原始的密码都添加一个随机数,然后再进行MD5加密,这个随机数就是通常所说的**盐(Salt)**,通过这种操作就能得到不同的MD5值,请注意此时我们也需要将这个随机数(盐值)也保存到数据库中,以便在进行密码验证时的校验。

为什么称这个随机数为盐呢?其实这个来源于生活。举个例子,当厨师在炒菜的时候,如果直接使用MD5,其实就相当于直接使用食材,而由于食材都是一样的,因此炒出来的味道都是一样的,但是如果加了不同分量的食盐,那么即使是相同的食材,炒出来的味道也是不同的,因此我们就称那个随机数为盐。

多次加密

除了加盐,还有一种方式就是多次加密,这样即使加密后的密码泄露了,但是由于不知道加密的次数,此时破解的难度也是很大。

举个例子,下面的代码就展示了如何使用Shiro框架自带的工具来生成盐,并采用三次MD5加密的方式,这样就可以得到安全系数很高的密码:

1
2
3
4
5
6
7
8
9
@Test
public void testSaltAndMultMD5(){
String password = "1234";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
int times =3; //加密次数为3
String algorithmName = "md5";
String encodePassword = new SimpleHash(algorithmName,password,salt,times).toString();
System.out.println(String.format("原始密码:%s,盐为:%s,加密次数为:%s,加密算法为:%s",password,salt,times,encodePassword));
}

执行结果如下所示:

1
原始密码:1234,盐为:y7QA+M0gJM3wDOrtwrWI7g==,加密次数为:3,加密算法为:3b739bad7c8857707656443471d69d96

Spring Boot集成Shiro

在传统的SSM框架中,手动整合Shiro时需要较为繁琐的配置步骤。而针对SpringBoot,Shiro官方提供了shiro-spring-boot-web-starter来简化Shiro在SpringBoot中的配置。由于此处侧重学习如何在Spring Boot中集成Shiro框架,因此这里依旧采用模拟数据库操作这一方式来进行演示。

第一步,创建SpringBoot Web项目并添加依赖。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为shirospringboot,然后在pom.xml文件中添加Shiro依赖以及页面模板引擎依赖,代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--添加Shiro依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!--添加thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--添加thymeleaf-extras-shiro依赖-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>

特别注意这里不需要添加spring-boot-starter-web依赖,因为shiro-spring-boot-web-starter中已经依赖了spring-boot-starter-web依赖。同时本案例使用了Thymeleaf模板,因此需要添加Thymeleaf依赖,另外为了在Thymeleaf中使用shiro标签,因此需要引入thymeleaf-extras-shiro依赖。
第二步,Shiro基本配置。首先在application.properties配置文件中配置Shiro的基本信息,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 开启Shiro配置,默认为true
shiro.enabled=true
# 开启Shiro Web配置,默认为true
shiro.web.enabled=true
# 设置登录地址,默认为/login.jsp
shiro.loginUrl=/login
# 设置登录成功地址,默认为/
shiro.successUrl=/index
# 设置未获授权默认跳转地址
shiro.unauthorizedUrl=/unauthorized
# 是否允许通过URL参数实现会话跟踪,如果网站支持Cookie,可以关闭该选项,默认为true
shiro.sessionManager.sessionIdUrlRewritingEnabled=true
# 是否允许通过Cookie实现会话跟踪,默认为true
shiro.sessionManager.sessionIdCookieEnabled=true

Shiro基本信息配置完成后,接下来在Java代码中配置Shiro,只需提供两个最基本的Bean即可。新建config包,并在其中创建ShiroConfig类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class ShiroConfig {
@Bean
public Realm realm(){
TextConfigurationRealm realm = new TextConfigurationRealm();
realm.setUserDefinitions("envy=1234,user\n admin=1234,admin");
realm.setRoleDefinitions("admin=read,write\n user=read");
return realm;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/login","anon");
chainDefinition.addPathDefinition("/doLogin","anon");
chainDefinition.addPathDefinition("/logout","logout");
chainDefinition.addPathDefinition("/**","authc");
return chainDefinition;
}
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}

解释一下上述代码的含义:

  • 这里提供了两个关键Bean,一个是Realm,另一个是ShiroFilterChainDefinition。至于ShiroDialect则是为了支持在Thymeleaf中使用Shiro标签,如果不在Thymeleaf中使用Shiro标签,那么可以不提供ShiroDialect
  • Realm可以是自定义的Realm,也可以是Shiro提供的Realm,简单起见这里没有配置数据库连接,这里直接配置了两个用户:envy/1234和admin/1234,分别对应角色user和admin,其中user角色只具有read权限,而admin角色拥有read和write权限。
  • ShiroFilterChainDefinition方法中配置了基本的过滤规则,/login/doLogin可以匿名访问,/logout是一个注销登录的请求,其余的请求都需要认证后才能访问。

第三步,新建controller类。接下来就是配置登录接口以及页面访问接口。新建一个controller包,并在其中创建UserController类,里面的代码为:

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 UserController {

@PostMapping("/doLogin")
public String doLogin(String username, String password, Model model){
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
}catch (AuthorizationException e){
model.addAttribute("error","用户名或密码输入错误!");
return "login";
}
return "redirect:/index";
}

@RequiresRoles("admin")
@GetMapping("/admin")
public String admin(){
return "admin";
}

@RequiresRoles(value = {"admin","user"},logical = Logical.OR)
@GetMapping("/user")
public String user(){
return "user";
}
}

简单解释一下上述代码的含义:

  • doLogin方法中,首先构造一个UsernamePasswordToken的实例,然后获取到一个Subject对象,并调用该对象中的login方法执行登录操作,在登录操作执行过程中,当有异常抛出时,说明登录失败,页面需要携带信息并返回给登录视图;当登录成功时,则重定向到/index接口。
  • 接下来暴露两个接口/admin/user,对于/admin接口来说需要具有admin角色的用户才能访问;而对于/user接口而言,具备admin或者user角色中的任意一个即可访问,因此需要使用Logical.OR来表示这种逻辑或关系。
  • 请注意由于这里是使用模板引擎,因此需要使用@Controller注解,而不是@RestController注解,这一点需要注意。

对于其他不需要角色就能访问的接口,直接定义在WebMvc中是最佳选择。新建一个WebMvcConfig类,注意它需要实现WebMvcConfigurer接口,并重写其中的addViewControllers方法,其中的代码为:

1
2
3
4
5
6
7
8
9
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/index").setViewName("index");
registry.addViewController("/unauthorized").setViewName("unauthorized");
}
}

这里就设置了三个URL,login、index和authorized页面及视图名称,访问这些URL是不需要经过controller控制器的。
第四步,新建异常处理类。接着创建全局异常处理器进行全局异常处理,本例子主要是处理授权异常。新建一个ExceptionController类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(AuthorizationException.class)
public ModelAndView error(AuthorizationException e){
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("error",e.getMessage());
modelAndView.setViewName("unauthorized");
return modelAndView;
}
}

当用户访问位授权的资源时,会自动跳转到unauthorized视图,并携带相应的出错信息。
第五步,新建对应的模板页面。当上述信息均配置完成时,接下来在resources/templates目录下创建5个HTML页面用于后续测试。
(1)新建index.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h3>Hello,<shiro:principal/></h3>
<h3><a href="/logout">注销登录</a></h3>
<h3><a shiro:hasRole="admin" href="/admin">管理员页面</a></h3>
<h3><a shiro:hasAnyRoles="admin,user" href="/user">普通用户页面</a></h3>
</body>
</html>

index.html是登录成功后的首页,首先展示当前登录用户的用户名,然后展示一个“注销登录”链接,若当前登录用户具备“admin”角色,则展示一个“管理员页面”的超链接;若当前登录用户具备“admin”或者“user”角色,则展示一个“普通用户页面”的超链接。注意这里导入的名称空间是xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"和在JSP页面中导入的Shiro名称空间不一致。

(2)新建login.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<div th:text="${error}"></div>
<input type="submit" value="登录">
</form>
</body>
</html>

login.html是一个普通的登录页面,在登录失败时通过一个div来显示登录失败的信息。
(3)新建user.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>普通用户</title>
</head>
<body>
<h1>普通用户个人页面</h1>
</body>
</html>

user.html是一个普通的用户信息展示页面。
(4)新建admin.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>管理员用户</title>
</head>
<body>
<h1>管理员个人页面</h1>
</body>
</html>

admin.html是一个管理员的信息展示页面。
(5)新建unauthorized.html页面,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>非法访问</title>
</head>
<body>
<h3>对不起,未获授权,非法访问</h3>
<h3 th:text="${error}"></h3>
</body>
</html>

unauthorized.html是一个授权失败的信息展示页面,该页面还会展示授权出错的信息。

第六步,测试。当上述信息均配置完成时,启动SpringBoot项目,访问登录页面,分别使用envy/1234和admin/1234进行登录,结果如下图所示:

注意因为envy用户不具备admin角色,因此登录成功后的页面是上没有前往管理员页面的超链接。

登录成功后,无论是envy还是admin用户,单机“注销登录”都会注销成功,然后回到登录页面,envy用户因为不具备admin角色,因此登录成功后的页面是上没有前往管理员页面的超链接,无法进入到管理员页面中。此时若开发者使用envy用户登录,然后手动在浏览器地址栏中输入http://localhost:8080/admin,则页面会跳转到未授权页面,如下图所示:

以上通过一个简单的例子学习了如何在SpringBoot中整合Shiro以及如何在Thymeleaf中使用Shiro标签,一旦整合成功,接下来Shiro的用法就和原来的一模一样。

那么本篇关于Shiro如何对密码进行加密以及如何在SpringBoot中集成Shiro框架的学习就到此为止,后续开始学习其他内容。