写在前面 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框架的学习就到此为止,后续开始学习其他内容。