本文讲解如何使用SpringBoot
整合Shiro
框架来实现认证及权限校验,但如今的互联网已经成为前后端分离的时代,所以本文在使用SpringBoot
整合Shiro
框架的时候会联合JWT
一起搭配使用。
Shiro
是apache
旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份
认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
Shiro架构图
用户、角色、权限之间的关系
1、UsernamePasswordToken
,Shiro
用来封装用户登录信息,使用用户的登录信息来创建令牌 Token
。
2、SecurityManager
,Shiro
的核心部分,负责安全认证和授权。
3、Suject
,Shiro
的一个抽象概念,包含了用户信息。
4、Realm
,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
5、AuthenticationInfo
,用户的角色信息集合,认证时使用。
6、AuthorzationInfo
,角色的权限信息集合,授权时使用。
7、DefaultWebSecurityManager
,安全管理器,开发者自定义的Realm
需要注入到 DefaultWebSecurityManager
进行管理才能生效。
8、ShiroFilterFactoryBean
,过滤器工厂,Shiro
的基本运行机制是开发者定制规则,Shiro
去执行,具体的执行操作就是由ShiroFilterFactoryBean
创建的一个个 Filter 对象来完成。
JWT(JSON WEB TOKEN)
:JSON
网络令牌,JWT
是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON
格式)。它是在Web
环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。
JWT的构成JWT
由三部分构成:Header
(头部)、Payload
(载荷)和Signature
(签名)。
1.Header(头) 作用:记录令牌类型、签名算法等 例如:{“alg":"HS256","type","JWT}
2.Payload(有效载荷)作用:携带一些用户信息 例如{"userId":"1","username":"mayikt"}
3.Signature(签名)作用:防止Token被篡改、确保安全性 例如 计算出来的签名,一个字符串
pom依赖
<dependencies>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.1</version>
</dependency>
<!-- shiro-chcache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.1</version>
</dependency>
<!-- jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<!-- fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.15</version>
</dependency>
</dependencies>
public class JWTUtils {
/**
* 过期时间
*/
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;
/**
* 校验
* @param token
* @param username
* @param password
* @return
*/
public static boolean verify(String token, String username, String password) {
try {
Algorithm algorithm = Algorithm.HMAC256(password);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 颁发令牌
* @param username
* @param password
* @return
*/
public static String sign(String username, String password) {
try {
//设置过期时间:获取当前时间+过期时间(毫秒)
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//设置签名的加密算法:HMAC256
Algorithm algorithm = Algorithm.HMAC256(password);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* 获取用户名
* @param token
* @return
*/
public static String getUsername(String token) {
if (token == null || "".equals(token)) {
return null;
}
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
JWTToken
是定义的一个Token
类,继承了AuthenticationToken
类,实现getPrincipal
和getCredentials
方法,(这两个方法本来是用于获取token中的信息,和识别token的,但JWTUtils
已经为我们提供了这样的方法,所以这两个方法对于JWTToken
没有意义)。用于将客户端传来的Token
进行封装,便于Realm
识别Token
类型,进行认证和授权。
public class JWTToken implements AuthenticationToken {
/**
* 密钥
*/
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
因为 JWT
的整合,我们需要⾃定义⾃⼰的过滤器 JWTFilter
,JWTFilter
继承了 BasicHttpAuthenticationFilter
,并部分原⽅法进⾏了重写。
public class JWTFilter extends BasicHttpAuthenticationFilter {
/**
* Header中的Token标志
*/
private static String LOGIN_SIGN = "Authorization";
/**
* 是否允许访问
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
if (e instanceof AuthorizationException) {
throw new AuthorizationException("访问资源权限不足!");
} else {
//token 异常 认证失败
throw new AuthenticationException("token 异常 认证失败");
}
}
}
return true;
}
/**
* 是登录尝试
* @param request
* @param response
* @return
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
//判断是否是登录请求
String authorization = req.getHeader(LOGIN_SIGN);
return authorization != null;
}
/**
* 执行登录
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
String header = req.getHeader(LOGIN_SIGN);
JWTToken token = new JWTToken(header);
//提交给realm进⾏登⼊,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
return true;
}
}
自定义的Realm
对象,该对象继承于AuthorizingRealm
,实现了Shiro具体认证和授权的方法。
doGetAuthenticationInfo
方法用于->认证:校验帐号和密码
doGetAuthorizationInfo
方法用于->授权:授予角色和权限
另外需要注意:
必须要重写supports
方法,因为是自己定义的Token
,shiro
无法识别,需要修改Realm
中的supports
方法,使 shiro
支持自定义Token
。
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
@Autowired
private UserService userService;
/**
* 因为是自己定义的Token,shiro无法识别,需要修改Realm中的supports方法,使 shiro 支持自定义token。
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 认证:校验帐号和密码
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
//从token中获取用户名
String username = JWTUtils.getUsername(token);
//获取数据库中存取的用户,密码是加密后的
User user = userService.selectByUserName(username);
if (user != null) {
// 密码验证
if (!JWTUtils.verify(token, username, user.getPassword())) {
// 密码不正确
throw new IncorrectCredentialsException();
}
return new SimpleAuthenticationInfo(token, token, getName());
} else {
throw new UnknownAccountException();
}
}
/**
* 授权:授予角色和权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户名
String userName = JWTUtils.getUsername(principals.toString());
//根据用户名查询用户
User user = userService.selectByUserName(userName);
//实例化一个授权信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (user != null) {
//赋予角色
List<Role> roles = roleService.selectRoleByUserId(user.getId());
for (Role role : roles) {
//将角色添加到授权信息中
info.addRole(role.getRoleKey());
}
//赋予资源
List<Menu> permissions = menuService.selectPermsByUserId(user.getId());
for (Menu permission : permissions) {
//将权限添加授权信息中
info.addStringPermission(permission.getPerms());
}
}
return info;
}
}
ShiroConfig
用于进行Shiro
的相关配置,主要包括ShiroFilterFactoryBean
、DefaultWebSecurityManager
和Realm
的配置。
@Configuration
public class ShiroConfig {
/**
* 生命周期处理器
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 加密方式
* @return
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
// 散列凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置哈希算法名称,这里使用MD5算法
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置哈希迭代,这里迭代2次,相当于 md5(md5(""))
credentialsMatcher.setHashIterations(2);
// 设置存储的凭据16进制编码,需要和生成密码时的一样,默认是 Base64
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
/**
* 自定义Realm
* @param cacheManager
* @return
*/
@Bean(name = "shiroRealm")
@DependsOn("lifecycleBeanPostProcessor")
public ShiroRealm shiroRealm(EhCacheManager cacheManager) {
ShiroRealm realm = new ShiroRealm();
realm.setCacheManager(cacheManager);
return realm;
}
/**
* 缓存管理器
* @return
*/
@Bean(name = "ehCacheManager")
@DependsOn("lifecycleBeanPostProcessor")
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return ehCacheManager;
}
/**
* 安全管理器
* @param shiroRealm
* @return
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) {
// 实例化会话管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置缓存管理器
securityManager.setCacheManager(ehCacheManager());
/**
* 关闭shiro自带的session
* 详情见文档: http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
// 设置自定义Realm
securityManager.setRealm(shiroRealm);
return securityManager;
}
/**
* 过滤工厂
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
factoryBean.setFilters(filters);
Map<String, String> filterChainDefinitionManager = new LinkedHashMap<>();
// 所有请求通过我们自己的JWT Filter
filterChainDefinitionManager.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
return factoryBean;
}
/**
* 自动代理配置
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
/**
* 开启注解支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
aASA.setSecurityManager(securityManager);
return aASA;
}
}
这里开启注解支持需要添加DefaultAdvisorAutoProxyCreator(可选)
和AuthorizationAttributeSourceAdvisor
,DefaultAdvisorAutoProxyCreator
也可以选择不加,这里加是因为防止重复代理和可能引起代理出错的问题
认证过滤器
anon
:无需认证。authc
:必须认证。authcBasic
:需要通过 HTTPBasic
认证。perms
:必须拥有某个权限才能访问。role
:必须拥有某个角色才能访问。port
:请求的端口必须是指定值才可以。rest
:请求必须基于 RESTful
,POST
、PUT
、GET
、DELETE
。ssl
:必须是安全的 URL
请求,协议 HTTPS
。使用@RestControllerAdvice
捕获Controller
层抛出的异常。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.OK)
public Object errorHandler(Exception e, HttpServletRequest httpServletRequest) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("success", false);
if (e instanceof NoHandlerFoundException) {
jsonObject.put("code", 404);
jsonObject.put("msg", "找不到请求资源");
} else if (e instanceof MissingServletRequestParameterException) {
jsonObject.put("code", -200);
jsonObject.put("msg", "缺少参数");
} else if (e instanceof UnauthenticatedException) {
jsonObject.put("code", 401);
jsonObject.put("msg", "用户未登录,请登录");
} else if (e instanceof AuthorizationException) {
jsonObject.put("code", 402);
jsonObject.put("msg", "权限不足");
} else if (e instanceof AuthenticationException) {
jsonObject.put("code", 403);
jsonObject.put("msg", "帐号密码错误,请重新登录");
} else if (e instanceof MaxUploadSizeExceededException) {
jsonObject.put("code", 240);
jsonObject.put("msg", "文件上传超出大小限制");
} else if (e instanceof SQLException) {
jsonObject.put("code", 250);
jsonObject.put("msg", "数据库操作失败");
} else if (e instanceof SocketTimeoutException) {
jsonObject.put("code", 260);
jsonObject.put("msg", "服务连接超时");
} else if (e instanceof SocketException) {
jsonObject.put("code", 240);
jsonObject.put("msg", "服务连接失败");
} else if (e instanceof IOException) {
jsonObject.put("code", 500);
jsonObject.put("msg", "系统错误");
e.printStackTrace();
} else {
jsonObject.put("code", 500);
jsonObject.put("msg", "系统错误");
e.printStackTrace();
}
return jsonObject;
}
}
用户实体类
@Data
public class User {
// ID
private Integer id;
// 用户名
private String userName;
// 密码
private String password;
// 盐值
private String salt;
}
角色实体类
@Data
public class Role {
// ID
private Integer id;
// 角色字符串
private String roleKey;
}
菜单实体类
@Data
public class Menu {
// ID
private Integer id;
// 权限字符串
private String perms;
}
接口
public interface UserService {
User selectByUserName(String username);
}
实现类
@Service
public class UserServiceImpl implements UserService {
@Override
public User selectByUserName(String username) {
User user = new User();
user.setUserName(username);
user.setPassword("dc483e80a7a0bd9ef71d8cf973673924");
return user;
}
}
这里为了方便演示把密码写死了
接口
public interface RoleService {
List<Role> selectRoleByUserId(Integer id);
}
实现类
@Service
public class RoleServiceImpl implements RoleService {
@Override
public List<Role> selectRoleByUserId(Integer id) {
List<Role> roles = new ArrayList<>();
Role admin = new Role();
admin.setRoleKey("admin");
roles.add(admin);
return roles;
}
}
这里为了方便演示把角色写死了
接口
public interface MenuService {
List<Menu> selectPermsByUserId(Integer id);
}
实现类
@Service
public class MenuServiceImpl implements MenuService {
@Override
public List<Menu> selectPermsByUserId(Integer id) {
Menu saveUser = new Menu();
saveUser.setPerms("sys:user:save");
List<Menu> menus = new ArrayList<>();
menus.add(saveUser);
return menus;
}
}
这里为了方便演示把权限写死了
public class R extends HashMap<String, Object> {
public static final int SUCCESS_CODE = 200;
private R() {
}
public static R build(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R success() {
R r = new R();
r.put("code", 200);
r.put("msg", "success");
return r;
}
public static R success(String key, Object value) {
R r = R.success();
r.put(key, value);
return r;
}
public static R failure() {
R r = new R();
r.put("code", 500);
r.put("msg", "操作失败");
return r;
}
public static R failure(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public R add(String key, Object value) {
super.put(key, value);
return this;
}
public R delete(String key) {
if (key != null && (!"code".equals(key) || !"msg".equals(key))) {
this.remove(key);
}
return this;
}
public int getCode() {
return (int) this.get("code");
}
public void setCode(int code) {
this.put("code", code);
}
public String getMsg() {
return (String) this.get("msg");
}
public void setMsg(String msg) {
this.put("msg", msg);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache"/>
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU"/>
<cache name="user"
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LFU"/>
</ehcache>
配置详解
<!--
name:缓存名称。
maxElementsInMemory:缓存最大个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
maxElementsOnDisk:硬盘最大缓存个数。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
-->
@RestController
public class LoginController {
@Autowired
private UserService service;
@RequestMapping("/login")
public R login(String userName, String password) {
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return R.failure(590, "帐号或者密码不能为空");
}
User user = service.selectByUserName(userName);
if (user == null) {
return R.failure(590, "帐号不存在");
}
if (!encrypt(userName, password).equals(user.getPassword())) {
return R.failure(590, "密码错误");
}
//token生成采用加密后的密码,这个和realm中的校验必须一致
return R.success("token", JWTUtils.sign(userName, user.getPassword()));
}
}
@RestController
@RequestMapping("user")
public class UserController {
@RequestMapping("save")
@RequiresPermissions("sys:user:save")
public R save() {
return R.success();
}
@RequestMapping("delete")
@RequiresPermissions("sys:user:delete")
public R delete() {
return R.success();
}
}
错误演示
正确演示
save
接口和delete
接口分别需要sys:user:save
权限和sys:user:delete
权限才能访问。
这里为了方便演示,查询权限的业务类写死了,任意用户都只有save
权限
由于查询权限业务写死,任意用户都有save
权限,而save
接口刚好需要save权限才能访问,所以我们可以正常访问。
但delete
接口就需要delete
权限,而我们的业务写死了,只有save
权限,这个时候访问delete
接口就没有权限访问。
RequiresAuthentication
:使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证(一般指需要登录)。RequiresGuest
:使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。RequiresPermissions
:当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。RequiresRoles
:当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常(下面列出解决办法)。RequiresUser
:当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。如果想模拟真实业务通过数据库查询用户信息以及角色和权限,以下为对应实体类的数据表脚本。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int NOT NULL AUTO_INCREMENT,
`perms` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES (1, 'sys:user:save');
INSERT INTO `menu` VALUES (2, 'sys:user:delete');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int NOT NULL,
`role_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色字符串',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'root');
INSERT INTO `role` VALUES (2, 'admin');
-- ----------------------------
-- Table structure for role_menu
-- ----------------------------
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu` (
`id` int NOT NULL AUTO_INCREMENT,
`rid` bigint NULL DEFAULT NULL COMMENT '角色ID',
`mid` bigint NULL DEFAULT NULL COMMENT '权限ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_menu
-- ----------------------------
INSERT INTO `role_menu` VALUES (1, 1, 1);
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
`salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '盐值',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', 'dc483e80a7a0bd9ef71d8cf973673924', NULL);
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int NOT NULL AUTO_INCREMENT,
`uid` bigint NULL DEFAULT NULL COMMENT '用户ID',
`rid` bigint NULL DEFAULT NULL COMMENT '角色ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户和角色关联表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
SET FOREIGN_KEY_CHECKS = 1;
本文教程案例下载:https://download.csdn.net/download/qq_31762741/85384639
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/qq_31762741/article/details/124784627
内容来源于网络,如有侵权,请联系作者删除!