之前介绍了springboot使用security进行权限管理,这篇文件介绍一下springboot使用shiro进行安全管理。
简述本文的场景,本文使用springboot1.5.9+mysql+jpa+thymeleaf+shiro制作一个简单的验证,其中有2个角色,分别是admin和user,admin可以使用select和delete功能,user只能使用select功能。
新建项目,加入shiro依赖,pom文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.dalaoyang</groupId> <artifactId>springboot_shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot_shiro</name> <description>springboot_shiro</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>net.sourceforge.nekohtml</groupId> <artifactId>nekohtml</artifactId> <version>1.9.15</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
配置文件如下:
##端口号 server.port=8888 ##数据库配置 ##数据库地址 spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=utf8&useSSL=false ##数据库用户名 spring.datasource.username=root ##数据库密码 spring.datasource.password=root ##数据库驱动 spring.datasource.driver-class-name=com.mysql.jdbc.Driver ##validate 加载hibernate时,验证创建数据库表结构 ##create 每次加载hibernate,重新创建数据库表结构,这就是导致数据库表数据丢失的原因。 ##create-drop 加载hibernate时创建,退出是删除表结构 ##update 加载hibernate自动更新数据库结构 ##validate 启动时验证表的结构,不会创建表 ##none 启动时不做任何操作 spring.jpa.hibernate.ddl-auto=update ##控制台打印sql spring.jpa.show-sql=true # 建议在开发时关闭缓存,不然没法看到实时页面 spring.thymeleaf.cache=false ##去除thymeleaf的html严格校验 spring.thymeleaf.mode=LEGACYHTML5
创建了三个实体类,分别是 SysUser(用户表)
package com.dalaoyang.entity; import org.hibernate.validator.constraints.NotEmpty; import javax.persistence.*; import java.io.Serializable; import java.util.List; /** * @author dalaoyang * @Description * @project springboot_learn * @package com.dalaoyang.entity * @email yangyang@dalaoyang.cn * @date 2018/5/2 */ @Entity public class SysUser implements Serializable { @Id @GeneratedValue private Integer userId; @NotEmpty private String userName; @NotEmpty private String passWord; //多对多关系 @ManyToMany(fetch= FetchType.EAGER) //急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载 //FetchType.LAZY:懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载 @JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "userId") }, inverseJoinColumns ={@JoinColumn(name = "roleId") }) private List<SysRole> roleList;// 一个用户具有多个角色 public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassWord() { return passWord; } public void setPassWord(String passWord) { this.passWord = passWord; } public List<SysRole> getRoleList() { return roleList; } public void setRoleList(List<SysRole> roleList) { this.roleList = roleList; } }
SysRole(角色表)
package com.dalaoyang.entity; import org.hibernate.validator.constraints.NotEmpty; import javax.persistence.*; import java.io.Serializable; import java.util.List; /** * @author dalaoyang * @Description * @project springboot_learn * @package com.dalaoyang.entity * @email yangyang@dalaoyang.cn * @date 2018/5/2 */ @Entity public class SysRole implements Serializable { @Id @GeneratedValue private Integer roleId; private String roleName; //多对多关系 @ManyToMany(fetch= FetchType.EAGER) @JoinTable(name="SysRoleMenu",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="menuId")}) private List<SysMenu> menuList; //多对多关系 @ManyToMany @JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="userId")}) private List<SysUser> userList;// 一个角色对应多个用户 public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } public List<SysMenu> getMenuList() { return menuList; } public void setMenuList(List<SysMenu> menuList) { this.menuList = menuList; } public List<SysUser> getUserList() { return userList; } public void setUserList(List<SysUser> userList) { this.userList = userList; } }
SysMenu(菜单表)
package com.dalaoyang.entity; import javax.persistence.*; import java.io.Serializable; import java.util.List; /** * @author dalaoyang * @Description * @project springboot_learn * @package com.dalaoyang.entity * @email yangyang@dalaoyang.cn * @date 2018/5/2 */ @Entity public class SysMenu implements Serializable { @Id @GeneratedValue private Integer menuId; private String menuName; @ManyToMany @JoinTable(name="SysRoleMenu",joinColumns={@JoinColumn(name="menuId")},inverseJoinColumns={@JoinColumn(name="roleId")}) private List<SysRole> roleList; public Integer getMenuId() { return menuId; } public void setMenuId(Integer menuId) { this.menuId = menuId; } public String getMenuName() { return menuName; } public void setMenuName(String menuName) { this.menuName = menuName; } public List<SysRole> getRoleList() { return roleList; } public void setRoleList(List<SysRole> roleList) { this.roleList = roleList; } }
创建一个UserRepository用于查询用户信息:
package com.dalaoyang.repository; import com.dalaoyang.entity.SysUser; import org.springframework.data.repository.CrudRepository; /** * @author dalaoyang * @Description * @project springboot_learn * @package com.dalaoyang.repository * @email yangyang@dalaoyang.cn * @date 2018/5/2 */ public interface UserRepository extends CrudRepository<SysUser,Long> { SysUser findByUserName(String username); }
创建几个前台页面进行测试,分别是: login.html:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> 错误信息:<h4 th:text="${msg}"></h4> <form action="" method="post"> <p>账号:<input type="text" name="username" value="dalaoyang"/></p> <p>密码:<input type="text" name="password" value="123"/></p> <p><input type="submit" value="登录"/></p> </form> </body> </html>
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> index <br/> <form th:action="@{/logout}" method="post"> <p><input type="submit" value="注销"/></p> </form> </body> </html>
delete.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> delete </body> </html>
select.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> select </body> </html>
403.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 403 </body> </html>
创建一个ShiroConfig,代码如下:
package com.dalaoyang.config; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; /** * @author dalaoyang * @Description * @project springboot_learn * @package com.dalaoyang.config * @email yangyang@dalaoyang.cn * @date 2018/5/2 */ @Configuration public class ShiroConfig { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { logger.info("启动shiroFilter--时间是:" + new Date()); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //shiro拦截器 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>(); //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 --> // 配置不被拦截的资源及链接 filterChainDefinitionMap.put("/static/**", "anon"); // 退出过滤器 filterChainDefinitionMap.put("/logout", "logout"); //配置需要认证权限的 filterChainDefinitionMap.put("/**", "authc"); // 如果不设置默认会自动寻找Web工程根目录下的"/login"页面,即本文使用的login.html shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权界面 shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } //自定义身份认证Realm(包含用户名密码校验,权限校验等) @Bean public MyShiroRealm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); return myShiroRealm; } @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } //开启shiro aop注解支持,不开启的话权限验证就会失效 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } //配置异常处理,不配置的话没有权限后台报错,前台不会跳转到403页面 @Bean(name="simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理 mappings.setProperty("UnauthorizedException","403"); simpleMappingExceptionResolver.setExceptionMappings(mappings); // None by default simpleMappingExceptionResolver.setDefaultErrorView("error"); // No default simpleMappingExceptionResolver.setExceptionAttribute("ex"); // Default is "exception" return simpleMappingExceptionResolver; } }
在配置一个MyShiroRealm用于登录认证和授权认证,代码如下:
package com.dalaoyang.config; import com.dalaoyang.entity.SysMenu; import com.dalaoyang.entity.SysRole; import com.dalaoyang.entity.SysUser; import com.dalaoyang.repository.UserRepository; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; 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 javax.annotation.Resource; /** * @author dalaoyang * @Description * @project springboot_learn * @package com.dalaoyang.config * @email yangyang@dalaoyang.cn * @date 2018/5/2 */ public class MyShiroRealm extends AuthorizingRealm { @Resource private UserRepository userRepository; //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); SysUser userInfo = (SysUser)principals.getPrimaryPrincipal(); for(SysRole role:userInfo.getRoleList()){ authorizationInfo.addRole(role.getRoleName()); for(SysMenu menu:role.getMenuList()){ authorizationInfo.addStringPermission(menu.getMenuName()); } } return authorizationInfo; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获得当前用户的用户名 String username = (String)token.getPrincipal(); System.out.println(token.getCredentials()); //根据用户名找到对象 //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 SysUser userInfo = userRepository.findByUserName(username); if(userInfo == null){ return null; } //这里会去校验密码是否正确 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userInfo, //用户名 userInfo.getPassWord(),//密码 getName() ); return authenticationInfo; } }
最后新建一个controller,其中本文使用了2种验证权限的方法,select方法使用@RequiresPermissions("select")来验证用户是否具有select权限,delete方法使用@RequiresRoles("admin")来验证用户是否是admin,代码如下:
package com.dalaoyang.controller; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import java.util.Map; /** * @author dalaoyang * @Description * @project springboot_learn * @package com.dalaoyang.controller * @email yangyang@dalaoyang.cn * @date 2018/5/2 */ @Controller public class TestController { @GetMapping({"/","/index"}) public String index(){ return"index"; } @GetMapping("/403") public String unauthorizedRole(){ return "403"; } @GetMapping("/delete") //@RequiresPermissions("delete") @RequiresRoles("admin") public String delete(){ return "delete"; } @GetMapping("/select") @RequiresPermissions("select") public String select(){ return "select"; } @RequestMapping("/login") public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{ System.out.println("HomeController.login()"); // 登录失败从request中获取shiro处理的异常信息。 // shiroLoginFailure:就是shiro异常类的全类名. String exception = (String) request.getAttribute("shiroLoginFailure"); String msg = ""; //根据异常判断错误类型 if (exception != null) { if (UnknownAccountException.class.getName().equals(exception)) { msg = "账号不存在"; } else if (IncorrectCredentialsException.class.getName().equals(exception)) { msg = "密码不正确"; } else { msg = "else >> "+exception; } } map.put("msg", msg); // 此方法不处理登录成功,由shiro进行处理 return "/login"; } @GetMapping("/logout") public String logout(){ return "/login"; } }
为了方便测试,本人插入了几条初始数据,sql如下:
INSERT INTO `shiro`.`sys_menu`(`menu_id`, `menu_name`) VALUES (1, 'add'); INSERT INTO `shiro`.`sys_menu`(`menu_id`, `menu_name`) VALUES (2, 'delete'); INSERT INTO `shiro`.`sys_menu`(`menu_id`, `menu_name`) VALUES (3, 'update'); INSERT INTO `shiro`.`sys_menu`(`menu_id`, `menu_name`) VALUES (4, 'select'); INSERT INTO `shiro`.`sys_role`(`role_id`, `role_name`) VALUES (1, 'admin'); INSERT INTO `shiro`.`sys_role`(`role_id`, `role_name`) VALUES (2, 'user'); INSERT INTO `shiro`.`sys_role_menu`(`role_id`, `menu_id`) VALUES (1, 1); INSERT INTO `shiro`.`sys_role_menu`(`role_id`, `menu_id`) VALUES (1, 2); INSERT INTO `shiro`.`sys_role_menu`(`role_id`, `menu_id`) VALUES (1, 3); INSERT INTO `shiro`.`sys_role_menu`(`role_id`, `menu_id`) VALUES (1, 4); INSERT INTO `shiro`.`sys_role_menu`(`role_id`, `menu_id`) VALUES (2, 4); INSERT INTO `shiro`.`sys_user`(`user_id`, `pass_word`, `user_name`) VALUES (1, '123', 'dalaoyang'); INSERT INTO `shiro`.`sys_user`(`user_id`, `pass_word`, `user_name`) VALUES (2, '123', 'xiaoli'); INSERT INTO `shiro`.`sys_user_role`(`role_id`, `user_id`) VALUES (1, 1); INSERT INTO `shiro`.`sys_user_role`(`role_id`, `user_id`) VALUES (2, 2);
启动项目,我在这里就不一一截图了,口述一下,访问http://localhost:8888/select由于没有登录的原因,会自动跳转到http://localhost:8888/login,输入错误的用户名和密码会出现对应的提示。输入角色user的用户名xiaoli,密码123。访问http://localhost:8888/select页面会正常跳转,访问http://localhost:8888/delete会拦截到403页面。
如果使用角色为admin的用户dalaoyang密码123登录,以上请求全可以正常访问。
源码下载 :大老杨码云
个人网站:dalaoyang.cn