Apache Shiro是一个强大灵活的开源安全框架,提供授权、会话管理以及密码加密等功能。
劣势:
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):可以跨会话记住用户身份,只在某些特殊情况下才需要强制登录。
org.apache.shiro
shiro-core
1.9.1
@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
PS:需要事先准备好数据库和实体类
定义Service接口
public interface IUserService extends IService {
User getUserInfoByName(String name);
}
实现接口
@Service
@RequiredArgsConstructor//lombok注解:对一开始需要初始化的变量进行初始化,这里对应userMapper
public class UserServiceImpl extends ServiceImpl implements IUserService {
//注入mapper
private final UserMapper userMapper;
@Override
public User getUserInfoByName(String name) {
//1.创建查询条件
QueryWrapper wrapper = new QueryWrapper().eq("name",name);
//2.查询并返回
return userMapper.selectOne(wrapper);
}
}
在pom.xml项目中添加依赖
org.apache.shiro
shiro-spring
jakarta
1.11.0
org.apache.shiro
shiro-core
org.apache.shiro
shiro-web
org.apache.shiro
shiro-core
jakarta
1.11.0
org.apache.shiro
shiro-web
jakarta
1.11.0
org.apache.shiro
shiro-core
这里我没有直接导入shiro-spring-boot-web-starter
,即下面这个maven坐标
org.apache.shiro
shiro-spring-boot-web-starter
2.0.1
是因为在启动过程中发生报错
Type javax.servlet.Filter not present
大概原因是Spring Boot 3.0 使用了Servlet 5.0,而javax.servlet此时已经迁移到了jakarta.servlet中。Shiro已经提供了适配Servlet 5.0 的依赖包,使用
标签即可选取适配版本,不过部分Shiro包中仍嵌套依赖了一些没有适配jakarta的依赖包,所以我们需要使用
将其排除,再引入同版本的jakarta适配包
参考:Java17和springboot3.0使用shiro报ClassNotFoundException_shiro java17-CSDN博客
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 wrapper = new LambdaQueryWrapper()
.eq(name != null, User::getName, name);
//2.查询并返回
return userMapper.selectOne(wrapper);
}
改成下面这样就没事了
@Override
public User getUserInfoByName(String name) {
//1.创建查询条件
QueryWrapper wrapper = new QueryWrapper().eq("name",name);
//2.查询并返回
return userMapper.selectOne(wrapper);
}
实体类的字段和数据库中的字段都是name,不清楚为什么会报错。