Apache Shiro权限管理解析三Apache Shiro应用
Shiro 的优势与适用场景
优势
- 简单易用:API 设计直观,适合中小型项目快速实现权限管理。
- 灵活性高:支持多种数据源(数据库、LDAP 等),并允许开发者自定义 Realm。
- 跨平台支持:不仅限于 Web 应用,还适用于桌面应用和分布式系统。
- 强大的会话管理:提供独立于 Servlet 的会话机制,支持集群环境。
适用场景
- 需要快速实现身份认证和授权的小型项目。
- 需要灵活定制权限管理规则的中型项目。
- 非 Web 应用场景(如桌面应用或微服务架构)。
开发流程
下面开始使用Apache Shiro 进行项目开发。步骤如下
1、导入依赖
2、设计数据库表
3、定义LoginRealm类,继承AuthorizingRealm
4、定义ShiroConfig 配置类
5、开发LoginController
6、配置application.yml
7、模拟登陆
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- mybatis-plus 多数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclusion>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
2、设计数据库表
user表
CREATE TABLE `user` (
`id` bigint(19) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_name` varchar(200) DEFAULT NULL,
`password` varchar(200) DEFAULT NULL,
`role` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
role 表
CREATE TABLE `role` (
`id` bigint(19) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`role` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3、定义LoginRealm类
AuthorizingRealm是Apache Shiro框架中的一个核心组件,主要用于实现用户的认证和授权功能。
功能和用途
AuthorizingRealm继承自AuthenticatingRealm,提供了认证和授权的功能。它通过重写doGetAuthenticationInfo和doGetAuthorizationInfo方法来实现具体的认证和授权逻辑。其中:
- doGetAuthenticationInfo方法用于获取用户的认证信息,通常需要从数据库中查询用户的用户名和密码等信息。
- doGetAuthorizationInfo方法用于获取用户的授权信息,通常涉及到角色的权限管理。
使用场景
在实际应用中,AuthorizingRealm常用于需要用户认证和授权的场景,如登录验证、权限控制等。通过继承AuthorizingRealm并重写相关方法,可以方便地实现自定义的认证和授权逻辑。
package org.example.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
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.example.mapper.UserMapper;
import org.example.model.User;
import org.example.utils.ApplicationContextProvider;
import java.util.HashSet;
import java.util.Set;
/**
* 认证和授权
*/
@Slf4j
public class LoginRealm extends AuthorizingRealm {
/**
* 用户表UserMapper
*/
private UserMapper getUserMapper() {
return ApplicationContextProvider.getApplicationContext().getBean(UserMapper.class);
}
/**
* 获取身份验证信息
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("————身份认证方法————");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 从数据库获取对应用户名密码的用户
User user = getUserMapper().getByName(token.getUsername());
if (null == user) {
throw new AccountException("用户名不正确");
} else if (!user.getPassword().equals(new String((char[]) token.getCredentials()))) {
throw new AccountException("密码不正确");
}
return new SimpleAuthenticationInfo(token.getPrincipal(), user.getPassword(), getName());
}
/**
* 获取授权信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("————权限认证————");
String username = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获得该用户角色
String role = getUserMapper().getRole(username);
Set<String> set = new HashSet<String>();
//需要将 role 封装到 Set 作为 info.setRoles() 的参数
set.add(role);
//设置该用户拥有的角色
info.setRoles(set);
return info;
}
}
4、定义ShiroConfig 配置类
@Configuration
public class ShiroConfig {
@Value("${shiro.redis.host}")
protected String redisHost;
@Value("${shiro.redis.port}")
protected String redisPort;
@Value("${shiro.redis.username}")
protected String redisUserName;
@Value("${shiro.redis.password}")
protected String redisPassword;
/**
* 登录过期时间(天)
*/
private static final Integer LOGIN_EXPIRE_DAYS = 3;
@Bean
public Realm realm() {
return new LoginRealm();
}
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/notLogin");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
// 设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//游客,开发权限
filterChainDefinitionMap.put("/guest/**", "anon");
//用户,需要角色权限 “user”
filterChainDefinitionMap.put("/user/**", "roles[user]");
//管理员,需要角色权限 “admin”
filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//开放登陆接口
filterChainDefinitionMap.put("/login", "anon");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
System.out.println("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(realm());
securityManager.setSessionManager(sessionManager());
ThreadContext.bind(securityManager);
return securityManager;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*/
@Bean(name = "redisCacheManager")
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(this.redisHost);
redisManager.setPort(Integer.parseInt(this.redisPort));
redisManager.setExpire(60 * 60 * 24 * LOGIN_EXPIRE_DAYS);// 配置缓存过期时间(秒)
redisManager.setTimeout(0);
redisManager.setPassword(this.redisUserName + ":" + this.redisPassword);
return redisManager;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
//登录超时时长(毫秒)
sessionManager.setGlobalSessionTimeout(1000L * 60 * 60 * 24 * LOGIN_EXPIRE_DAYS);
return sessionManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
5、开发LoginController
package org.example.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.example.mapper.UserMapper;
import org.example.model.ApiResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class LoginController {
@Resource
private UserMapper userMapper;
@RequestMapping(value = "/notLogin", method = RequestMethod.GET)
public ApiResult notLogin() {
return ApiResult.success("您尚未登陆");
}
@RequestMapping(value = "/notRole", method = RequestMethod.GET)
public ApiResult notRole() {
return ApiResult.success("您没有权限!");
}
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public ApiResult logout() {
Subject subject = SecurityUtils.getSubject();
//注销
subject.logout();
return ApiResult.success("成功注销!");
}
/**
* 登陆
*
* @param username 用户名
* @param password 密码
*/
@RequestMapping(value = "/login")
public ApiResult login(String username, String password) {
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行认证登陆
subject.login(token);
//根据权限,指定返回数据
String role = userMapper.getRole(username);
if ("user".equals(role)) {
return ApiResult.success("欢迎登陆");
}
if ("admin".equals(role)) {
return ApiResult.success("欢迎来到管理员页面");
}
return ApiResult.fail(-1, "权限错误!");
}
}
6、配置application.yml
server:
port: 8081
spring:
datasource:
# druid-spring-boot-starter 依赖自动生效 druid,可以不配置 type 属性,但建议配置
type: com.alibaba.druid.pool.DruidDataSource
dynamic:
primary: user
strict: false
datasource:
user:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/user?characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
#数据源其他配置
druid:
# 配置初始化大小、最小、最大线程数
initialSize: 5
minIdle: 5
# CPU核数+1,也可以大些但不要超过20,数据库加锁时连接过多性能下降
maxActive: 20
# 最大等待时间,内网:800,外网:1200(三次握手1s)
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最大空间时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1
testWhileIdle: true
# 设置从连接池获取连接时是否检查连接有效性,true检查,false不检查
testOnBorrow: true
# 设置从连接池归还连接时是否检查连接有效性,true检查,false不检查
testOnReturn: true
# 可以支持PSCache(提升写入、查询效率)
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
# 保持长连接
keepAlive: true
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
7、模拟登陆
在浏览器输入地址:
http://127.0.0.1:8081/login?username=yyp&password=yyp