写在前面: 从2018年末开始学习SpringBoot,也用SpringBoot写过一些项目。这里对学习Springboot的一些知识总结记录一下。若是你也在学习SpringBoot,能够关注我,一块儿学习,一块儿进步。css
以前写项目安全控件基本都是用的SpringSecurity,后来发现Shiro在实际开发中用的也挺多的。这里一块儿来看一下Shiro在Springboot中的基本用法吧!想了解SpringSecurity的请移步:SpringSecurity安全控件使用指南。html
Apache Shiro 是 Java 的一个安全(权限)框架。 Shiro 能够很是容易的开发出足够好的应用,其不只能够用在 JavaSE 环境,也能够用在 JavaEE 环境。Shiro 能够完成:认证、受权、加密、会话管理、与Web 集成、缓存等。相对于SpringSecurity简单的多,也没有SpringSecurity那么复杂。java
Shiro三大功能模块mysql
细分功能git
菜单(TbMenu)=====> 页面上须要显示的全部菜单github
角色(SysRole)=====> 角色及角色对应的菜单web
用户(SysUser)=====> 用户及用户对应的角色redis
用户和角色中间表(sys_user_role)====> 用户和角色中间表
【注】 权限管理实现原理都差很少,虽然使用的安全控件不同,但数据库表的设计基本同样,这里数据库表的设计和SpringSecurity实现动态权限菜单控制基本一致。spring
菜单表tb_menu
角色及菜单权限表sys_role,其中父节点parent 为null时为角色,不为null时为对应角色的菜单权限。
用户表sys_user。
用户和角色多对多关系,用户和角色中间表sys_user_role(由Spring-Data-Jpa自动生成)。
sql
建立一个SprintBoot项目,添加项目须要的依赖(这里持久层使用的是SpringDataJpa)。
Shiro依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
pom.xml完整依赖代码
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.mcy</groupId> <artifactId>springboot-shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-shiro</name> <description>Demo project for Spring Boot</description> <properties> <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-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- shiro与spring整合依赖 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</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> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.use-new-id-generator-mappings=false spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true server.port=80 server.servlet.context-path=/shiro
菜单表实体类TbMenu,Spring-Data-Jpa能够根据实体类去数据库新建或更新对应的表结构,详情能够访问Spring-Data-Jpa入门
import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.data.annotation.CreatedBy; import javax.persistence.*; import java.util.ArrayList; import java.util.List; /** * 菜单表 * @author */ @Entity public class TbMenu { private Integer id; private String name; private String url; private Integer idx; @JsonIgnore private TbMenu parent; @JsonIgnore private List<TbMenu> children = new ArrayList<>(); @Id @GeneratedValue public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Column(unique = true) public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public Integer getIdx() { return idx; } public void setIdx(Integer idx) { this.idx = idx; } @ManyToOne @CreatedBy public TbMenu getParent() { return parent; } public void setParent(TbMenu parent) { this.parent = parent; } @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent") @OrderBy(value = "idx") public List<TbMenu> getChildren() { return children; } public void setChildren(List<TbMenu> children) { this.children = children; } }
角色及权限表SysRole,parent 为null时为角色,不为null时为权限。
import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.data.annotation.CreatedBy; import javax.persistence.*; import java.util.ArrayList; import java.util.List; @Entity /*** * 角色及角色对应的菜单权限 * @author *parent 为null时为角色,不为null时为权限 */ public class SysRole { private Integer id; private String name; //名称 @JsonIgnore private SysRole parent; private Integer idx; //排序 @JsonIgnore private List<SysRole> children = new ArrayList<>(); @Id @GeneratedValue public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Column(length = 20) public String getName() { return name; } public void setName(String name) { this.name = name; } @ManyToOne @CreatedBy public SysRole getParent() { return parent; } public void setParent(SysRole parent) { this.parent = parent; } @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent") public List<SysRole> getChildren() { return children; } public void setChildren(List<SysRole> children) { this.children = children; } public Integer getIdx() { return idx; } public void setIdx(Integer idx) { this.idx = idx; } }
最后实现的就是用户管理了,只须要对添加的用户分配对应的角色就能够了,用户登陆时,显示角色对应的权限。
import com.fasterxml.jackson.annotation.JsonIgnore; import javax.persistence.*; import java.util.ArrayList; import java.util.List; /** * 用户表 */ @Entity public class SysUser { private Integer id; private String username; //帐号 private String password; //密码 private String name; //姓名 @JsonIgnore private List<SysRole> roles = new ArrayList<>(); //角色 @Id @GeneratedValue public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Column(length = 20, unique = true) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Column(length = 100) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Column(length = 20) public String getName() { return name; } public void setName(String name) { this.name = name; } @ManyToMany(cascade = CascadeType.REFRESH, fetch = FetchType.EAGER) @JoinTable(name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) public List<SysRole> getRoles() { return roles; } public void setRoles(List<SysRole> roles) { this.roles = roles; } }
Shiro配置类ShiroConfig。
import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * @description Shiro配置类 */ @Configuration public class ShiroConfig { @Bean("hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //指定加密方式为MD5 credentialsMatcher.setHashAlgorithmName("MD5"); //加密次数 credentialsMatcher.setHashIterations(1024); credentialsMatcher.setStoredCredentialsHexEncoded(true); return credentialsMatcher; } @Bean("userRealm") public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) { UserRealm userRealm = new UserRealm(); userRealm.setCredentialsMatcher(matcher); return userRealm; } @Bean public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置 SecurityManager,安全管理器 bean.setSecurityManager(securityManager); // 设置登陆跳转页面 bean.setLoginUrl("/index"); // 设置未受权提示页面(没有权限访问后的页面) bean.setUnauthorizedUrl("/unAuth"); /** * Shiro内置过滤器,能够实现拦截器相关的拦截器 * 经常使用的过滤器: * anon:无需认证(登陆)能够访问 * authc:必须认证才能够访问 * user:若是使用rememberMe的功能能够直接访问 * perms:该资源必须获得资源权限才能够访问 * role:该资源必须获得角色权限才能够访问 **/ Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/login","anon"); filterMap.put("/user","authc"); filterMap.put("/system","perms[system]"); filterMap.put("/static/**","anon"); filterMap.put("/**","authc"); filterMap.put("/logout", "logout"); bean.setFilterChainDefinitionMap(filterMap); return bean; } /** * 注入 securityManager */ @Bean(name="securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联realm. securityManager.setRealm(userRealm(hashedCredentialsMatcher)); return securityManager; } }
自定义Realm用于查询用户的角色和权限信息并保存到权限管理器中。代码以下:
import com.mcy.springbootshiro.entity.SysRole; import com.mcy.springbootshiro.entity.SysUser; import com.mcy.springbootshiro.service.SysUserService; 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; import java.util.Set; public class UserRealm extends AuthorizingRealm { @Autowired private SysUserService userService; /** * 受权逻辑方法 **/ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行受权"); //获取当前登陆对象 Subject subject = SecurityUtils.getSubject(); SysUser user = (SysUser)subject.getPrincipal(); if(user != null){ SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 角色与权限字符串集合 Collection<String> rolesCollection = new HashSet<>(); //得到当前用户的角色 List<SysRole> roles = user.getRoles(); for(SysRole role : roles){ rolesCollection.add(role.getName()); } //添加当前用户的角色权限,用于判断能够访问那些功能 info.addStringPermissions(rolesCollection); return info; } return null; } /** * 认证 **/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行认证"); // 编写shiro判断逻辑,判断用户名和密码 UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken; // 判断用户名 SysUser bean = userService.findByUsername(token.getUsername()); //用户名不存在 if(bean == null){ throw new UnknownAccountException(); } //以帐号做为加密的盐值 ByteSource credentialsSalt = ByteSource.Util.bytes(bean.getUsername()); // 判断密码, return new SimpleAuthenticationInfo(bean, bean.getPassword(), credentialsSalt, getName()); } }
其中密码加密使用的是MD5加密,加密代码以下:
public static void main(String[] args){ String hashAlgorithName = "MD5"; //加密密码 String password = "123456"; //加密次数 int hashIterations = 1024; //帐号做为加密的盐值 ByteSource credentialsSalt = ByteSource.Util.bytes("admin"); Object obj = new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations); System.out.println(obj); }
新建IndexController测试控制器。这里持久层框架使用的是SpringDataJpa,查询只需遵循查询方法命名规范便可,因此这里就直接写控制器代码了。想进一步了解SpringDataJpa的小伙伴,请移步:Spring-Data-Jpa入门。
IndexController控制器代码以下:
import com.mcy.springbootshiro.entity.SysRole; import com.mcy.springbootshiro.entity.SysUser; import com.mcy.springbootshiro.service.SysRoleService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class IndexController { @Autowired private SysRoleService roleService; @RequestMapping({"/index", "/"}) public String index(){ return "login"; } @RequestMapping(value = "/login") public String login(String username, String password, Model model){ //1.获取subject Subject subject = SecurityUtils.getSubject(); //2.封装用户数据 UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { //3.执行登陆方法 subject.login(token); //登陆成功 return "redirect:/mian"; } catch (UnknownAccountException e) { //e.printStackTrace(); //登陆失败:用户名不存在 model.addAttribute("msg", "用户名不存在"); return "login"; } catch (IncorrectCredentialsException e) { //e.printStackTrace(); //登陆失败:用户名不存在 model.addAttribute("msg", "密码输入有误"); return "login"; } } @RequestMapping(value = "/mian") public String main(){ SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal(); System.out.println(user.getName()); //登陆成功后,输出对应的角色和菜单 for(SysRole role: user.getRoles()){ System.out.println(role.getName()+"=====角色"); for(SysRole roles : role.getChildren()){ System.out.println(roles.getName()+"===="+role.getName()+"角色对应的菜单"); } } return "main"; } @RequestMapping("/logout") public String logout(){ Subject subject = SecurityUtils.getSubject(); if (subject != null) { subject.logout(); } return "redirect:/main"; } @RequestMapping("/unAuth") public String unAuth(){ return "unAuth"; } @RequestMapping("/system") public String system(){ return "system"; } @RequestMapping("/user") public String user(){ return "user"; } }
页面内容都比较简单,主要为一个登陆页面,登陆成功后的页面和几个访问页面。
登陆页面login.html代码以下(测试页面写的比较简单,页面中font标签为登陆失败的提示信息,内容为后台传递过来的):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登陆页面</title> <style type="text/css"> #main{ width: 400px; margin: 100px auto; } input{ border-radius: 5px; width: 200px; height: 20px; } button{ margin-left: 30px; width: 80px; border-radius: 15px; } </style> </head> <body> <div id="main"> <form action="login" method="post"> <font color="red" th:text="${msg}"></font><br><br> 帐号:<input type="text" name="username"><br><br> 密码:<input type="password" name="password"><br><br> <button type="submit">登陆</button> </form> </div> </body> </html>
登陆成功main.html页面(几个页面跳转超连接)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <li><a href="user">登陆便可访问</a></li> <li><a href="system">管理员权限才能访问</a></li> <li><a href="logout">退出登陆</a></li> </body> </html>
管理员权限才能访问的页面system.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 管理员权限,才能访问 </body> </html>
登陆便可访问的页面user.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 登陆后便可访问 </body> </html>
没有权限访问的页面,提示页面unAuth.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p>您暂时没有权限访问该页面</p> </body> </html>
运行项目后访问http://localhost/shiro/index,进入登陆页面,登陆拥有管理员权限的用户。
登陆成功后,能够从编辑器控制台中看出第一步进行了Shrio的认证。以后执行了登陆成功后跳转页面的后台方法,输出了对应的角色和角色对应的菜单。
【注】 此案例中前台菜单没有进行遍历,菜单,权限等功能也没有写对应的增删改查。直接在数据库中添加数据进行测试的。若是想了解菜单遍历,菜单权限等功能的实现,请访问SpringSecurity实现动态菜单加载。其中数据库设计,功能操做都是同样的。
点击“管理员权限才能访问”首先会进行执行受权,根据判断是否有管理员身份,肯定是否能够访问。由于是拥有管理员身份的用户登陆的,因此能够访问,效果以下:
正是system.html页面中的内容。若是没有管理员身份,则显示效果以下:
案例代码下载地址:https://github.com/machaoyin/springboot-shrio
最后有什么不足之处,欢迎你们指出,期待与你的交流。