一、概念
介绍
Apache Shiro是一个强大灵活的开源安全框架,提供授权、会话管理以及密码加密等功能。
与Spring Security对比
劣势:
-
Spring Security基于Spring开发,项目若使用Spring作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发
-
SpringSecurity功能比Shiro更加丰富些,例如安全维护方面
-
Spring Security社区资源相对比Shiro更加丰富
优势:
-
Shiro的配置和使用比较简单,Spring Security上手复杂
-
Shiro依赖性低,不需要任何框架和容器,可以独立运行。Spring Security需要依赖Spring容器
-
Shiro不仅仅是可以使用在web中,它可以工作在任何应用环境中。在集群会话时Shiro最重要的一个好处或许就是它的会话可以独立于容器。
基本功能
基础功能:
-
认证(Authentication): 或“登录”,用以验证用户身份。
-
授权(Authorization): 访问控制, 比如决定谁可以访问某些资源。
-
会话管理(Session Management): 管理用户相关的session,即使是在非web或EJB应用中。
-
加密(Cryptography):可以非常方便地使用(各种)加密算法保证数据的安全。
其他功能:
-
对Web的支持(Web Support): Shiro自带的支持Web的API可以很容易地保证web应用的安全。
-
缓存(Caching):缓存在Apache Shiro的API中是“一等公民”,可以保证操作的快速高效。
-
并发(Concurrency): Apache Shiro的并发功能支持开发多线程的应用。
-
测试(Testing):对测试的支持可以帮助你编写单元测试与集成测试。
-
“以...(身份)运行”(Run As):允许一个用户使用另外某个用户的身份(执行操作),这个功能常用于管理场景中(比如“以管理员身份运行”)。
-
“自动登陆”(Remember Me):可以跨会话记住用户身份,只在某些特殊情况下才需要强制登录。
二、基本使用
在maven中使用
<!--Shiro安全框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.1</version>
</dependency>
验证登录
@Test
void testShiro(){
//1.初始化SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//1.1获取认证管理器
SecurityManager securityManager = factory.getInstance();
//1.2将认证管理器添加到SecurityUtils中
SecurityUtils.setSecurityManager(securityManager);
//2.获取Subject对象
Subject subject = SecurityUtils.getSubject();
//3.创建token对象,web应用用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","z3");
//4.完成登录
try {
subject.login(token);
System.out.println("登录成功");
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}catch (AuthenticationException e) {
e.printStackTrace();
}
}
判断用户角色
//5.判断角色
boolean role = subject.hasRole("role1");
System.out.println("是否含有'role1'角色 = " + role);
判断角色权限
//6.判断角色权限
//6.1有返回值
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否拥有'user:insert'权限 = " + permitted);
//6.2 无返回值,没有权限时会抛异常
subject.checkPermission("user:insert");
信息加密
一般数据库中都不会将密码明文存储,而是存储加密后的密码。在认证认证过程中,会将用户输入的密码进行相同的加密过程,如果用户输入的密码加密后和数据库中的一致,则认证成功,否则认证失败。
@Test
void testShiroMD5() {
//1.密码明文
String password = "z3";
//2.使用MD5加密
Md5Hash passwordMD5 = new Md5Hash(password);
System.out.println("加密后的密码 =" + passwordMD5.toHex());
//3.带盐的MD5加密
Md5Hash passMD5Salt = new Md5Hash(password,"salt");
System.out.println("带盐的MD5加密 = " + passMD5Salt.toHex());
//4.为了保证数据安全,可以对密码进行多次加密
Md5Hash passMD5SaltPlus = new Md5Hash(password,"salt",3);
System.out.println("迭代三次带盐的MD5加密 = " + passMD5SaltPlus.toHex());
//5.使用Md5Hash的父类SimpleHash进行加密
SimpleHash simpleHash = new SimpleHash("MD5", password, "salt", 3);
System.out.println("使用SimpleHash进行加密 = " + simpleHash);
}
自定义登录认证
Shiro默认的登录认证是不带加密的,要实现加密认证需要自定义Realm来实现登录认证
自定义登录认证
.ini配置
[main]
# 配置MD5散列凭证匹配器
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
# 设置散列迭代次数,用于增强密码安全性
md5CredentialsMatcher.hashIterations=3
# 配置自定义Realm,com.womeng.mp.shiro.realm.MyRealm是自定义Realm的完整类名
myRealm=com.womeng.mp.shiro.realm.MyRealm
# 将配置的MD5散列凭证匹配器设置到自定义Realm中
myRealm.credentialsMatcher=$md5CredentialsMatcher
# 将自定义Realm添加到SecurityManager的realms列表中
securityManager.realms=$myRealm
三、SpringBoot3整合Shiro
定义Service接口
PS:需要事先准备好数据库和实体类
-
定义Service接口
public interface IUserService extends IService<User> { User getUserInfoByName(String name); }
-
实现接口
@Service @RequiredArgsConstructor//lombok注解:对一开始需要初始化的变量进行初始化,这里对应userMapper public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { //注入mapper private final UserMapper userMapper; @Override public User getUserInfoByName(String name) { //1.创建查询条件 QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("name",name); //2.查询并返回 return userMapper.selectOne(wrapper); } }
添加依赖
在pom.xml项目中添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<classifier>jakarta</classifier>
<version>1.11.0</version>
<!-- 排除仍使用了javax.servlet的依赖 -->
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入适配jakarta的依赖包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
<version>1.11.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
这里我没有直接导入shiro-spring-boot-web-starter
,即下面这个maven坐标
<!--Shiro安全框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>2.0.1</version>
</dependency>
是因为在启动过程中发生报错
Type javax.servlet.Filter not present
大概原因是Spring Boot 3.0 使用了Servlet 5.0,而javax.servlet此时已经迁移到了jakarta.servlet中。Shiro已经提供了适配Servlet 5.0 的依赖包,使用<classifier>
标签即可选取适配版本,不过部分Shiro包中仍嵌套依赖了一些没有适配jakarta的依赖包,所以我们需要使用<exclude>
将其排除,再引入同版本的jakarta适配包
参考:Java17和springboot3.0使用shiro报ClassNotFoundException_shiro java17-CSDN博客
自定义Realm
import com.womeng.mp.entity.User;
import com.womeng.mp.service.IUserService;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor//lombok注解:对一开始需要初始化的变量进行初始化,这里对应userService
public class MyRealm extends AuthorizingRealm {
private final IUserService userService;
//1.自定义授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;//因为暂时用不到,所以直接返回null
}
//2.自定义登录认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取用户输入身份信息
String name = authenticationToken.getPrincipal().toString();
//2.获取数据库用户信息,可以自己建一张只有name和password的表,仅用于测试
User user = userService.getUserInfoByName(name);//这个是自定义的Service接口
//3.非空判断
if (user != null){
return new SimpleAuthenticationInfo(
authenticationToken.getPrincipal(),//身份信息
user.getPassword(),//获取数据库中加密好的密码
ByteSource.Util.bytes("salt"),//盐值,要注意,一般要从数据库中获取
authenticationToken.getPrincipal().toString()//身份信息的字符串形式
);
}
return null;
}
}
创建配置类
要想在Spring项目中使用Shiro,还需要创建配置类,可以新建config包放在项目根目录下(跟application启动类同级)
import com.womeng.mp.shiro.realm.MyRealm;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class ShiroConfig {
private final MyRealm myRealm;
//配置SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1.创建SecurityManager
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//2.创建加密对象,设置加密规则,要注意,这里的加密规则要保证跟数据库中密码的加密规则一致
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//2.1 采用MD5加密
matcher.setHashAlgorithmName("md5");
//2.2 设置迭代次数
matcher.setHashIterations(3);
//3.将加密规则存储到MyRealm中
myRealm.setCredentialsMatcher(matcher);
//4.将MyRealm存入SecurityManager
securityManager.setRealm(myRealm);
//5.将securityManager添加到SecurityUtils中进行全局配置
SecurityUtils.setSecurityManager(securityManager);
//6.返回,不返回不行,springboot会报错,即使设置成void
return securityManager;
}
//配置Shiro内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/user/userLogin","anon");
definition.addPathDefinition("/login","anon");
//设置需要进行登录认证才能访问的资源
definition.addPathDefinition("/**","authc");
return definition;
}
}
上面这段代码中的第5点,如果不在这里设置,后面使用SecurityUtils.getSubject()时会找不到securityManager,具体报错信息如下:
No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
我能想到的就是在这里将securityManager添加到SecurityUtils中进行全局配置。
测试
-
定义Controller接口
@RestController @RequestMapping("/user") @RequiredArgsConstructor //lombok注解:对一开始需要初始化的变量进行初始化 public class UserController { //注意final修饰!!! private final IUserService userService; @GetMapping("userLogin") public String userLogin(String name , String pwd){ //1.获取subject对象 Subject subject = SecurityUtils.getSubject(); //2.封装请求数据到token UsernamePasswordToken token = new UsernamePasswordToken(name, pwd); //3.调用login方法进行登录认证 try { subject.login(token); return "登录成功"; } catch (AuthenticationException e) { e.printStackTrace(); System.out.println("登录失败"); return "登录失败"; } } }
-
运行启动类,并在浏览器或其他测试工具中访问
http://localhost:8080/user/userLogin?name=Xxx&pwd=Xxx
PS:将Xxx换成自己的测试字段,如果不出意外,会显示
登录成功
后话
在测试过程中还出现过如下报错
No realms have been configured! One or more realms must be present to execute an authentication attempt.
网上找了好多办法,都没用。后面发现是Service接口的问题。
一开始的接口是这样子的,想着不会硬编码,但是会报错
@Override
public User getUserInfoByName(String name) {
//1.创建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.eq(name != null, User::getName, name);
//2.查询并返回
return userMapper.selectOne(wrapper);
}
改成下面这样就没事了
@Override
public User getUserInfoByName(String name) {
//1.创建查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("name",name);
//2.查询并返回
return userMapper.selectOne(wrapper);
}
实体类的字段和数据库中的字段都是name,不清楚为什么会报错。