有源码的教程,不会的同学下载源码,根据教程学一下哈~
一. 说明作者:Sans_
Shiro 是一个安全框架, 项目中主要用它做认证, 授权, 加密, 以及用户的会话管理, 虽然 Shiro 没有 SpringSecurity 功能更丰富, 但是它轻量, 简单, 在项目中通常业务需求 Shiro 也都能胜任.
二. 项目环境-
MyBatis-Plus 版本: 3.1.0
-
SpringBoot 版本: 2.1.5
-
JDK 版本: 1.8
-
Shiro 版本: 1.4
-
Shiro-redis 插件版本: 3.1.0
数据表 (SQL 文件在项目中): 数据库中测试号的密码进行了加密, 密码皆为 123456
Maven 依赖如下:
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-aop
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-data-redis-reactive
com.baomidou
mybatis-plus-boot-starter
3.1.0
com.alibaba
druid
1.1.6
org.apache.shiro
shiro-spring
1.4.0
org.crazycake
shiro-redis
3.1.0
org.apache.commons
commons-lang3
3.5
配置如下:
# 配置端口
server:
port: 8764
spring:
# 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
# Redis数据源
redis:
host: localhost
port: 6379
timeout: 6000
password: 123456
jedis:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5# 连接池中的最小空闲连接
# mybatis-plus相关配置
mybatis-plus:
# xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
mapper-locations: classpath:mapper
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static T getBean(Class beanClass) {
return context.getBean(beanClass);
}
}
创建 Shiro 工具
public class ShiroUtils {
private ShiroUtils(){}
private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);
public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}
public static void logout() {
SecurityUtils.getSubject().logout();
}
public static SysUserEntity getUserInfo() {
return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
}
public static void deleteCache(String username, boolean isRemoveSession){
//从缓存中获取Session
Session session = null;
Collection sessions = redisSessionDAO.getActiveSessions();
SysUserEntity sysUserEntity;
Object attribute = null;
for(Session sessionInfo : sessions){
//遍历Session,找到该用户名称对应的Session
attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
sysUserEntity = (SysUserEntity) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (sysUserEntity == null) {
continue;
}
if (Objects.equals(sysUserEntity.getUsername(), username)) {
session=sessionInfo;
}
}
if (session == null||attribute == null) {
return;
}
//删除session
if (isRemoveSession) {
redisSessionDAO.delete(session);
}
//删除Cache,在访问受限接口时会重新授权
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
Authenticator authc = securityManager.getAuthenticator();
((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
}
}
创建 Shiro 的 SessionId 生成器
三. 编写 Shiro 核心类创建 Realm 用于授权和认证
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal();
//获取用户ID
Long userId =sysUserEntity.getUserId();
//这里可以进行授权和处理
Set rolesSet = new HashSet<>();
Set permsSet = new HashSet<>();
//查询角色和权限(这里根据业务自行查询)
List sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId);
for (SysRoleEntity sysRoleEntity:sysRoleEntityList) {
rolesSet.add(sysRoleEntity.getRoleName());
List sysMenuEntityList = sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());
for (SysMenuEntity sysMenuEntity :sysMenuEntityList) {
permsSet.add(sysMenuEntity.getPerms());
}
}
//将查到的权限和角色分别传入authorizationInfo中
authorizationInfo.setStringPermissions(permsSet);
authorizationInfo.setRoles(rolesSet);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户的输入的账号.
String username = (String) authenticationToken.getPrincipal();
//通过username从数据库中查找 User对象,如果找到进行验证
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUserEntity user = sysUserService.selectUserByName(username);
//判断账号是否存在
if (user == null) {
throw new AuthenticationException();
}
//判断账号是否被冻结
if (user.getState()==null||user.getState().equals("PROHIBIT")){
throw new LockedAccountException();
}
//进行验证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户名
user.getPassword(), //密码
ByteSource.Util.bytes(user.getSalt()), //设置盐值
getName()
);
//验证成功开始踢人(清除缓存和Session)
ShiroUtils.deleteCache(username,true);
return authenticationInfo;
}
}
创建 SessionManager 类
创建 ShiroConfig 配置类
@Configuration
public class ShiroConfig {
private final String CACHE_KEY = "shiro:cache:";
private final String SESSION_KEY = "shiro:session:";
private final int EXPIRE = 1800;
//Redis配置
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.password}")
private String password;
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filterChainDefinitionMap = new linkedHashMap<>();
// 注意过滤器配置顺序不能颠倒
// 配置过滤:不会被拦截的链接
filterChainDefinitionMap.put("/static
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义Ssession管理
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现
securityManager.setCacheManager(cacheManager());
// 自定义Realm验证
securityManager.setRealm(shiroRealm());
return securityManager;
}
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用SHA256算法;
shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
// 散列的次数,比如散列两次,相当于 md5(md5(""));
shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
return shaCredentialsMatcher;
}
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix(CACHE_KEY);
// 配置缓存的话要求放在session里面的实体类必须有个id标识
redisCacheManager.setPrincipalIdFieldName("userId");
return redisCacheManager;
}
@Bean
public ShiroSessionIdGenerator sessionIdGenerator(){
return new ShiroSessionIdGenerator();
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setKeyPrefix(SESSION_KEY);
redisSessionDAO.setExpire(expire);
return redisSessionDAO;
}
@Bean
public SessionManager sessionManager() {
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
shiroSessionManager.setSessionDAO(redisSessionDAO());
return shiroSessionManager;
}
}
四. 实现权限控制
Shiro 可以用代码或者注解来控制权限, 通常我们使用注解控制, 不仅简单方便, 而且更加灵活. Shiro 注解一共有五个:
一般情况下我们在项目中做权限控制, 使用最多的是 RequiresPermissions 和 RequiresRoles, 允许存在多个角色和权限, 默认逻辑是 AND, 也就是同时拥有这些才可以访问方法, 可以在注解中以参数的形式设置成 OR
示例
使用顺序: Shiro 注解是存在顺序的, 当多个注解在一个方法上的时候, 会逐个检查, 知道全部通过为止, 默认拦截顺序是: RequiresRoles->RequiresPermissions->RequiresAuthentication->
RequiresUser->RequiresGuest
示例
创建 UserRoleController 角色拦截测试类
@RestController
@RequestMapping("/role")
public class UserRoleController {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
@Autowired
private SysRoleMenuService sysRoleMenuService;
@RequestMapping("/getAdminInfo")
@RequiresRoles("ADMIN")
public Map getAdminInfo(){
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只有管理员角色能访问的接口");
return map;
}
@RequestMapping("/getUserInfo")
@RequiresRoles("USER")
public Map getUserInfo(){
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只有用户角色能访问的接口");
return map;
}
@RequestMapping("/getRoleInfo")
@RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
@RequiresUser
public Map getRoleInfo(){
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只要有ADMIN或者USER角色能访问的接口");
return map;
}
@RequestMapping("/getLogout")
@RequiresUser
public Map getLogout(){
ShiroUtils.logout();
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","登出");
return map;
}
}
创建 UserMenuController 权限拦截测试类
@RestController
@RequestMapping("/menu")
public class UserMenuController {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
@Autowired
private SysRoleMenuService sysRoleMenuService;
@RequestMapping("/getUserInfoList")
@RequiresPermissions("sys:user:info")
public Map getUserInfoList(){
Map map = new HashMap<>();
List sysUserEntityList = sysUserService.list();
map.put("sysUserEntityList",sysUserEntityList);
return map;
}
@RequestMapping("/getRoleInfoList")
@RequiresPermissions("sys:role:info")
public Map getRoleInfoList(){
Map map = new HashMap<>();
List sysRoleEntityList = sysRoleService.list();
map.put("sysRoleEntityList",sysRoleEntityList);
return map;
}
@RequestMapping("/getMenuInfoList")
@RequiresPermissions("sys:menu:info")
public Map getMenuInfoList(){
Map map = new HashMap<>();
List sysMenuEntityList = sysMenuService.list();
map.put("sysMenuEntityList",sysMenuEntityList);
return map;
}
@RequestMapping("/getInfoAll")
@RequiresPermissions("sys:info:all")
public Map getInfoAll(){
Map map = new HashMap<>();
List sysUserEntityList = sysUserService.list();
map.put("sysUserEntityList",sysUserEntityList);
List sysRoleEntityList = sysRoleService.list();
map.put("sysRoleEntityList",sysRoleEntityList);
List sysMenuEntityList = sysMenuService.list();
map.put("sysMenuEntityList",sysMenuEntityList);
return map;
}
@RequestMapping("/addMenu")
public Map addMenu(){
//添加管理员角色权限
SysRoleMenuEntity sysRoleMenuEntity = new SysRoleMenuEntity();
sysRoleMenuEntity.setMenuId(4L);
sysRoleMenuEntity.setRoleId(1L);
sysRoleMenuService.save(sysRoleMenuEntity);
//清除缓存
String username = "admin";
ShiroUtils.deleteCache(username,false);
Map map = new HashMap<>();
map.put("code",200);
map.put("msg","权限添加成功");
return map;
}
}
创建 UserLoginController 登录类
@RestController
@RequestMapping("/userLogin")
public class UserLoginController {
@Autowired
private SysUserService sysUserService;
@RequestMapping("/login")
public Map login(@RequestBody SysUserEntity sysUserEntity){
Map map = new HashMap<>();
//进行身份验证
try{
//验证身份和登陆
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
//验证成功进行登录操作
subject.login(token);
}catch (IncorrectCredentialsException e) {
map.put("code",500);
map.put("msg","用户不存在或者密码错误");
return map;
} catch (LockedAccountException e) {
map.put("code",500);
map.put("msg","登录失败,该用户已被冻结");
return map;
} catch (AuthenticationException e) {
map.put("code",500);
map.put("msg","该用户不存在");
return map;
} catch (Exception e) {
map.put("code",500);
map.put("msg","未知异常");
return map;
}
map.put("code",0);
map.put("msg","登录成功");
map.put("token",ShiroUtils.getSession().getId().toString());
return map;
}
@RequestMapping("/unauth")
public Map unauth(){
Map map = new HashMap<>();
map.put("code",500);
map.put("msg","未登录");
return map;
}
}
五. POSTMAN 测试
登录成功后会返回 TOKEN, 因为是单点登录, 再次登陆的话会返回新的 TOKEN, 之前 Redis 的 TOKEN 就会失效了
当第一次访问接口后我们可以看到缓存中已经有权限数据了, 在次访问接口的时候, Shiro 会直接去缓存中拿取权限, 注意访问接口时候要设置请求头.
ADMIN 这个号现在没有 sys:info:all 这个权限的, 所以无法访问 getInfoAll 接口, 我们要动态分配权限后, 要清掉缓存, 在访问接口时候, Shiro 会去重新执行授权方法, 之后再次把权限和角色数据放入缓存中
访问添加权限测试接口, 因为是测试, 我把增加权限的用户 ADMIN 写死在里面了, 权限添加后, 调用工具类清掉缓存, 我们可以发现, Redis 中已经没有缓存了
再次访问 getInfoAll 接口, 因为缓存中没有数据, Shiro 会重新授权查询权限, 拦截通过
六. 项目源码gitee.com/liselotte/spring-boot-shiro-demo
github.com/xuyulong2017/my-java-demo
(完)



