luckyxl0 2020-04-19
本文首发于:https://antoniopeng.com
数据库这里以 MySQL 为例
所需表如下:
/* 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 语句
public interface UserService { /** * 根据用户名查询用户信息(包含角色及权限信息) * @param username 用户名 * @return User */ User selectByUsername(String username); }
@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; } }
创建 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 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"; } }
引入 jquery.js
<!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>
<!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>
<!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>
除了在 ShiroConfig 配置类中自定义权限过滤规则,还可以使用 Shiro 提供的注解实现权限过滤,在 Controller 中的每个请求方法上可以添加以下注解实现权限控制:
@RequiresAuthentication: 只有认证通过的用户才能访问
@RequiresRoles(value = {“root”}, logical = Logical.OR) :
@RequiresPermissions(value = {“/user/delete”}, logical = Logical.OR) :
修改 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 标签有以下: