目录
上一篇文章springboot整合shiro实现认证和授权已经介绍了怎么使用shiro来实现身份认证和资源授权,但是还存在一些问题。
数据库中添加对应的权限,并给角色绑定权限,然后给用户分配角色
/*
Navicat Premium Data Transfer
Source Server : MariaDB
Source Server Type : MariaDB
Source Server Version : 100605 (10.6.5-MariaDB)
Source Host : localhost:3306
Source Schema : shiro
Target Server Type : MariaDB
Target Server Version : 100605 (10.6.5-MariaDB)
File Encoding : 65001
Date: 03/07/2023 17:48:13
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称',
`type` tinyint(3) UNSIGNED NOT NULL COMMENT '权限类型(父权限/子权限)',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '接口路径',
`method` tinyint(3) UNSIGNED NOT NULL DEFAULT 0 COMMENT '请求方式(0-get;1-post)',
`service` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'mhxysy' COMMENT '服务名',
`parent_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '父级权限id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统权限表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES ('UserController', '用户管理', 0, '/user', 0, 'shiro', NULL);
INSERT INTO `permission` VALUES ('UserController_delete', '删除用户', 0, '/user/delete', 1, 'shiro', 'UserController');
INSERT INTO `permission` VALUES ('UserController_login', '用户登录', 1, '/user/login', 1, 'shiro', 'UserController');
INSERT INTO `permission` VALUES ('UserController_update', '修改用户', 1, '/user/update', 1, 'shiro', 'UserController');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '名称',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',
`sort` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '自定义排序序号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, '超级管理员', '最高权限,拥有系统所有权限', 0);
INSERT INTO `role` VALUES (2, '系统管理员', '拥有系统设置相关权限', 100);
-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`role_id` int(10) UNSIGNED NOT NULL COMMENT '角色id',
`permission_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色-权限关联表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES (1, 1, 'UserController_login');
INSERT INTO `role_permission` VALUES (2, 1, 'UserController_delete');
INSERT INTO `role_permission` VALUES (3, 1, 'UserController_update');
INSERT INTO `role_permission` VALUES (4, 2, 'UserController_login');
INSERT INTO `role_permission` VALUES (5, 2, 'UserController_delete');
INSERT INTO `role_permission` VALUES (6, 2, 'UserController_update');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '12345' COMMENT '密码',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`gender` tinyint(4) UNSIGNED NOT NULL COMMENT '性别,0-女;1-男',
`is_enable` tinyint(4) UNSIGNED NOT NULL COMMENT '是否启用(0-未启用;1-启用中)',
`last_login_time` datetime NULL DEFAULT NULL COMMENT '上一次登录时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('2023', '系统管理员', 'system', '123456', '18888888888', 2, 1, '2022-11-25 00:15:42');
INSERT INTO `user` VALUES ('mhxy1218', '沐雨橙风ιε', 'mumu', 'mhxy1218', '16666666666', 0, 1, '2023-07-03 15:00:32');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`role_id` int(10) UNSIGNED NOT NULL COMMENT '角色id,数据来源于role表的主键',
`user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id,数据来源于user表的主键',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户-角色关系表表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 'mhxy1218');
INSERT INTO `user_role` VALUES (2, 2, '2023');
SET FOREIGN_KEY_CHECKS = 1;
解决问题一
优化:在配置类里查询全部系统权限,把这些权限的URL交给我们自定义过滤器处理。
package com.example.shiro.config;
import com.example.shiro.filter.AuthorizationFilter;
import com.example.shiro.mapper.PermissionMapper;
import com.example.shiro.realm.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* shiro配置类
*/
@Configuration
public class ShiroConfig {
private final PermissionMapper permissionMapper;
@Autowired
public ShiroConfig(PermissionMapper mapper) {
this.permissionMapper = mapper;
}
/**
* 配置安全管理器
* @param userRealm UserRealm
* @return DefaultWebSecurityManager
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 配置Shiro过滤器工厂
* @param securityManager 安全管理器
* @return ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 注册安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 当用户访问认证资源的时候,如果用户没有登录,那么就会跳转到该属性指定的页面
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 添加自定义过滤器
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authorization", new AuthorizationFilter());
shiroFilterFactoryBean.setFilters(filters);
// 定义资源访问规则
Map<String, String> map = new LinkedHashMap<>();
map.put("/", "authc");
map.put("/html/*", "authc");
map.put("/index.html", "authc");
// 登录接口不需要鉴权
map.put("/user/login", "anon");
// 查询所有权限
List<String> permissions = permissionMapper.selectAll();
for (String permission : permissions) {
map.put(permission, "authorization");
}
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
这样的话,和接口无关的资源就不需要我们自定义过滤器来处理了,可以进一步改进过滤器的代码。简化之后的代码如下:
package com.example.shiro.filter;
import com.alibaba.fastjson.JSON;
import com.example.shiro.restful.JsonResult;
import com.example.shiro.restful.ResponseCode;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 鉴权过滤器
* @author heyunlin
* @version 1.0
*/
@WebFilter
public class AuthorizationFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
String requestURI = request.getRequestURI();
if ("/user/login".equals(requestURI)) {
chain.doFilter(req, resp);
return;
}
Subject subject = SecurityUtils.getSubject();
if (subject != null && !subject.isPermitted(requestURI)) {
HttpServletResponse response = (HttpServletResponse) resp;
response.setContentType("application/json;charset=utf-8");
// 构建返回对象
JsonResult<Void> jsonResult= JsonResult.error(ResponseCode.UNAUTHORIZED, "正在访问未授权的资源");
String data = JSON.toJSONString(jsonResult);
response.getWriter().write(data);
return;
}
chain.doFilter(req, resp);
}
}
解决问题二
PermissionMapper
package com.example.shiro.mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @author heyunlin
* @version 1.0
*/
@Repository
public interface PermissionMapper {
/**
* 查询所有权限
* @return List<String>
*/
List<String> selectAll();
/**
* 通过用户名查询用户权限
* @param username 用户名
* @return List<String> 用户权限的URL列表
*/
List<String> selectPermissionsByUsername(String username);
}
PermissionMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.shiro.mapper.PermissionMapper">
<select id="selectAll" resultType="java.lang.String">
select url from permission
</select>
<select id="selectPermissionsByUsername" resultType="java.lang.String">
select qx.url from permission qx
left join role_permission jsqx on qx.id = jsqx.permission_id
left join user_role yhjs on jsqx.role_id = yhjs.role_id
left join user yh on yhjs.user_id = yh.id
where yh.username= #{username}
</select>
</mapper>
UserRealm中的权限从数据库获取
package com.example.shiro.realm;
import com.example.shiro.entity.User;
import com.example.shiro.exception.GlobalException;
import com.example.shiro.mapper.PermissionMapper;
import com.example.shiro.mapper.UserMapper;
import com.example.shiro.restful.ResponseCode;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.List;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class UserRealm extends AuthorizingRealm {
private final UserMapper userMapper;
private final PermissionMapper permissionMapper;
@Autowired
public UserRealm(UserMapper userMapper, PermissionMapper permissionMapper) {
this.userMapper = userMapper;
this.permissionMapper = permissionMapper;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 得到用户名
String username = token.getUsername();
// 根据用户名查询用户信息
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "登录失败,用户不存在~");
}
if (user.getIsEnable()) {
String password = new String(token.getPassword());
if (user.getPassword().equals(password)) {
return new SimpleAuthenticationInfo(user, password, username);
} else {
throw new GlobalException(ResponseCode.BAD_REQUEST, "用户名或密码错误,登录失败!");
}
}
return null;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principals.getPrimaryPrincipal();
String username = user.getUsername();
// 通过用户名获取用户的权限
List<String> permissions = permissionMapper.selectPermissionsByUsername(username);
authorizationInfo.setStringPermissions(new HashSet<>(permissions));
return authorizationInfo;
}
}
新的问题
第一步:添加依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步:添加redis配置类
package com.example.shiro.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, ?> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json()); // 值(Value)使用JSON
return redisTemplate;
}
}
第三步:创建redis工具类
package com.example.shiro.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class RedisUtils {
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 保存数据到Redis
* @param key 键
* @param value 值
*/
public void save(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
redisTemplate.expire(key, 7, TimeUnit.DAYS);
}
/**
* 通过用户名获取用户的权限信息
* @param key 键
*/
public Object get(String key) {
log.debug("从redis中获取key{}...", key);
if (hasKey(key)) {
return redisTemplate.opsForValue().get(key);
} else {
return null;
}
}
/**
* 通过用户名删除用户的权限信息
* @param key 用户名
*/
public void remove(String key) {
if (hasKey(key)) {
redisTemplate.delete(key);
log.debug("从redis中删除key{}...", key);
}
}
/**
* 删除全部用户的权限信息
*/
public void removeAll() {
Set<String> keys = redisTemplate.keys("*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
log.debug("从redis中删除全部key...");
}
/**
* 判断key是否存在
* @param key 键
* @return Boolean
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
}
第四步:创建用于操作用户权限的redis仓库
package com.example.shiro.redis;
import com.alibaba.fastjson2.JSON;
import com.example.shiro.mapper.PermissionMapper;
import com.example.shiro.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class RedisRepository extends RedisUtils {
private static final String SUFFIX = "ROLE_PERMISSIONS:";
private final PermissionMapper permissionMapper;
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public RedisRepository(PermissionMapper permissionMapper, RedisTemplate<String, Object> redisTemplate) {
super(redisTemplate);
this.permissionMapper = permissionMapper;
this.redisTemplate = redisTemplate;
}
/**
* 查询用户的权限信息,并保存到redis
* @param username 用户名
*/
public void save(String username) {
String key = SUFFIX + username;
// 查询用户的权限
List<String> permissions = permissionMapper.selectPermissionsByUsername(username);
redisTemplate.opsForValue().set(key, JSON.toJSONString(permissions));
redisTemplate.expire(key, 7, TimeUnit.DAYS);
log.debug("用户{}的权限保存到了redis中", username);
}
/**
* 通过用户名获取用户的权限信息
* @param username 用户名
*/
@Override
public List<String> get(String username) {
String key = SUFFIX + username;
log.info("从redis中获取用户{}的权限", username);
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
String permissions = (String) redisTemplate.opsForValue().get(key);
if (StringUtils.isNotEmpty(permissions)) {
Object object = JSON.parse(permissions);
return object != null ? (List<String>) object : null;
}
return null;
} else {
save(username);
return get(username);
}
}
/**
* 通过用户名删除用户的权限信息
* @param username 用户名
*/
@Override
public void remove(String username) {
String key = SUFFIX + username;
super.remove(key);
log.debug("从redis中删除用户{}的权限...", username);
}
}
第五步:保存用户权限到redis
博主采用了折中的办法,用户登录的时候查询用户的权限并保存到redis中,用户退出的时候从redis删除用户的权限。这样做的好处是,用户没有登录的时候,是不需要用到该该用户的权限缓存的,可以节省内存,而且每次登录获取到的权限都是最新的。
接下来,在用户登录的方法中添加保存用户权限的代码
package com.example.shiro.service.impl;
import com.example.shiro.dto.UserLoginDTO;
import com.example.shiro.entity.User;
import com.example.shiro.exception.GlobalException;
import com.example.shiro.mapper.UserMapper;
import com.example.shiro.redis.RedisRepository;
import com.example.shiro.restful.ResponseCode;
import com.example.shiro.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* @author heyunlin
* @version 1.0
*/
@Service
public class UserServiceImpl implements UserService {
private final UserMapper mapper;
private final RedisRepository redisRepository;
@Autowired
public UserServiceImpl(UserMapper mapper, RedisRepository redisRepository) {
this.mapper = mapper;
this.redisRepository = redisRepository;
}
@Override
public void login(UserLoginDTO loginDTO) {
String username = loginDTO.getUsername();
// 查询用户信息
User user = mapper.selectByUsername(username);
if (user != null) {
// 如果用户被锁定,提前退出
if (user.getIsEnable()) {
// shiro登录认证
UsernamePasswordToken token = new UsernamePasswordToken(username, loginDTO.getPassword());
Subject subject = SecurityUtils.getSubject();
subject.login(token);
// 设置session失效时间:永不超时
subject.getSession().setTimeout(-1001);
// 修改管理员上一次登录时间
user.setLastLoginTime(LocalDateTime.now());
mapper.updateById(user);
redisRepository.remove(username);
redisRepository.save(username);
} else {
throw new GlobalException(ResponseCode.FORBIDDEN, "账号已被锁定,禁止登录!");
}
} else {
throw new GlobalException(ResponseCode.NOT_FOUND, "用户名不存在~");
}
}
}
最新代码访问地址为:shiro: springboot整合shiro实现认证和授权 - Gitee.com