在 Spring 中,流行的涉及权限管理的框架有两个:Spring Security 和 Apache Shiro。但是去了解一下 Spring Security 就知道,简单的权限管理根本用不到那么复杂的功能。在自己的项目中,我更倾向于使用简单明了的 Apache Shiro。
我们以最常见的用户、角色、权限
关系做例子。一个用户有多个角色、一个角色有多个用户、一个角色有多个权限、一个权限有多个角色。即用户与角色、角色与权限是多对多关系。
引入 shiro-spring 包
pom包依赖
重要的是 shiro-spring 这个包。
1 | <dependency> |
配置文件
1 | spring.datasource.driver-class-name=org.mariadb.jdbc.Driver |
实体类
用户类
1 | package me.xlui.spring.entity; |
角色类
1 | package me.xlui.spring.entity; |
权限类
1 | package me.xlui.spring.entity; |
初始化数据库表
1 | INSERT INTO shiro_user (id, password, salt, username) VALUES |
查询接口
UserRepository:
1 | package me.xlui.spring.repository; |
其他省略。
Shiro 配置
Apache Shiro 核心通过 Filter 实现,是基于 URL 规则来进行过滤和权限校验。我们通过注入类来进行 Shiro 的配置:
1 | package me.xlui.spring.config; |
通过 ShiroFilterFactoryBean
来处理拦截资源文件的问题(单独的 ShiroFilterFactoryBean 配置会出错,因为 Shiro 还需要 SecurityManager)。
ShiroFilterFactory 中已经由 Shiro 官方实现的过滤器(只列举常用的):
Filter Name | 作用 |
---|---|
anon | 匿名可访问 |
authc | 需要认证 |
user | 配置记住我或认证可访问 |
Shiro 认证和授权
Shiro 的认证、授权最终都交给 Realm 来处理,同时在 Shiro 中,用户、角色和权限等信息也是在 Realm 中获取。我们要做的是自定义一个类,继承抽象基类 AuthorizingRealm:
1 | package me.xlui.spring.config; |
继承 AuthorizingRealm 类需要实现两个方法:doGetAuthorizationInfo() 和 doGetAuthenticationInfo()。doGetAuthorizationInfo()
负责权限管理,即为用户设置允许的权限,doGetAuthenticationInfo()
方法负责身份认证,即检验用户名密码的正确性。
1 | SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( |
默认使用 CredentialsMatcher
来进行用户名密码确认,如果觉得默认的不好可以自己手动实现,下面讲密码加密存储会涉及到。注释的一行是密码加密时使用的盐,如果是明文密码去掉注释的一行即可。
接下来需要把自定义的 Realm 注入到 SecurityManager 中,代码在 ShiroConfiguration 类中已经实现:
1 |
|
测试
经过上面的操作 Shiro 的集成基本已经是完成了,下面进行测试:
控制器
1 | package me.xlui.spring.web; |
模板
index.html:
1 |
|
login.html:
1 |
|
运行
运行项目,访问 http://localhost:8080/。
密码加盐加密存储
实际应用中我们往往不会直接明文存储密码,因为这样非常不安全。而单纯的使用 MD5、SHA 之类的算法加密密码会存在数据库中两个密码相同用户的 password
字段也相同的情况,这样也很容易被撞库攻击。一种更安全的方式是加盐加密。
加盐加密的思路是在使用 MD5、SHA 之类算法的时候在用户的密码字段加一个随机、唯一的字符串(盐),这样生成的加密密码串几乎不可能存在相同的。即使是两个相同的密码,因为盐的不同,生成的密码串也是万万不同的。
生成加盐密码串
我采用的是访问特定 url 生成加盐密码串存储,实际应用的时候可以直接在用户注册或者修改密码的时候生成。
1 | // HelloController |
SimpleHash
是 Shiro 提供给我们的加密类,第一个参数是加密算法名,第二个参数是原密码,第三个参数是盐,因为在 Realm 中向 SimpleAuthenticationInfo
类传递参数时需要 ByteSource 类实例,所以我们在这里使用了相同的格式。实际上 SimpleHash 类对盐的具体类型没有要求,其形参的类型是 Object。第四个参数是加密的次数。
我们用自己的方式生成了加盐加密的密码串,接下来还需要告诉 Shiro 使用这种方式验证。
注入加密方式
本来我们应该编写一个加密算法类,但是 Shiro 已经替我们实现了,HashedCredentialsMatcher
,我们只需要注入使用即可。有两种使用方式:
(1)重写 MyShiroRealm
(自定义 Realm 类)的 setCredentialsMatcher
方法:
1 | public class MyShiroRealm extends AuthorizingRealm { |
(2)在 ShiroConfiguration
中注入:
1 |
|
两种方法无太大优劣,都可以成功告知 Shiro 使用这种方式进行加盐密码验证。
如果觉得默认的 HashedCredentialsMatcher
不好,可以自己动手实现一个,继承 CredentialsMatcher
接口,然后按照上面的方法集成即可。
验证
运行程序,访问 http://localhost:8080/en
,跳转到登录,登录后返回,对密码进行加盐存储。
查看数据库中用户表相应字段是否更新。
关闭浏览器,重新访问 http://localhost:8080
使用原用户名密码成功登录。
吐槽
Spring Boot 和 Shiro 似乎存在一些问题。我一般开发的时候都在配置文件(application.properties)中这样设置:
1 | spring.jpa.hibernate.ddl-auto=create |
然后再 classpath 也就是 src/main/resources 下新建 data.sql。这样 Spring Boot 在启动的时候就会删除所有相关表重建并且执行 data.sql 中的语句进行初始化。
但是在使用 Shiro 的情况下 data.sql 一直无法成功执行。Google 和 StackOverflow 都没有发现理想的回答[摊手]。
还有就是关于密码加盐存储这一点,百度到的博客基本就是抄来抄去,大部分只提了如何给密码加盐,基本没提到加盐存储之后 Shiro 如何验证。
源码
源代码已经上传 GitHub:https://github.com/xlui/Spring-Boot-Examples/tree/master/spring-boot-shiro。如果对你有所帮助,不妨留个 star 再走。