最近也是一直在保持学习课外拓展技术,所以想自己做一个简单小项目,于是就有了这个快速上手 Shiro 和 Redis 的小项目,说白了就是拿来练手调调 API,然后做完后拿来总结的小项目,完整的源代码已经上传到 CodeChina平台上,文末有仓库链接
有 1 和 2 用户,用户名和密码也分别为 1 和 2 ,1 用户有增加和删除的权限,2用户有更新的权限,登录的时候需要验证码并且需要缓存用户的角色和权限,用户登出的时候需要将缓存的认证和授权信息删除。
其实就是传统的 RBAC 模型,不加外键的原因是因为增加外键会造成数据库压力。
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80021
Source Host : localhost:3306
Source Schema : spring-security
Target Server Type : MySQL
Target Server Version : 80021
File Encoding : 65001
Date: 23/04/2021 18:18:01
*/
create database if not exists `spring-security` charset=utf8mb4;
use `spring-security`;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`permissionid` int NOT NULL AUTO_INCREMENT,
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`perm` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`permissionid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES (1, '/toUserAdd', 'user:add');
INSERT INTO `permission` VALUES (2, '/toUserUpdate', 'user:update');
INSERT INTO `permission` VALUES (3, '/toUserDelete', 'user:delete');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`roleid` int NOT NULL AUTO_INCREMENT,
`role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`roleid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'student');
INSERT INTO `role` VALUES (2, 'parent');
INSERT INTO `role` VALUES (3, 'teacher');
-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`permissionid` int NOT NULL,
`roleid` int NOT NULL,
PRIMARY KEY (`permissionid`, `roleid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES (1, 1);
INSERT INTO `role_permission` VALUES (2, 2);
INSERT INTO `role_permission` VALUES (3, 3);
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`userid` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` int NULL DEFAULT 1,
PRIMARY KEY (`userid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '1', 'a0c68c64557599483e99630ce4d2f30e', 'ainjee', 1);
INSERT INTO `user` VALUES (2, '2', '78fc06a914bcf261ed749952b0c9f67b', 'eeiain', 1);
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`userid` int NOT NULL,
`roleid` int NOT NULL,
PRIMARY KEY (`userid`, `roleid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1);
INSERT INTO `user_role` VALUES (1, 3);
INSERT INTO `user_role` VALUES (2, 2);
SET FOREIGN_KEY_CHECKS = 1;
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.jmugroupId>
<artifactId>shiro_demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>shiro_demoname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASEspring-boot.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>2.3.7.RELEASEversion>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>org.apache.velocitygroupId>
<artifactId>velocity-engine-coreartifactId>
<version>2.3version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.7.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.0.0version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-ehcacheartifactId>
<version>1.7.1version>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.3.7.RELEASEversion>
<configuration>
<mainClass>com.jmu.shiro_demo.ShiroDemoApplicationmainClass>
configuration>
<executions>
<execution>
<id>repackageid>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
<resources>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.xmlinclude>
includes>
<filtering>truefiltering>
resource>
resources>
build>
project>
package com.jmu.shiro_demo.shiro;
import com.jmu.shiro_demo.entity.Permission;
import com.jmu.shiro_demo.entity.Role;
import com.jmu.shiro_demo.entity.User;
import com.jmu.shiro_demo.service.UserService;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import java.util.List;
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String username = (String) SecurityUtils.getSubject().getPrincipal();
User user = userService.getUserByUserName(username);
//1.动态分配角色
List<Role> roles = userService.getUserRoleByUserId(user.getUserid());
roles.stream().forEach(role -> {
authorizationInfo.addRole(role.getRole());});
//2.动态授权
List<Permission> perms = userService.getUserPermissionsByUserId(user.getUserid());
perms.stream().forEach(permission -> {
authorizationInfo.addStringPermission(permission.getPerm());});
return authorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
User user = userService.getUserByUserName(username);
if(ObjectUtils.isEmpty(user)) {
return null;
}
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), new MyByteSource(user.getSalt()),this.getName());
}
}
编写 ShiroConfig 配置
核心是配置 ShiroFilterFactoryBean,DefaultSecurityManager,UserRealm(这里的Realm是自定义的),ShiroDialect 是整合 shiro与thymleaf在前端使用 shiro 的标签的拓展包,来源于 github 的开源项目。
依次关系为
ShiroFilterFactoryBean 中 set DefaultSecurityManager,
DefaultSecurityManager 中 set UserRealm,
UserRealm 中 set CacheManager 和 加密的算法
CacheManager 可以为 EhCacheManager 也可以为 RedisCacheManager,此项目整合 redis 的 缓存管理器
package com.jmu.shiro_demo.shiro;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.jmu.shiro_demo.entity.Permission;
import com.jmu.shiro_demo.service.PermissionService;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.DefaultSecurityManager;
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 java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Autowired
private PermissionService permissionService;
//1.配置ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
/** 设置Shiro 内置过滤器
* anon: 无需认证(登陆)可以访问
* authc: 必须认证才可以访问
* user: 如果使用 rememberMe 的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
Map<String ,String> filterMap = new LinkedHashMap<String ,String>();
//配置页面请求拦截
filterMap.put("/index","anon");
filterMap.put("/login","anon");
filterMap.put("/getAuthCode","anon");
//配置动态授权
List<Permission> perms = permissionService.list();
for (Permission permission : perms) {
filterMap.put(permission.getUrl(),"perms["+permission.getPerm()+"]");
}
filterMap.put("/*","authc");
shiroFilterFactoryBean.setLoginUrl("/toLogin");
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
//2.配置DefaultSecurityManager
@Bean(name = "securityManager")
public DefaultSecurityManager defaultSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(userRealm);
return defaultSecurityManager;
}
//3.配置Realm
@Bean(name = "userRealm")
public UserRealm getRealm() {
//1. 创建自定义的 userRealm 对象
UserRealm userRealm = new UserRealm();
//2. 设置 userRealm 的 CredentialsMatcher密码校验器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//2.1 设置加密算法
matcher.setHashAlgorithmName("MD5");
//2.2 设置散列次数
matcher.setHashIterations(6);
userRealm.setCredentialsMatcher(matcher);
userRealm.setCacheManager(new RedisCacheManager());
userRealm.setAuthenticationCachingEnabled(true); //认证
userRealm.setAuthenticationCacheName("authenticationCache");
userRealm.setAuthorizationCachingEnabled(true); //授权
userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
//4.配置Thymleaf的Shiro扩展标签
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
}
package com.jmu.shiro_demo.shiro;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;
import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;
/**
* 解决:
* shiro 使用缓存时出现:java.io.NotSerializableException:
* org.apache.shiro.util.SimpleByteSource
* 序列化后,无法反序列化的问题
*/
public class MyByteSource implements ByteSource, Serializable {
private static final long serialVersionUID = 1L;
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public MyByteSource(){
}
public MyByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MyByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MyByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MyByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MyByteSource(File file) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
}
public MyByteSource(InputStream stream) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
@Override
public byte[] getBytes() {
return this.bytes;
}
@Override
public String toHex() {
if(this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
@Override
public String toBase64() {
if(this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
@Override
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toString() {
return this.toBase64();
}
@Override
public int hashCode() {
return this.bytes != null && this.bytes.length != 0? Arrays.hashCode(this.bytes):0;
}
@Override
public boolean equals(Object o) {
if(o == this) {
return true;
} else if(o instanceof ByteSource) {
ByteSource bs = (ByteSource)o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
package com.jmu.shiro_demo.shiro;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
public class RedisCacheManager implements CacheManager {
//参数1 :认证或者是授权缓存的统一名称
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println(cacheName);
return new RedisCache<K,V>(cacheName);
}
}
package com.jmu.shiro_demo.shiro;
import com.jmu.shiro_demo.utils.ApplicationContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Collection;
import java.util.Set;
@Slf4j
public class RedisCache<k,v> implements Cache<k,v> {
private String cacheName;
public RedisCache() {
}
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
@Override
public v get(k k) throws CacheException {
System.out.println("get key:" + k);
return (v) getRedisTemplate().opsForHash().get(this.cacheName,k.toString());
}
@Override
public v put(k k, v v) throws CacheException {
System.out.println("put key: " + k);
System.out.println("put value: " + v);
getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
return v;
}
@Override
public v remove(k k) throws CacheException {
log.info("remove k:" + k.toString());
return (v) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
}
@Override
public void clear() throws CacheException {
log.info("clear");
getRedisTemplate().delete(this.cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<k> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}
@Override
public Collection<v> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}
public RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBeanByBeanName("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
<mapper namespace="com.jmu.shiro_demo.mapper.UserMapper">
<select id="getUserPermissionsByUserId" parameterType="int" resultType="permission">
select url,perm
from user,user_role,role,role_permission,permission
where user.userid = user_role.userid
and user_role.roleid = role.roleid
and role.roleid = role_permission.roleid
and role_permission.permissionid = permission.permissionid
and user.userid=#{userid};
select>
<select id="getUserRoleByUserId" parameterType="int" resultType="role">
select role.roleid,role
from user,user_role,role
where user.userid = user_role.userid
and user_role.roleid = role.roleid
and user.userid = #{userId};
select>
mapper>
<?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.jmu.shiro_demo.mapper.RoleMapper">
<!--查出一个角色所拥有的全部权限-->
<select id="getRolePermissionsByRoleId" parameterType="int" resultType="permission">
select url,perm
from role,role_permission,permission
where role.roleid = role_permission.roleid
and role_permission.permissionid = permission.permissionid
and role.roleid = #{roleId};
</select>
</mapper>
shiro_demo
Redis 反序列化的时候报错 no valid constructor;
解决:MyByteSource 加盐类实现的时候需要实现ByteSource接口,然后提供无参构造方法
用户退出的时候Redis中认证信息的缓存没有删除干净
解决:UserRealm 的认证方法返回的第一个参数不要用 User实体对象,而是用 User 的 getUsername() 返回唯一标识用户的用户名,其他有用到 Principal 的时候获得到的都是 这个 username。
跟着视频学习的时候如果不熟悉可以先跟着敲代码,但是最好看完后,自己想个小练手项目,自己完整的实现一遍,这样子记忆会更加深刻,学习的也更加扎实。希望学到的童鞋可以给博主点个赞!谢谢!
坚持分享,坚持原创,喜欢博主的靓仔靓女们可以看看博主的首页博客!
您的点赞与收藏是我分享博客的最大赞赏!
博主博客地址: https://blog.csdn.net/weixin_43967679