文章目录
1. SpringBoot集成shiro快速入门
导入shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
1. shiro 用户认证
@SpringBootTest
class ShiroApplicationTests {
@Test
void authentication() {
//构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//创建一个SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加一个测试账号(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("zhangsan","123456");
//设置Realm
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
// 进行登录,提交认证
subject.login(token);
}catch (IncorrectCredentialsException exception){
System.out.println("用户名密码不匹配");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch (UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
System.out.println("执行logout()方法");
subject.logout();
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
}
}
用户的认证状态:isAuthenticated=true
执行logout()方法
用户的认证状态:isAuthenticated=false
将密码改成:
simpleAccountRealm.addAccount("zhangsan","1234");
用户名密码不匹配
用户的认证状态:isAuthenticated=false
执行logout()方法
用户的认证状态:isAuthenticated=false
将用户名改成lisi:
simpleAccountRealm.addAccount("lisi","1234");
用户不存在
用户的认证状态:isAuthenticated=false
执行logout()方法
用户的认证状态:isAuthenticated=false
2. shiro用户授权
做权限管理的时候,分为两块,一个认证,一个是授权,认证是判断用户是否账号密 码正确,授权是判断用户登入以后有什么权限。
@SpringBootTest
class ShiroApplicationTests {
@Test
public void authentication1(){
//构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//创建一个SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加一个测试账号、和所拥有的角色(后面可以做成读取动态读取数据库),user拥有admin角色
simpleAccountRealm.addAccount("zhangsan","123456","admin","user");
//设置realm
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码(用户输入的用户名密码)生成token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
// 登录,提交认证
subject.login(token);
//检测用户是否拥有传入的角色,即只要有一个不是用户所拥有的角色就会抛出异常。
subject.checkRoles("admin","user");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名密码不匹配");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
System.out.println("执行logout()方法");
subject.logout();
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
}
}
用户的认证状态:isAuthenticated=true
执行logout()方法
用户的认证状态:isAuthenticated=false
将用户改成test,看其是否有admin的权限:
subject.checkRoles("admin","test");
用户没有权限
用户的认证状态:isAuthenticated=true
执行logout()方法
用户的认证状态:isAuthenticated=false
2. SpringBoot 使用IniRealm进行认证授权
SimpleAccountRealm 在程序中写死了用户安全数据,接下来我们使用.ini
将数据移到配置文件中。IniRealm是Shiro提供一种Realm实现。用户、角色、权限等信息集中在一个.ini
文件那里。
在 resources 目录下创建一个 shiro.ini 文件
# 账号信息
# 账号=密码,角色
[users]
test=123456,test
admin=123456,admin
# 角色信息
# 角色=权限1,权限2
[roles]
test=user:list,user:deleted,user:edit
# 拥有所有权限
admin= *
测试:
@SpringBootTest
class ShiroApplicationTests {
@Test
public void testIniRealm(){
//配置文件中的用户权限信息,文件在类路径下
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
//构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置realm
defaultSecurityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
try {
//主体提交认证请求
subject.login(token);
//检查是否有角色,判断该用户是否拥有 test 角色
subject.checkRoles("test");
//检查是否拥有权限,检查用户是否拥有 user:list 的权限
subject.checkPermissions("user:list");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
}
3. Spring Boot 使用 JdbcRealm 进行认证授权
把用户安全信息(相应的角色/权限)配置在 .ini 文件,使用 IniRealm 去读取 .ini 文件获得用户的安全信息。也是 有局限性的。因为我们得事先把所有用户信息配置在.ini 文件,这样显然是行不通的,我们的系统用户都是动态的不固定的,它的一些 用户信息权限信息都是变化的,所以固定在.ini 配置文件显然是行不通的。这些数据通常我们都是把它存入到DB 中,那shiro 有没有提 供直接从DB读取用户安全信息的域呢 ? (Realm)
shiro 作为一个优秀的开源框架,显然是可以的,即 JdbcRealm。
1. 数据库驱动
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
2. 数据库表结构
下面我们创建一个名为 shiro 的数据库、分别创建三张表 users、user_roles、roles_permissions
CREATE DATABASE IF NOT EXISTS shiro DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(25) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_name` varchar(25) DEFAULT NULL,
`username` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `roles_permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`permission` varchar(255) DEFAULT NULL,
`role_name` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
USE shiro;
INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('1', 'admin', '123456');
INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('2', 'test', '123456');
INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('1', 'admin', 'admin');
INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('2', 'test', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('1','user:deleted', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('2','user:list', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('3', '*','admin');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('4','user:edit', 'test');
一个用户对应多个权限,一个权限对应多个用户,多对多的关系
3. 创建 testJdbcRealm方法
@SpringBootTest
class ShiroApplicationTests {
@Test
public void testJdbcRealm(){
//配置数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
//配置文件中的用户权限信息,文件在类路径下
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
//使用JdbcRealm下面的值需要为true,不然无法查询用户权限
jdbcRealm.setPermissionsLookupEnabled(true);
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
//检查是否拥有角色
subject.checkRoles("admin");
//检查是否拥有权限
subject.checkPermissions("user:delete");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
}
用户的认证状态:isAuthenticated=true
当用户 test 登录进来的时候:
UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
用户的认证状态:isAuthenticated=true
用户没有权限
通过源码可以看出JdbcRealm 已经帮我们写好查询语句了,所以我们就要在数据库创建与之对应的表结构,这样才能查出数据,但是这里只能使用他默认的 sql,在实际的开发中,我们不可能就简单的使用 JdbcRealm 默认的 sql 语句,而是自己自定义的 sql 语句,更多时候我们的数据库以及数据表都是根据业务需要自己创建的,而不是默认的数据库表。
4. 更改数据库表名
USE shiro;
ALTER TABLE users RENAME sys_users;
ALTER TABLE user_roles RENAME sys_user_roles;
ALTER TABLE roles_permissions RENAME sys_roles_permissions;
@SpringBootTest
class ShiroApplicationTests {
@Test
public void testJdbcRealm(){
//配置数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
//配置文件中的用户权限信息,文件在类路径下
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
//使用JdbcRealm下面的值需要为true,不然无法查询用户权限
jdbcRealm.setPermissionsLookupEnabled(true);
//使用自定义sql查询
String sql = "select password from sys_users where username=?";
jdbcRealm.setAuthenticationQuery(sql);
String roleSQl = "select role_name from sys_user_roles where username=?";
jdbcRealm.setUserRolesQuery(roleSQl);
String permissionSql = "select permission from sys_roles_permissions where role_name=?";
jdbcRealm.setPermissionsQuery(permissionSql);
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
//检查是否拥有角色
subject.checkRoles("admin");
//检查是否拥有权限
subject.checkPermissions("user:delete");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
}
4. Spring Boot 使用自定义 Realm 进行认证授权
虽然 jdbcRealm 已经实现了从数据库中获取用户的验证信息,但是 jdbcRealm灵活性也是稍差一些的,如果要实现自己的一些特殊应 用时将不能支持,这个时候可以通过自定义realm来实现身份的认证功能。
通常自定义Realm只需要继承:AuthorizingRealm重写 doGetAuthenticationInfo(用户认证)、doGetAuthorizationInfo(用户授权) 这 两个方法即可。
public class CustomRealm extends AuthorizingRealm {
/**
* 模拟数据库中的用户名和密码
*/
private Map<String, String> userMap =new HashMap<>();
{
userMap.put("admin","123456");
userMap.put("test","123456");
}
/**
* 获取用户认证信息:用户名+密码
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String)authenticationToken.getPrincipal();
String password = getPasswordByUsername(username);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,getName());
return simpleAuthenticationInfo;
}
/**
* 获取用户授权信息:用户+角色+权限
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) principalCollection.getPrimaryPrincipal();
//从数据库或者缓存中获取角色数据
List<String> roles = getRolesByUsername(username);
//从数据库或者缓存中获取权限数据
List<String> permissions = getPermissionsByUsername(username);
//创建AuthorizationInfo,并设置角色和权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(permissions);
simpleAuthorizationInfo.addRoles(roles);
return simpleAuthorizationInfo;
}
/**
* 通过数据库,根据用户名获取权限信息
* @param username
* @return
*/
private List<String> getPermissionsByUsername(String username) {
List<String> permissions = new ArrayList<>();
/**
* 只有是 admin 用户才有 新增、删除权限
*/
if(username.equals("admin")){
permissions.add("user:delete");
permissions.add("user:add");
}
permissions.add("user:edit");
permissions.add("user:list");
return permissions;
}
/**
* 通过数据库,根据用户名获取角色信息
* @param username
* @return
*/
List<String> getRolesByUsername(String username){
List<String> roles = new ArrayList<>();
if(username.equals("admin")){
roles.add("admin");
}
roles.add("test");
return roles;
}
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
}
测试:
@SpringBootTest
class ShiroApplicationTests {
@Test
public void testCustomRealm(){
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
CustomRealm customRealm = new CustomRealm();
defaultSecurityManager.setRealm(customRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
//检查是否拥有角色
subject.checkRoles("admin");
//检查是否拥有权限
subject.checkPermissions("user:delete");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
}
shiro 更多的是帮助我们完成验证过程。我们需要从数据库查询当前用户的角色、权限,把这些信息告诉 shiro 框架。当我们执行用户认证的时候首先调用 doGetAuthenticationInfo 进行获取用户认证信息,当我们要校验权限的时候 就会执行 doGetAuthorizationInfo 进行获取用户授权信息。
5. SpringBoot整合shiro之盐值加密认证详解
自定义 Realm,里面的用户认证所使用的密码都是明文,这种方式是不可取的往往我们在实战开发中用户的密码 都是以密文形势进行存储,并且要求加密算法是不可逆的,著名的加密算法有MD5、SHA1等。
public class CustomRealm extends AuthorizingRealm {
/**
* 获取用户认证信息:用户名+密码
* @param authenticationToken
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String)authenticationToken.getPrincipal();
String password = getPasswordByUsername(username);
String matcherPassword = getPasswordMatcher(password);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,matcherPassword,getName());
return simpleAuthenticationInfo;
}
/**
* 获取密文密码
* @param currentPassword
*/
private String getPasswordMatcher(String currentPassword){
return new Md5Hash(currentPassword, null,2).toString();
}
// 省略...
}
@Test
public void testCustomRealm(){
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
CustomRealm customRealm = new CustomRealm();
// 加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 采用MD5加密
matcher.setHashAlgorithmName("md5");
// 设置加密次数
matcher.setHashIterations(2);
customRealm.setCredentialsMatcher(matcher);
defaultSecurityManager.setRealm(customRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
//检查是否拥有角色
subject.checkRoles("admin");
//检查是否拥有权限
subject.checkPermissions("user:delete");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
当两个用户的密码相同时,单纯使用不加盐的MD5加密方式,会发现数据库中存在相同结构的密码,这样也是不安全的。我们希望即 便是两个人的原始密码一样,加密后的结果也不一样。如何做到呢?其实就好像炒菜一样,两道一样的鱼香肉丝,加的盐不一样,炒 出来的味道就不一样。MD5加密也是一样,需要进行盐值加密。
public class CustomRealm extends AuthorizingRealm {
/**
* 获取用户认证信息:用户名+密码
* @param authenticationToken
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String)authenticationToken.getPrincipal();
String password = getPasswordByUsername(username);
String salt = UUID.randomUUID().toString().substring(5);
String matcherPassword = getPasswordMatcher(password,salt);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,matcherPassword,getName());
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
return simpleAuthenticationInfo;
}
/**
* 获取密文密码
* @param currentPassword
* @param salt
*/
private String getPasswordMatcher(String currentPassword,String salt){
return new Md5Hash(currentPassword, salt).toString();
}
// 省略...
}
测试:
@Test
public void testCustomRealm(){
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
CustomRealm customRealm = new CustomRealm();
// 加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 采用MD5加密
matcher.setHashAlgorithmName("md5");
// 设置加密次数
matcher.setHashIterations(1);
customRealm.setCredentialsMatcher(matcher);
defaultSecurityManager.setRealm(customRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
//检查是否拥有角色
subject.checkRoles("admin");
//检查是否拥有权限
subject.checkPermissions("user:delete");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}