1.简介
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
shiro官网
2.核心组件
三个核心组件:Subject, SecurityManager 和 Realms.
- Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。 - SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
- Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
3.使用版本
- springboot版本是: 2.3.0.RELEASE
- shiro-spring-boot-web-starter 版本是: 1.5.3
4.shiro实战应用
4.1.目录结构
4.2.引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.3</version>
</dependency>
4.3.实体类
4.3.1.User类
存放用户的信息
public class User implements Serializable {
private Integer id;
private String username;
private String password;
/**
* 密码加密用的盐
*/
private String salt;
/**
* 用户拥有的角色
*/
private Set<Role> roles;
//省略... get set方法
}
4.3.2.Role类
存放角色的信息
public class Role {
public Role(String roleName, Set<Permission> permissions) {
this.roleName=roleName;
this.permissions=permissions;
}
private Integer id;
private String roleName;
/**
* 角色拥有的权限
*/
private Set<Permission> permissions;
//省略... get set方法
}
存放权限信息
4.3.3.Permission类
public class Permission {
public Permission(String permissionName){
this.permissionName=permissionName;
}
private Integer id;
private String permissionName;
//省略... get set方法
}
4.4.权限认证管理类
public class CustomRealm extends AuthorizingRealm {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
@Autowired
private UserRepository userRepository;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> permissionList = new HashSet<>();
Set<String> roleNameList = new HashSet<>();
for(Role role: user.getRoles()){
for(Permission permission : role.getPermissions()){
//添加权限
permissionList.add(permission.getPermissionName());
}
//添加角色
roleNameList.add(role.getRoleName());
}
info.setStringPermissions(permissionList);
info.setRoles(roleNameList);
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken;
User user=userRepository.findByUserName(upt.getUsername());
if(user==null){
//帐号不存在
logger.info("account does not exist!");
throw new UnknownAccountException();
}
logger.info("account exist ,Certification...!");
//把数据库的密码和(当前登录输入的密码+盐 后加密的密码比对)
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),getName());
return authenticationInfo;
}
}
4.5.Shiro配置类
- userRealm() 配置自定义权限认证管理
- shiroFilterFactoryBean() 非常重要,配置资源过滤和跳转路径
- credentialsMatcher()设置密码匹配规则,本处使用的Sha256Hash算法
完整代码如下
@Configuration
public class ShiroConfig {
/**
* 配置自定义Realm
*/
@Bean
public CustomRealm userRealm() {
CustomRealm userRealm = new CustomRealm();
//配置使用哈希密码匹配
userRealm.setCredentialsMatcher(credentialsMatcher());
return userRealm;
}
/**
* 设置对应的过滤条件和跳转条件
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
//没有登录的用户请求需要登录的页面时自动跳转到登录页面。
shiroFilterFactoryBean.setLoginUrl("/login");
//filterChainDefinitionMap 配置过滤规则,从上到下的顺序匹配。
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//authc是需要认证后访问的接口,anon是放行的接口
filterChainDefinitionMap.put("/user/**", "authc");
filterChainDefinitionMap.put("/admin/**", "authc");
//除了user和admin路径开头的接口,其它资源都开放
filterChainDefinitionMap.put("/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面。
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
return shiroFilterFactoryBean;
}
/**
* 设置用于匹配密码的CredentialsMatcher
*/
@Bean
public HashedCredentialsMatcher credentialsMatcher() {
// 散列算法,这里使用更安全的sha256算法
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
// 数据库存储的密码字段使用HEX还是BASE64方式加密
credentialsMatcher.setStoredCredentialsHexEncoded(false);
// 散列迭代次数
credentialsMatcher.setHashIterations(1024);
return credentialsMatcher;
}
/**
* 配置security并设置userReaml,避免xxxx required a bean named 'authorizer' that could not be found.的报错
*/
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
app.setProxyTargetClass(true);
return app;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
}
4.6.密码加盐加密工具类
public class PasswordHelper {
private static RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
//加密算法
private static String algorithmName = Sha256Hash.ALGORITHM_NAME;
//迭代次数
private static int hashIterations = 1024;
//生成随机的盐
public static String randomSalt(){
return randomNumberGenerator.nextBytes().toBase64();
}
public static String encryptPassword(String username, String password,String salt) {
String encryPassword= new SimpleHash(algorithmName, password,
ByteSource.Util.bytes(salt), hashIterations).toBase64();
System.out.printf("username=%s ,password=%s , salt=%s , encryPassword=%s ",username,password,salt,encryPassword);
return encryPassword;
}
public static void main(String[] args) {
String username="admin";
//盐前面必须拼接用户名,不然密码认证的时候会失败 (盐需要存在数据库)
String salt=username+randomSalt();
String newpassword=encryptPassword(username,"123",salt);
}
}
4.7.持久化操作类
为方便测试,此处制造假数据.实际场景需要重数据库获取
/**
* @author Dominick Li
* @description 初始化用户数据,模拟数据库
**/
@Component
public class UserRepository {
HashMap<String, User> users = new HashMap();
{
//初始化权限
Set<Permission> permissions = new HashSet<>();
permissions.add(new Permission("save"));
permissions.add(new Permission("delete"));
permissions.add(new Permission("select"));
Set<Role> roles = new HashSet<>();
//初始化角色
roles.add(new Role("admin",permissions));
//初始化管理员
User user = new User();
user.setUsername("admin");
String userPassword="123";
//盐前面必须拼接用户名,不然密码认证的时候会失败 (盐需要存在数据库)
String salt=user.getUsername()+PasswordHelper.randomSalt();
user.setPassword(PasswordHelper.encryptPassword(user.getUsername(),userPassword,salt));
user.setSalt(salt);
user.setRoles(roles);
//初始化普通用户信息
Set<Permission> permissions2 = new HashSet<>();
permissions.add(new Permission("select"));
Set<Role> roles2 = new HashSet<>();
roles2.add(new Role("user",permissions2));
User user2 = new User();
user2.setUsername("test");
String user2Password="123";
String salt2=user2.getUsername()+PasswordHelper.randomSalt();
user2.setPassword(PasswordHelper.encryptPassword(user2.getUsername(),user2Password,salt2));
user2.setSalt(salt2);
user2.setRoles(roles2);
users.put("admin",user);
users.put("test",user2);
}
/**
* 模拟重数据库获取用户信息
*/
public User findByUserName(String username) {
return users.get(username);
}
public List<User> findAll() {
List<User> userList = new ArrayList<>();
for (User user : users.values()) {
userList.add(user);
}
return userList;
}
public void deleteByUserName(String username) {
users.remove(username);
}
}
4.8.登录接口
ResponseResult 是我自定义返回数据的map类,大家可以换成自己的返回类型
@PostMapping("/login.do")
@ResponseBody
public ResponseResult login(User user, HttpSession session) {
try {
//进行验证,这里可以捕获异常,然后返回对应信息
UsernamePasswordToken upt = new UsernamePasswordToken(user.getUsername(), user.getPassword());
SecurityUtils.getSubject().login(upt);
} catch (AuthenticationException e) {
e.printStackTrace();
return new ResponseResult(false, "账号或密码错误!");
} catch (AuthorizationException e) {
e.printStackTrace();
return new ResponseResult(false, "没有权限!");
}
return new ResponseResult(true,"登录成功!");
}
4.9.自定义全局异常处理器
@ControllerAdvice
public class GlobalExceptionController {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
private static final String ERROR_MESSAGE = "系统内部错误,请联系管理员!";
@ExceptionHandler(value = Exception.class)
public ModelAndView ExceptionHndler(Exception e) {
ModelAndView mv=new ModelAndView("error");
String msg=ERROR_MESSAGE;
if(e instanceof UnauthorizedException){
msg=e.getMessage();
}
mv.addObject("error",msg);
logger.error("global error:{}", e);
return mv;
}
}
4.10.资源认证拦截注解
4.10.1.@RequiresRoles (角色认证注解)
- value属性可以支持多个值,多个值写在{}里,单个值直接value = "角色名"即可
- logical属性 默认是 Logical.AND,需要多个角色都拥有才能访问,Logical.OR 表示只要拥有其中一个角色即可访问
@RequiresRoles( value = {"admin", "user"}, logical = Logical.OR)
@GetMapping("/user/index")
public String user(Model model)
{
List<User> users=userRepository.findAll();
model.addAttribute("userList",users);
return "user/index";
}
4.10.2.@RequiresPermissions(权限认证注解)
- value属性可以支持多个值,多个值写在{}里,单个值直接value = "权限名"即可
- logical属性 默认是 Logical.AND,需要多个权限都拥有才能访问,Logical.OR 表示只要拥有其中一个权限即可访问
@RequiresPermissions(value = {"delete","*"},logical = Logical.OR)
@GetMapping("/user/delete/{username}")
public String delete(@PathVariable String username){
userRepository.deleteByUserName(username);
return "forward:/user/index";
}
4.5.测试
教程配套代码已上传到github,https://github.com/Dominick-Li/springboot-master
4.5.1.user角色测试
使用普通用户登录,测试跨权限访问资源
帐号: test 密码: 123
4.5.1.1.测试跨角色访问
点击导航到管理员页面按钮,跳转管理员页面
因为管理员页面设置了只有拥有admin角色才能访问,test用户只有user角色,所以不能访问,重定向到了错误页
4.5.1.2.测试跨权限访问
点击删除按钮,删除用户
因为test用户没有delete或者*权限,所有无法执行删除用户操作
4.5.2.admin角色访问
帐号: admin 密码: 123
跳转管理员页面和删除用户操作都可以执行的。
5.项目配套代码
创作不易,要是觉得我写的对你有点帮助的话,麻烦在gitee上帮我点下 Star
【SpringBoot框架篇】其它文章如下,后续会继续更新。
- 1.搭建第一个springboot项目
- 2.Thymeleaf模板引擎实战
- 3.优化代码,让代码更简洁高效
- 4.集成jta-atomikos实现分布式事务
- 5.分布式锁的实现方式
- 6.docker部署,并挂载配置文件到宿主机上面
- 7.项目发布到生产环境
- 8.搭建自己的spring-boot-starter
- 9.dubbo入门实战
- 10.API接口限流实战
- 11.Spring Data Jpa实战
- 12.使用druid的monitor工具查看sql执行性能
- 13.使用springboot admin对springboot应用进行监控
- 14.mybatis-plus实战
- 15.使用shiro对web应用进行权限认证
- 16.security整合jwt实现对前后端分离的项目进行权限认证
- 17.使用swagger2生成RESTful风格的接口文档
- 18.使用Netty加websocket实现在线聊天功能
- 19.使用spring-session加redis来实现session共享
- 20.自定义@Configuration配置类启用开关
- 21.对springboot框架编译后的jar文件瘦身
- 22.集成RocketMQ实现消息发布和订阅
- 23.集成smart-doc插件零侵入自动生成RESTful格式API文档
- 24.集成FastDFS实现文件的分布式存储
- 25.集成Minio实现文件的私有化对象存储
- 26.集成spring-boot-starter-validation对接口参数校验
- 27.集成mail实现邮件推送带网页样式的消息
- 28.使用JdbcTemplate操作数据库
- 29.Jpa+vue实现单模型的低代码平台
- 30.使用sharding-jdbc实现读写分离和分库分表
- 31.基于分布式锁或xxx-job实现分布式任务调度
- 32.基于注解+redis实现表单防重复提交
- 33.优雅集成i18n实现国际化信息返回
- 34.使用Spring Retry完成任务的重试