在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是,仅仅从技术角度来说,也可以使用 Shiro。
一般来说,Spring Security
和 Shiro
的比较如下:
Spring Security
是一个重量级的安全管理框架;Shiro
则是一个轻量级的安全管理框架
Spring Security
概念复杂,配置繁琐;Shiro
概念简单、配置简单
Spring Security
功能强大;Shiro
功能简单
……
虽然 Shiro
功能简单,但是也能满足大部分的业务场景。所以在传统的 SSM 项目中,一般来说,可以整合 Shiro
。
在 Spring Boot 中,由于 Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ,当然也提供了 Spring Security
的 Starter ,使得在 Spring Boot 中使用 Spring Security
变得更加容易,甚至只需要添加一个依赖就可以保护所有的接口,所以,如果是 Spring Boot 项目,一般选择 Spring Security
。这只是一个建议的组合,单纯从技术上来说,无论怎么组合,都是没有问题的。
在 Spring Boot 中整合 Shiro
,有两种不同的方案:
第一种就是原封不动的,将 SSM 整合 Shiro
的配置用 Java 重写一遍。
第二种就是使用 Shiro
官方提供的一个 Starter 来配置,但是,这个 Starter 并没有简化多少配置。
准备工作
#### 创建数据库
所需表如下:
user:用户表
role:角色表
perm:权限菜单表
user_role:用户与角色关联的中间表
role_prem:角色与权限菜单关联的中间表
执行数据库脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0 ;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; INSERT INTO `perm` VALUES (1 , '/user/*' , '拥有对用户的所有操作权限' );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; INSERT INTO `role` VALUES (1 , '超级管理员' , '超级管理员' );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; INSERT INTO `role_perm` VALUES (1 , 1 );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; INSERT INTO `user` VALUES (1 , 'root' , 'e10adc3949ba59abbe56e057f20f883e' );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; INSERT INTO `user_role` VALUES (1 , 1 );SET FOREIGN_KEY_CHECKS = 1 ;
数据库创建完成以后,用逆向工程生成对应的实体类和 mapper.xml 文件并加入到项目当中。
业务代码 这里我们需要定义一个业务接口查询用户的相关信息(包括用户关联的角色与权限)
UserService
1 2 3 4 5 6 7 8 9 public interface UserService { User selectByUsername (String username) ; }
UserServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User selectByUsername (String username) { return userMapper.selectByUsername(username); } }
Web 页面 创建 login.html
引入 jquery.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <!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) { window .location.href = '/index.html' } else { $("#tip" ).text(res.msg) } } , error: function () { $("#tip" ).text('服务器响应失败' ) } }) } </script > </html >
创建 index.html
1 2 3 4 5 6 7 8 9 10 11 12 <!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
1 2 3 4 5 6 7 8 9 10 11 <!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 >
原生整合
#### 引入依赖
加入 Shiro 相关的依赖,完整的 pom.xml 文件中的依赖如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-web</artifactId > <version > 1.4.0</version > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-spring</artifactId > <version > 1.4.0</version > </dependency >
创建 Realm 接下来我们来自定义核心组件 Realm
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class MyRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principals) { 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 ; } @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()); } }
配置 Shiro 接下来进行 Shiro
的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 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 ); credentialsMatcher.setStoredCredentialsHexEncoded(true ); return credentialsMatcher; } @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" ); 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; } @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(); 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(); defaultWebSessionManager.setGlobalSessionTimeout(1000L * 60L * 60L * 24L ); return defaultWebSessionManager; } @Bean (name = "shiroDialect" ) public ShiroDialect shiroDialect () { return new ShiroDialect(); } }
在这里进行 Shiro 的配置主要配置 3 个 Bean :
首先需要提供一个 Realm
的实例。
需要配置一个 SecurityManager
,在 SecurityManager
中配置 Realm。
配置一个 ShiroFilterFactoryBean
,在 ShiroFilterFactoryBean
中指定路径拦截规则等。
配置登录和测试接口。
其中,ShiroFilterFactoryBean
的配置稍微多一些,配置含义如下:
setSecurityManager
表示指定 SecurityManager
。
setLoginUrl
表示指定登录页面。
setSuccessUrl
表示指定登录成功页面。
接下来的 Map 中配置了路径拦截规则,注意,要有序。
这些东西都配置完成后,接下来配置登录 Controller
创建 Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 @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" ; } }
最后访问 http://localhost:8080/login.html 进行登录即可。账号:root,密码:123456
整合 Shiro Starter
上面这种配置方式实际上相当于把 SSM 中的 XML 配置拿到 Spring Boot 中用 Java 代码重新写了一遍,除了这种方式之外,我们也可以直接使用 Shiro 官方提供的 Starter 。
引入依赖 添加 shiro-spring-boot-web-starter
依赖,这个依赖可以代替之前的 shiro-web
和 shiro-spring
两个依赖,如下:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-spring-boot-web-starter</artifactId > <version > 1.4.0</version > </dependency >
创建 Realm 这里的 Realm 和前面的一样,我就不再赘述。
配置 Shiro 基本信息 接下来在 application.properties 中配置 Shiro
的基本信息:
1 2 3 4 5 6 shiro.sessionManager.sessionIdCookieEnabled =true shiro.sessionManager.sessionIdUrlRewritingEnabled =true shiro.unauthorizedUrl =/unauthorizedurl shiro.web.enabled =true shiro.successUrl =/index shiro.loginUrl =/login
配置解释:
第一行表示是否允许将sessionId 放到 cookie 中
第二行表示是否允许将 sessionId 放到 Url 地址拦中
第三行表示访问未获授权的页面时,默认的跳转路径
第四行表示开启 shiro
第五行表示登录成功的跳转页面
第六行表示登录页面
配置 ShiroConfig 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Configuration public class ShiroConfig { @Bean MyRealm myRealm () { return new MyRealm(); } @Bean DefaultWebSecurityManager securityManager () { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm()); return manager; } @Bean ShiroFilterChainDefinition shiroFilterChainDefinition () { DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition(); definition.addPathDefinition("/doLogin" , "anon" ); definition.addPathDefinition("/**" , "authc" ); return definition; } }
这里的配置和前面的比较像,但是不再需要 ShiroFilterFactoryBean
实例了,替代它的是 ShiroFilterChainDefinition
,在这里定义 Shiro 的路径匹配规则即可。这里定义完之后,接下来的登录接口定义以及测试方法都和前面的一致,我就不再赘述了,大家可以参考上文。
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` 命名空间:
1 2 3 <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="userAdd>"
:需要拥有 userAdd 权限
<shiro:hasAnyPerms="userAdd,userDelete>"
:需要拥有 userAdd 和 userDelete 中的任意一个权限
<shiro:hasAllPerms="userAdd,userDelete>"
:需要同时拥有 userAdd 和 userDelete 权限
更多干货请移步:https://antoniopeng.com
如果你喜欢这个博客或发现它对你有用,欢迎你点击右下角 “OPEN CHAT” 进行评论。也欢迎你分享这个博客,让更多的人参与进来。如果在博客中的内容侵犯了您的版权,请联系博主删除它们。谢谢你!