Spring Boot 整合 Shiro 实现登录认证与权限控制
本文首发于:https://antoniopeng.com
用户角色权限数据库设计
数据库这里以 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
相关推荐
杜鲁门 2020-11-05
luckyxl0 2020-08-16
Dullonjiang 2020-08-09
xclxcl 2020-08-03
zmzmmf 2020-08-03
MicroBoy 2020-08-02
ganjing 2020-08-02
likesyour 2020-08-01
zmzmmf 2020-07-09
MicroBoy 2020-07-05
zzhao 2020-06-26
子云 2020-06-18
visionzheng 2020-06-07
neweastsun 2020-06-04
ErixHao 2020-06-03
GDreams0 2020-06-01
ganjing 2020-05-29
zmzmmf 2020-05-28
nullcy 2020-05-26