Apache Shiro
:是一款 Java 安全框架,不依赖任何容器,可以运行在 Java 项目中,它的主要作用是做身份认证、授权、会话管理和加密等操作。
其实不用 Shiro,我们使用原生 Java API 就可以实现安全管理,使用过滤器去拦截用户的各种请求,然后判断是否登录、是否拥有权限即可。但是对于一个大型的系统,分散去管理编写这些过滤器的逻辑会比较麻烦,不成体系,所以需要使用结构化、工程化、系统化的解决方案。
与 Spring Security 相比,shiro 属于轻量级框架,相对于 Spring Security 简单的多,也没有那么复杂。
Shiro 的运行机制如下图所示:
1)UsernamePasswordToken
:封装用户登录信息,根据用户的登录信息创建令牌 token,用于验证令牌是否具有合法身份以及相关权限。
2)SecurityManager
:核心部分,负责安全认证与授权。
3)Subject
:一个抽象概念,包含了用户信息。
4)Realm
:开发者自定义的模块,根据项目的需求,验证和授权的逻辑在 Realm 中实现。
5)AuthenticationInfo
:用户的角色信息集合,认证时使用。
6)AuthorizationInfo
:角色的权限信息集合,授权时使用。
7)DefaultWebSecurityManager
:安全管理器,开发者自定义的 Realm 需要注入到 DefaultWebSecurityManager 中进行管理才能生效。
8)ShiroFilterFactoryBean
:过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由 ShiroFilterFactoryBean 创建一个个 Filter 对象来完成。
SpringBoot 集成 Shiro 思路图如下:
项目包结构如下:
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-starterartifactId>
<version>1.12.0version>
dependency>
自定义 Realm 主要实现了两大模块:
认证
:根据用户名,查询密码,然后封装返回 SimpleAuthenticationInfo 认证信息。授权
:根据用户名,查询角色和权限,然后封装返回 SimpleAuthorizationInfo 授权信息。CustomRealm.java
import com.demo.module.entity.User;
import com.demo.module.service.UserService;
import com.demo.util.SpringUtils;
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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
/**
* @Title CustomRealm
*
@Description 自定义Realm
*
* @author ACGkaka
* @date 2023/10/15 12:42
*/
public class CustomRealm extends AuthorizingRealm {
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户名
String principal = (String) authenticationToken.getPrincipal();
// 根据用户名查询数据库
UserService userService = SpringUtils.getBean(UserService.class);
User user = userService.findByUsername(principal);
if (user != null) {
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), this.getName());
}
return null;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取用户名
String principal = (String) principalCollection.getPrimaryPrincipal();
if ("admin".equals(principal)) {
// 管理员拥有所有权限
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("admin");
info.addStringPermission("admin:*");
info.addRole("user");
info.addStringPermission("user:find:*");
return info;
}
return null;
}
}
配置类中指定了如下内容:
ShiroConfig.java
import com.demo.config.shiro.realm.CustomRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @Title ShiroConfig
*
@Description Shiro配置类
*
* @author ACGkaka
* @date 2023/10/15 12:44
*/
@Configuration
public class ShiroConfig {
/**
* ShiroFilter过滤所有请求
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给ShiroFilter配置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置系统公共资源、系统受限资源(公共资源必须在受限资源上面,不然会造成死循环)
Map<String, String> map = new HashMap<>();
// 系统公共资源
map.put("/login", "anon");
map.put("/register", "anon");
map.put("/static/**", "anon");
// 受限资源
map.put("/**", "authc");
// 设置认证界面路径
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 创建安全管理器
*/
@Bean
public DefaultWebSecurityManager securityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
/**
* 创建自定义Realm
*/
@Bean
public Realm realm() {
CustomRealm realm = new CustomRealm();
// 设置使用哈希凭证匹配
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置使用MD5加密算法
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置散列次数:加密次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
return realm;
}
}
静态资源映射主要将 css
、js
等静态文件夹映射到浏览器端,方便页面加载。
WebConfiguration.java
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.ResourceUtils;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* SpringBoot静态路径配置
*
* @author ACGkaka
* @date 2019/11/27 15:38
*/
@Configuration
@Primary
public class WebConfiguration implements WebMvcConfigurer {
/**
* 访问外部文件配置
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/css/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/css/");
registry.addResourceHandler("/static/js/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/js/");
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}
主要用于进行 thymeleaf 模板引擎的页面跳转,以及登录、注册、退出登录等功能的实现。
AuthController.java
import com.demo.module.entity.User;
import com.demo.module.service.UserService;
import lombok.AllArgsConstructor;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import javax.annotation.Resource;
/**
* @Title IndexController
*
@Description 鉴权Controller
*
* @author ACGkaka
* @date 2019/10/23 20:23
*/
@Controller
@AllArgsConstructor
public class AuthController {
@Resource
private UserService userService;
/**
* 默认跳转主页
*/
@GetMapping("/")
public String showIndex() {
return "redirect:/index";
}
/**
* 主页
*/
@GetMapping("/index")
public String index() {
return "index.html";
}
/**
* 主页
*/
@GetMapping("/login")
public String login() {
return "login.html";
}
/**
* 注册页
*/
@GetMapping("/register")
public String register() {
return "/register.html";
}
/**
* 登录
*/
@PostMapping("/login")
public String login(String username, String password) {
// 获取主题对象
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username, password));
System.out.println("登录成功!!!");
return "redirect:/index";
} catch (UnknownAccountException e) {
System.out.println("用户错误!!!");
} catch (IncorrectCredentialsException e) {
System.out.println("密码错误!!!");
}
return "redirect:/login";
}
/**
* 注册
*/
@PostMapping("/register")
public String register(User user) {
userService.register(user);
return "redirect:/login.html";
}
/**
* 退出登录
*/
@GetMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login.html";
}
}
封装了用户的基本属性。
User.java
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
*
* 用户表
*
*
* @author ACGkaka
* @since 2021-04-25
*/
@Data
@TableName("t_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 随机盐
*/
private String salt;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
封装了注册(用户新增)和根据用户名查找接口。
UserService.java
import com.baomidou.mybatisplus.extension.service.IService;
import com.demo.module.entity.User;
/**
* 用户表 服务类
*/
public interface UserService extends IService<User> {
/**
* 注册
* @param user 用户
*/
void register(User user);
/**
* 根据用户名查询用户
* @param principal 用户名
* @return 用户
*/
User findByUsername(String principal);
}
实现了注册(用户新增)和根据用户名查找功能。
UserServiceImpl.java
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.demo.module.entity.User;
import com.demo.module.mapper.UserMapper;
import com.demo.module.service.UserService;
import com.demo.util.SaltUtils;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.stereotype.Service;
/**
*
* 用户表 服务实现类
*
*
* @author ACGkaka
* @since 2021-04-25
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public void register(User user) {
// 注册用户
checkRegisterUser(user);
// 1.生成随机盐
String salt = SaltUtils.getSalt(8);
// 2.将随机盐保存到数据库
user.setSalt(salt);
// 3.明文密码进行MD5 + salt + hash散列次数
Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
user.setPassword(md5Hash.toHex());
// 4.保存用户
this.save(user);
}
@Override
public User findByUsername(String principal) {
// 根据用户名查询用户
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, principal);
return this.getOne(queryWrapper);
}
// ------------------------------------------------------------------------------------------
// 内部方法
// ------------------------------------------------------------------------------------------
/**
* 校验注册用户
* @param user 用户
*/
private void checkRegisterUser(User user) {
if (user == null) {
throw new RuntimeException("用户信息不能为空");
}
if (user.getUsername() == null || "".equals(user.getUsername())) {
throw new RuntimeException("用户名不能为空");
}
if (user.getPassword() == null || "".equals(user.getPassword())) {
throw new RuntimeException("密码不能为空");
}
// 判断用户名是否已存在
User existUser = this.getOne(new UpdateWrapper<User>().eq("username", user.getUsername()));
if (existUser != null) {
throw new RuntimeException("用户名已存在");
}
}
}
在自定义 Realm 配置好用户权限后,用于测试对用户权限和角色权限的控制。
OrderController.java
import com.demo.common.Result;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Title OrderController
*
@Description 订单控制器
*
* @author ACGkaka
* @date 2023/10/15 17:15
*/
@RestController
@RequestMapping("/order")
public class OrderController {
/**
* 新增订单(代码实现权限判断)
*/
@GetMapping("/add")
public Result<Object> add() {
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {
return Result.succeed().setData("新增订单成功");
} else {
return Result.failed().setData("操作失败,无权访问");
}
}
/**
* 编辑订单(注解实现权限判断)
*/
@RequiresRoles({"admin", "user"}) // 用来判断角色,同时拥有admin和user角色才能访问
@RequiresPermissions("user:edit:01") // 用来判断权限,拥有user:edit:01权限才能访问
@GetMapping("/edit")
public String edit() {
System.out.println("编辑订单");
return "redirect:/index";
}
}
访问地址:http://localhost:8081,我们配置了根路径默认访问主页,由于用户没有登录,会默认跳转登录页面。
访问地址:http://localhost:8081/register,输入用户名和密码后,系统会在数据库中新增用户信息,自动跳转登录页面,输入刚才的用户名和密码即可登录。
输入正确的用户名密码,即可登录至主页。
我们在主页中加入了 OrderController 中的接口,用于测试权限校验。
当使用非 admin 用户进行登录的时候,代码实现权限判断的 /add
接口,报错如下:
Shiro 注解实现权限判断的 /edit
接口,报错如下:
从提示信息可以看到,返回了状态码 500
,报错信息为:当前用户的主体没有 admin 权限。
由此可见,两种鉴权结果均成功生效,具体使用哪一种由业务场景来定。
由于 Shiro 核心是通过 Session 来实现用户登录的,所以有很多场景不支持,比如:
1)Session 默认存储在内存中,重启应用后所有用户登录状态会失效;
2)默认配置不支持分布式,需要分布式部署的时候,可以通过 Redis
或数据库
方式来同步 Session 会话。
整理完毕,完结撒花~ 🌻
参考地址:
1.SpringBoot之整合Shiro(最详细),https://blog.csdn.net/Yearingforthefuture/article/details/117384035
2.超详细 Spring Boot 整合 Shiro 教程!https://cloud.tencent.com/developer/article/1643122
3.springboot整合shiro(完整版),https://blog.csdn.net/w399038956/article/details/120434244
4.Shiro安全框架【快速入门】就这一篇!https://zhuanlan.zhihu.com/p/54176956