網上翻了好久 都沒有SpringBoot+Shiro的入門教程 原本想看《跟我學Shiro》
然后發現這是從頭開始 但是我就需要和SpringBoot整一塊 不需要那么多的東西 感覺這個當參考書不錯
於是東拼西湊終於整成了 把別人的教程上我用不到的都刪了 該改的改 終於拿到了我理想中的效果
先是數據庫部分 因為是簡單的實現 就沒有弄得太復雜
三部分 用戶 -- 角色 -- 權限
四張表
用戶和角色是多對一的關系(多對多懶得弄...)
角色和權限是多對多的關系(這個沒啥好說的)
直接放代碼了 那個url忽略就好了 完全沒用上 我打算弄菜單的自動獲取能訪問的列表 這樣就不用寫死了

DROP TABLE IF EXISTS tb_user; CREATE TABLE tb_user ( `id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE KEY, `password` VARCHAR(255) NOT NULL, `role_id` INT NOT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); DROP TABLE IF EXISTS tb_role; CREATE TABLE tb_role ( `id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL UNIQUE KEY, `desc` VARCHAR(50) NOT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); DROP TABLE IF EXISTS tb_permission; CREATE TABLE tb_permission ( `id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT, `parent_id` INT, `name` VARCHAR(50) NOT NULL UNIQUE KEY, `desc` VARCHAR(50) NOT NULL, `url` VARCHAR(255) NOT NULL DEFAULT '#', `order_by` INT, `type` INT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); DROP TABLE IF EXISTS tb_role_permission; CREATE TABLE tb_role_permission ( `id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT, `role_id` INT NOT NULL, `permission_id` INT NOT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); INSERT INTO tb_permission (`id`, `name`, `desc`, url, `order_by`, `type`) VALUES ('1', 'user:*', '用戶管理', '/getAll', '1', '1'); INSERT INTO tb_permission (`id`, `name`, `desc`, url, `order_by`, `type`) VALUES ('2', 'user:query', '查詢用戶', '#', '1', '1'); INSERT INTO tb_permission (`id`, `name`, `desc`, url, `order_by`, `type`) VALUES ('3', 'user:insert', '新增用戶', '#', '1', '1'); INSERT INTO tb_permission (`id`, `name`, `desc`, url, `order_by`, `type`) VALUES ('4', 'user:update', '修改用戶', '#', '1', '1'); INSERT INTO tb_permission (`id`, `name`, `desc`, url, `order_by`, `type`) VALUES ('5', 'user:delete', '刪除用戶', '#', '1', '1'); INSERT INTO tb_role (`id`, `name`, `desc`) VALUES ('1', 'admin', '系統管理員'); INSERT INTO tb_role (`id`, `name`, `desc`) VALUES ('2', 'users', '普通用戶'); INSERT INTO tb_role_permission (`role_id`, `permission_id`) VALUES ('1', '1'); INSERT INTO tb_role_permission (`role_id`, `permission_id`) VALUES ('2', '2'); INSERT INTO tb_user (`id`, `username`, `password`, `role_id`) VALUES ('1', 'admin', 'admin', '1'); INSERT INTO tb_user (`id`, `username`, `password`, `role_id`) VALUES ('2', 'user1', '123456', '2');
這塊設置了倆角色 管理組和用戶組 管理組有查詢權限 用戶組沒有查詢權限
數據庫整完之后就是java部分 先上依賴

<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion> <spring.version>2.2.6.RELEASE</spring.version> <nekohtml.version>1.9.22</nekohtml.version> <jdbc.version>8.0.16</jdbc.version> <druid.version>1.1.10</druid.version> <mybatis-spring.version>1.3.0</mybatis-spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-tomcat</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${jdbc.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <!-- thymeleaf網頁解析 --> <dependency> <groupId>net.sourceforge.nekohtml</groupId> <artifactId>nekohtml</artifactId> <version>${nekohtml.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies>
springboot的配置文件 現在咋都好用yml了 properties多好 yml還要考慮縮進

server: port: 8080 tomcat: uri-encoding: utf-8 spring: thymeleaf: mode: LEGACYHTML5 cache: false datasource: url: jdbc:mysql://localhost/db_test username: test password: test driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource mybatis: typeAliasesPackage: cn.erika.user.model mapper-locations: classpath:static/mybatis/*Mapper.xml shiro: url: login: login index: index unauthorized: login session: timeout: 30 validationInterval: 15 cookie: domain: path: / timeout: 7
shiro部分除了url 其他的都沒用上 但還是配上了
鑒權的數據來自與數據庫 所以首先要把數據查詢部分搞好
mybatis咋玩在這就不寫了 在這個實驗性質的程序上 至少要實現根據用戶查角色和根據用戶查權限
我的一個用戶只有一個角色 所以根據用戶名查到用戶就行
查到用戶之后就可以根據用戶查權限 這是服務層的這個方法的簽名
public Set<String> getPermissionsByUserId(Integer userId);
返回值是字符串的set集 注意是set 我習慣性的返回list之后 寫realm的時候發現他只接受set
下面這些都是shiro相關的代碼 需要自己去實現的部分

1 package cn.erika.shiro.realm; 2 3 import cn.erika.user.model.User; 4 import cn.erika.user.service.IUserService; 5 import org.apache.shiro.authc.*; 6 import org.apache.shiro.authz.AuthorizationInfo; 7 import org.apache.shiro.authz.SimpleAuthorizationInfo; 8 import org.apache.shiro.realm.AuthorizingRealm; 9 import org.apache.shiro.subject.PrincipalCollection; 10 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.stereotype.Component; 12 13 /** 14 * 這個就是認證的實現類 15 * 你得自己去實現如何去認證 16 * 這里實現了使用用戶名和密碼進行認證 17 */ 18 @Component 19 public class UserRealm extends AuthorizingRealm { 20 21 @Autowired 22 private IUserService userService; 23 24 // 這塊是權限鑒定 就是登錄成功后把權限查出來 25 @Override 26 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 27 // 首先要把用戶查出來 28 User user = userService.getUserByUsername(principalCollection.toString()); 29 // 然后把他的角色和權限查出來 扔進 30 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 31 info.setRoles(user.getRoleNames()); 32 info.setStringPermissions(userService.getPermissionsByUserId(user.getId())); 33 return info; 34 } 35 36 // 這塊是身份鑒定 相當於登錄 37 @Override 38 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { 39 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; 40 User user = userService.getUserByUsername(token.getUsername()); 41 // 我這塊就寫了一個異常 42 if (user == null) { 43 throw new UnknownAccountException(); 44 } 45 // 實際上有好些異常可以往外拋 46 // IncorrectCredentialsException 憑證錯誤 可以理解為密碼錯誤 47 // DisabledAccountException 賬號被禁用 48 // LockedAccountException 賬號被鎖定 49 // ExcessiveAttemptsException 登錄失敗次數超過限制 50 // ... 還有好些 他們都是 AuthenticationException的子類 51 52 // 這里返回一個認證信息 53 return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName()); 54 } 55 }
Shiro的配置

1 package cn.erika.shiro; 2 3 import cn.erika.shiro.realm.UserRealm; 4 import org.apache.shiro.mgt.SecurityManager; 5 import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; 6 import org.apache.shiro.spring.web.ShiroFilterFactoryBean; 7 import org.apache.shiro.web.mgt.DefaultWebSecurityManager; 8 import org.springframework.beans.factory.annotation.Value; 9 import org.springframework.context.annotation.Bean; 10 import org.springframework.context.annotation.Configuration; 11 12 import java.util.HashMap; 13 14 @Configuration 15 public class ShiroConfig { 16 // 登錄頁面的URL 17 @Value("${shiro.url.login}") 18 private String loginUrl; 19 20 // 主頁URL 認證成功會跳到這里 21 @Value("${shiro.url.index}") 22 private String indexUrl; 23 24 // 認證失敗的URL 我這里其實還是跳到了登錄頁面 25 @Value("${shiro.url.unauthorized}") 26 private String unauthorizedUrl; 27 28 @Bean 29 public SecurityManager securityManager(UserRealm userRealm) { 30 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); 31 securityManager.setRealm(userRealm); 32 return securityManager; 33 } 34 35 @Bean 36 public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { 37 ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); 38 factoryBean.setSecurityManager(securityManager); 39 factoryBean.setLoginUrl(loginUrl); 40 factoryBean.setUnauthorizedUrl(unauthorizedUrl); 41 42 HashMap<String, String> filterChain = new HashMap<>(); 43 filterChain.put("/favicon.ico", "anon"); 44 filterChain.put("/login", "anon"); 45 46 filterChain.put("/logout", "logout"); 47 filterChain.put("/**","authc"); 48 49 factoryBean.setFilterChainDefinitionMap(filterChain); 50 51 return factoryBean; 52 } 53 54 @Bean 55 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { 56 AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); 57 authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); 58 return authorizationAttributeSourceAdvisor; 59 } 60 }
這是控制器

1 package cn.erika.user.controller; 2 3 import cn.erika.user.dao.UserDao; 4 import cn.erika.user.model.User; 5 import org.apache.shiro.SecurityUtils; 6 import org.apache.shiro.authc.AuthenticationException; 7 import org.apache.shiro.authc.IncorrectCredentialsException; 8 import org.apache.shiro.authc.UnknownAccountException; 9 import org.apache.shiro.authc.UsernamePasswordToken; 10 import org.apache.shiro.authz.annotation.RequiresPermissions; 11 import org.apache.shiro.subject.Subject; 12 import org.springframework.beans.factory.annotation.Autowired; 13 import org.springframework.stereotype.Controller; 14 import org.springframework.web.bind.annotation.*; 15 import org.springframework.web.servlet.mvc.support.RedirectAttributes; 16 17 import java.util.List; 18 19 @Controller 20 public class UserController { 21 22 @Autowired 23 private UserDao userDao; 24 25 @RequestMapping({"", "index"}) 26 public String index() { 27 return "index"; 28 } 29 30 @RequiresPermissions("user:query") 31 @RequestMapping("/users") 32 @ResponseBody 33 public List<User> getUsers() { 34 return userDao.getUsers(); 35 } 36 37 @GetMapping("/login") 38 public String loginFrom() { 39 return "login"; 40 } 41 42 @PostMapping("/login") 43 public String login(User user) { 44 String username = user.getUsername(); 45 System.out.println("用戶名: " + username); 46 UsernamePasswordToken token = new UsernamePasswordToken(username, user.getPassword()); 47 48 Subject subject = SecurityUtils.getSubject(); 49 try { 50 System.out.println("登錄驗證: " + username); 51 subject.login(token); 52 System.out.println("驗證通過: " + username); 53 } catch (UnknownAccountException e) { 54 System.err.println("未知的賬戶"); 55 } catch (IncorrectCredentialsException e) { 56 System.err.println("密碼錯誤"); 57 } catch (AuthenticationException e) { 58 System.err.println("其他錯誤"); 59 e.printStackTrace(); 60 } 61 if (subject.isAuthenticated()) { 62 System.out.println("登錄成功"); 63 return "redirect:/index"; 64 } else { 65 token.clear(); 66 return "redirect:/login"; 67 } 68 } 69 70 @GetMapping("/logout") 71 public String logout(RedirectAttributes redirectAttributes) { 72 SecurityUtils.getSubject().logout(); 73 redirectAttributes.addFlashAttribute("message", "退出登錄"); 74 return "redirect:/login"; 75 } 76 77 78 }
頁面我就不放了 沒啥看頭
主頁就是倆按鈕 一個指向users 一個指向logout
登錄頁面就是用戶名和密碼 指向login
其實到這里功能已經實現了 登錄和權限鑒定都有了
但是很不美 因為如果沒有權限 他直接蹦403 你要是ajax肯定不爽
所以搞了一下Spring的異常統一處理 捕獲了一下AuthorizationException異常 這樣就舒服了

1 package cn.erika.exception; 2 3 import cn.erika.model.Message; 4 import org.apache.shiro.authz.AuthorizationException; 5 import org.springframework.web.bind.annotation.ExceptionHandler; 6 import org.springframework.web.bind.annotation.RestControllerAdvice; 7 8 @RestControllerAdvice 9 public class DefaultExceptionHandler { 10 11 @ExceptionHandler(AuthorizationException.class) 12 public Message authorizationException(AuthorizationException e) { 13 return Message.failed("沒有操作權限"); 14 } 15 }
那個Message類就倆屬性
int code;
String message;
我寫了一堆靜態方法方便調用 不用每次都要填錯誤信息了
我用user1登錄后 查詢用戶信息 就會返回這條信息
開發的時候還是火狐好用 自帶json格式化和上色 這多舒服
數據少的不明顯 你看看數據多的時候
這是谷歌的
這是火狐的
忘了說了 我user里面roleNames這個屬性是為了圖省事 不然就得調 user.getRole().getName(); 總覺得這樣用的不爽