网上翻了好久 都没有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(); 总觉得这样用的不爽