用户角色权限数据库设计
数据库这里以 MySQL 为例
创建数据库
所需表如下:
- user:用户表
- role:角色表
- perm:权限菜单表
- user_role:用户与角色关联的中间表
- role_prem:角色与权限菜单关联的中间表
执行数据库脚本
/*
Navicat Premium Data Transfer
Source Server : 127.0.0.1
Source Server Type : MySQL
Source Server Version : 50718
Source Host : 127.0.0.1:3306
Source Schema : shiro
Target Server Type : MySQL
Target Server Version : 50718
File Encoding : 65001
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for perm
-- ----------------------------
DROP TABLE IF EXISTS `perm`;
CREATE TABLE `perm` (
`perm_id` int(32) NOT NULL COMMENT '权限主键',
`perm_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限url',
`perm_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限描述',
PRIMARY KEY (`perm_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of perm
-- ----------------------------
INSERT INTO `perm` VALUES (1, '/user/*', '拥有对用户的所有操作权限');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`role_id` int(32) NOT NULL COMMENT '角色主键',
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色名',
`role_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, '超级管理员', '超级管理员');
-- ----------------------------
-- Table structure for role_perm
-- ----------------------------
DROP TABLE IF EXISTS `role_perm`;
CREATE TABLE `role_perm` (
`role_id` int(32) NOT NULL COMMENT '角色主键',
`perm_id` int(32) DEFAULT NULL COMMENT '权限主键'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_perm
-- ----------------------------
INSERT INTO `role_perm` VALUES (1, 1);
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` int(32) NOT NULL COMMENT '用户主键',
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '密码(存储加密后的密码)',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '5dbc683c53b7f317fa45c05bf9499fdd');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` int(32) NOT NULL COMMENT '用户主键',
`role_id` int(32) NOT NULL COMMENT '角色主键'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1);
SET FOREIGN_KEY_CHECKS = 1;
数据库设计完成以后,将相对应的实体类和 mapper 文件加入到项目当中
业务代码
这里我们需要定义一个业务接口查询用户的相关信息(包括用户关联的角色与权限)
这里不阐述具体的 SQL 语句
UserService
public interface UserService {
/**
* 根据用户名查询用户信息(包含角色及权限信息)
* @param username 用户名
* @return User
*/
User selectByUsername(String username);
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User selectByUsername(String username) {
return userMapper.selectByUsername(username);
}
}
引入依赖
在 pox.xml
中添加 org.apache.shiro:shiro-spring
和 com.github.theborakompanioni:thymeleaf-extras-shiro
依赖
<properties>
<thymeleaf-extras-shiro.version>2.0.0</thymeleaf-extras-shiro.version>
<shiro.version>1.4.0</shiro.version>
</properties>
<dependencies>
<!-- Shiro核心依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- Thymeleaf对Shiro的支持 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>${thymeleaf-extras-shiro.version}</version>
</dependency>
</dependencies>
自定义认证和授权
创建 MyRealm
类实现认证与授权
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.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
/**
* 自定义Realm,实现授权与认证
*/
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 用户认证
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userService.selectByUsername(token.getUsername());
if (user == null) {
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
/**
* 用户授权
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
if (user != null) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<String> roles = new LinkedList<>();
List<String> perms = new LinkedList<>();
for (Role role : user.getRoleList()) {
roles.add(role.getRoleName());
}
for (Perm perm : user.getPermList()) {
perms.add(perm.getPermUrl());
}
simpleAuthorizationInfo.addRoles(roles);
simpleAuthorizationInfo.addStringPermissions(perms);
return simpleAuthorizationInfo;
}
return null;
}
}
Shiro 配置类
创建 ShiroConfig
配置类
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
@Configuration
public class ShiroConfig {
/**
* 配置密码加密
*/
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 散列算法(加密)
credentialsMatcher.setHashAlgorithmName("MD5");
// 散列次数(加密次数)
credentialsMatcher.setHashIterations(1);
// storedCredentialsHexEncoded 默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
/**
* 注入自定义的 Realm
*/
@Bean("MyRealm")
public MyRealm MyRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
MyRealm MyRealm = new MyRealm();
MyRealm.setCredentialsMatcher(matcher);
return MyRealm;
}
/**
* 配置自定义权限过滤规则
*/
@Bean
public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setSuccessUrl("/index.html");
bean.setLoginUrl("/login.html");
bean.setUnauthorizedUrl("/unauthorized.html");
/**
* anon:匿名用户可访问
* authc:认证用户可访问
* user:使用rememberMe可访问
* perms:对应权限可访问
* role:对应角色权限可访问
**/
Map<String, String> filterMap = new LinkedHashMap<>();
/**
* 允许匿名访问静态资源
*/
filterMap.put("/image/**", "anon");
filterMap.put("/css/**", "anon");
filterMap.put("/js/**", "anon");
filterMap.put("/plugin/**", "anon");
/**
* 允许匿名访问登录页面和登录操作
*/
filterMap.put("/login.html", "anon");
filterMap.put("/login.do", "anon");
/**
* 其它所有请求需要登录认证后才能访问
*/
filterMap.put("/**", "authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
/**
* 注入 securityManager
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher, @Qualifier("sessionManager") DefaultWebSessionManager defaultWebSessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(MyRealm(hashedCredentialsMatcher));
securityManager.setSessionManager(defaultWebSessionManager);
return securityManager;
}
/**
* 开启权限注解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 配置异常跳转页面
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
// 未认证跳转页面(跳转路径为项目里的页面相对路径,并非 URL)
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "login");
// 权限不足跳转页面
properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "unauthorized");
resolver.setExceptionMappings(properties);
return resolver;
}
/**
* 会话管理器
*/
@Bean("sessionManager")
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
// 设置用户登录信息失效时间为一天(单位:ms)
defaultWebSessionManager.setGlobalSessionTimeout(1000L * 60L * 60L * 24L);
return defaultWebSessionManager;
}
/**
* 重置 ShiroDialect,省略此步将不能在 Thymeleaf 页面使用 Shiro 标签
*/
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}
Controller
@Controller
public class IndexController {
@Autowired
private UserService userService;
@RequestMapping(value = "login.html")
public String loginView() {
// 判断当前用户是否通过认证
if (SecurityUtils.getSubject().isAuthenticated()) {
// 认证通过,重定向到首页
return "redirect:index.html";
} else {
// 未认证或认证失败,转发到登录页
return "login";
}
}
@RequestMapping(value = "login.do")
@ResponseBody
public AppReturn loginDo(@RequestParam String username, @RequestParam String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
try {
// 执行认证
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
return AppReturn.defeated("账号不存在");
} catch (IncorrectCredentialsException e) {
return AppReturn.defeated("密码错误");
}
return AppReturn.succeed("登录成功");
}
@RequestMapping(value = "index.html")
public String indexView() {
return "index";
}
@RequestMapping(value = "logout.do")
public String logoutDo() {
if (SecurityUtils.getSubject().isAuthenticated()) {
// 退出
SecurityUtils.getSubject().logout();
}
return "redirect:login.html";
}
@RequestMapping(value = "unauthorized.html")
public String unauthorizedView() {
return "unauthorized";
}
}
@Controller
public class IndexController {
@Autowired
private UserService userService;
@RequestMapping(value = "login.html")
public String loginView() {
// 判断当前用户是否通过认证
if (SecurityUtils.getSubject().isAuthenticated()) {
// 认证通过,重定向到首页
return "redirect:index.html";
} else {
// 未认证或认证失败,转发到登录页
return "login";
}
}
@RequestMapping(value = "login.do")
@ResponseBody
public AppReturn loginDo(@RequestParam String username, @RequestParam String password) {
return userService.loginDo(username, password);
}
@RequestMapping(value = "index.html")
public String indexView() {
return "index";
}
@RequestMapping(value = "logout.do")
public String logoutDo() {
if (SecurityUtils.getSubject().isAuthenticated()) {
// 退出
SecurityUtils.getSubject().logout();
}
return "redirect:login.html";
}
@RequestMapping(value = "unauthorized.html")
public String unauthorizedView() {
return "unauthorized";
}
}
Web 页面
引入 jquery.js
login.html
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div>
用户名:<input id="username" name="username" type="text" /><br/>
密码:<input id="password" name="password" type="password"><br/>
<span id="tip" class="tip"></span><br/>
<button onclick="login()">点击登录</button>
</div>
</body>
<script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
<script type="text/javascript">
function login() {
var username = $('#username').val()
var password = $('#password').val()
$.ajax({
url: '/login.do'
, data: {
username: username
, password: password
}
, type: 'post'
, dataType: 'json'
, success: function(res) {
if (res.code == 200) {
// 登录成功,跳转到 index.html
window.location.href = '/index.html'
} else {
// 登录失败,提示登录错误信息
$("#tip").text(res.msg)
}
}
, error: function() {
$("#tip").text('服务器响应失败')
}
})
}
</script>
</html>
index.html
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
Hello Shiro
<a href="/logout.do">退出</a>
</body>
</html>
unauthorized.html
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>无权访问</title>
</head>
<body>
权限不足
</body>
</html>
Java 中使用 Shiro 权限注解
除了在 ShiroConfig 配置类中自定义权限过滤规则,还可以使用 Shiro 提供的注解实现权限过滤,在 Controller 中的每个请求方法上可以添加以下注解实现权限控制:
@RequiresAuthentication: 只有认证通过的用户才能访问
@RequiresRoles(value = {“root”}, logical = Logical.OR) :
- value:指定拥有 root 角色才能访问,角色可以是多个,以逗号隔开
- logical:该属性有两个值,Logical.OR(只要拥有其中一个角色就能访问),Logical.AND(需要拥有指定的全部角色才能访问,否则会抛出权限不足异常)
@RequiresPermissions(value = {“/user/delete”}, logical = Logical.OR) :
- **value:**指定拥有 /user/delete 权限才能访问,权限可以是多个,以逗号隔开
- **logical:**有两个值,Logical.OR(只要拥有其中一个权限就访问),Logical.AND(需要拥有指定的全部权限才能访问,否则会抛出权限不足异常)
Thymeleaf 模板中使用 Shiro 权限标签
修改 thymeleaf 模板的 html 标签,加入 xmlns:shiro=”http://www.pollix.at/thymeleaf/shiro 命名空间:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
常用的 Shiro 标签有以下:
- shiro:hasRole=”root”:需要拥有root角色
- shiro:hasAnyRoles=”root,guest”:需要拥有root和guest中的任意一个角色
- <shiro:hasAllRoles =”root,guest”>:需要同时拥有root和guest角色
- shiro:hasPerm:原理同上
- shiro:hasAnyPerms :原理同上
- shiro:hasAllPerms :原理同上
登录
- 启动项目
- 访问 http://localhost:8080
- 用户名:root
- 登录密码:123456
- 文章作者:彭超
- 本文首发于个人博客:https://antoniopeng.com/2019/06/14/springboot/SpringBoot%E6%95%B4%E5%90%88Shiro%E5%AE%9E%E7%8E%B0%E7%99%BB%E5%BD%95%E8%AE%A4%E8%AF%81%E4%B8%8E%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6/
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 彭超 | Blog!