本文首發於:https://antoniopeng.comjavascript
數據庫這裏以 MySQL 爲例css
所需表以下:html
/* 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 文件加入到項目當中java
這裏咱們須要定義一個業務接口查詢用戶的相關信息(包括用戶關聯的角色與權限)jquery
這裏不闡述具體的 SQL 語句git
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
依賴github
<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
類實現認證與受權web
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
配置類ajax
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 標籤有如下: