在上文 Apache Shiro权限框架理论介绍 中,咱们介绍了Apache Shiro的基础理论知识。本文咱们将在 SpringBoot 中集成Apache Shiro,完成一些简单的Case开发。html
Apache Shiro和Spring Security不一样,它没有自带的登陆页面和基于内存的权限验证。因此咱们将使用jsp去编写简单的登陆页面,使用Mybatis链接MySQL数据库进行用户及其权限和角色信息的存取。java
首先在IDEA中,建立一个Spring Boot工程:mysql
选择须要的模块:web
项目建立完成后,补充相应的依赖,pom.xml文件中配置的完整依赖项以下:正则表达式
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- apache shiro 依赖 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.3</version> </dependency> <!-- alibaba的druid数据库链接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version> </dependency> <!-- apache 工具包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> <!-- spring 工具包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.0.7.RELEASE</version> </dependency> <!-- jsp 依赖 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> </dependencies>
注:在本文中,不会赘述SpringBoot集成Mybatis的配置,若对此不熟悉的话,能够参考我另外一篇文章:SpringBoot2.x整合MyBatisspring
以上也提到了咱们须要在数据库中进行用户及其权限和角色信息的存取,而且咱们将按照RBAC模型完成文中Case的开发,因此首先须要建立数据库表格及向表格插入一些数据。具体的sql语句以下:sql
-- 权限表 -- create table permission ( pid int (11) not null auto_increment, name varchar (255) not null default '', url varchar (255) default '', primary key (pid) ) engine = InnoDB default charset = utf8; insert into permission values ('1','add',''); insert into permission values ('2','delete',''); insert into permission values ('3','edit',''); insert into permission values ('4','query',''); -- 用户表 -- create table user ( uid int (11) not null auto_increment, username varchar (255) not null default '', password varchar (255) default '', primary key (uid) ) engine = InnoDB default charset = utf8; insert into user values ('1','admin','123'); insert into user values ('2','user','123'); -- 角色表 -- create table role ( rid int (11) not null auto_increment, rname varchar (255) not null default '', primary key (rid) ) engine = InnoDB default charset = utf8; insert into role values ('1','admin'); insert into role values ('2','customer'); -- 权限、角色关系表 -- create table permission_role ( rid int (11) not null, pid int (11) not null, key idx_rid(rid), key idx_pid(pid) ) engine = InnoDB default charset = utf8; insert into permission_role values ('1','1'); insert into permission_role values ('1','2'); insert into permission_role values ('1','3'); insert into permission_role values ('1','4'); insert into permission_role values ('2','1'); insert into permission_role values ('2','4'); -- 用户、角色关系表 -- create table user_role ( uid int (11) not null, rid int (11) not null, key idx_uid(uid), key idx_rid(rid) ) engine = InnoDB default charset = utf8; insert into user_role values (1,1); insert into user_role values (2,2);
建立与表格所对应的pojo类。以下:数据库
@Data @AllArgsConstructor @NoArgsConstructor public class Permission { private Integer pid; private String name; private String url; } @Data @AllArgsConstructor @NoArgsConstructor public class Role { private Integer rid; private String rname; private Set<Permission> permissions = new HashSet<>(); private Set<User> users = new HashSet<>(); } @Data @AllArgsConstructor @NoArgsConstructor public class User { private Integer uid; private String username; private String password; private Set<Role> roles = new HashSet<>(); }
而后建立dao层的mapper接口:apache
public interface UserMapper { /** * 根据用户名查找用户 * * @param username 用户名 * @return user */ User findByUserName(@Param("username") String username); }
以及编写与之对应的xml文件:缓存
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.zero.example.shiro.mapper.UserMapper"> <resultMap id="userMap" type="org.zero.example.shiro.model.User"> <id property="uid" column="uid"/> <result property="username" column="username"/> <result property="password" column="password"/> <collection property="roles" ofType="org.zero.example.shiro.model.Role"> <id property="rid" column="rid"/> <result property="rname" column="rname"/> <collection property="permissions" ofType="org.zero.example.shiro.model.Permission"> <id property="pid" column="pid"/> <result property="name" column="name"/> <result property="url" column="url"/> </collection> </collection> </resultMap> <select id="findByUserName" parameterType="string" resultMap="userMap"> select u.*, r.*, p.* from user u inner join user_role ur on ur.uid = u.uid inner join role r on r.rid = ur.rid inner join permission_role pr on pr.rid = r.rid inner join permission p on pr.pid = p.pid where u.username = #{username} </select> </mapper>
接着是service层接口:
public interface UserService { /** * 根据用户名查找用户 * * @param username 用户名 * @return user */ User findByUserName(String username); }
编写实现类来实现UserService接口:
@Service("userService") public class UserServiceImpl implements UserService { private final UserMapper userMapper; @Autowired public UserServiceImpl(UserMapper userMapper) { this.userMapper = userMapper; } @Override public User findByUserName(String username) { return userMapper.findByUserName(username); } }
到此为止,咱们就完成了项目基本结构的搭建,接下来咱们就能够开始Case的开发了。
咱们来基于Apache Shiro实现一个自定义的认证、受权及密码匹配规则。首先是建立咱们自定义的Realm,在Realm实现受权及认证登陆,代码以下:
package org.zero.example.shiro.realm; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.util.CollectionUtils; import org.zero.example.shiro.model.Permission; import org.zero.example.shiro.model.Role; import org.zero.example.shiro.model.User; import org.zero.example.shiro.service.UserService; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * @program: shiro * @description: 自定义Realm * @author: 01 * @create: 2018-09-08 16:13 **/ public class AuthRealm extends AuthorizingRealm { @Autowired private UserService userService; // 受权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 从session中拿出用户对象 User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next(); List<String> permissionList = new ArrayList<>(); Set<String> roleNameSet = new HashSet<>(); // 获取用户的角色集 Set<Role> roleSet = user.getRoles(); if (!CollectionUtils.isEmpty(roleSet)) { for (Role role : roleSet) { // 添加角色名称 roleNameSet.add(role.getRname()); // 获取角色的权限集 Set<Permission> permissionSet = role.getPermissions(); if (!CollectionUtils.isEmpty(permissionSet)) { for (Permission permission : permissionSet) { // 添加权限名称 permissionList.add(permission.getName()); } } } } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermissions(permissionList); info.setRoles(roleNameSet); return info; } // 认证登陆 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; // 获取登陆的用户名 String userName = usernamePasswordToken.getUsername(); // 从数据库中查询用户 User user = userService.findByUserName(userName); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName()); } }
由于登陆时用户输入的密码须要与数据库里的密码进行对比,因此咱们还能够自定义一个密码校验规则。代码以下:
package org.zero.example.shiro.matcher; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; /** * @program: shiro * @description: 自定义密码校验规则 * @author: 01 * @create: 2018-09-08 16:30 **/ public class CredentialMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; String password = new String(usernamePasswordToken.getPassword()); String dbPassword = (String) info.getCredentials(); return this.equals(password, dbPassword); } }
最后是新建一个配置类来注入shiro相关的配置,代码以下:
package org.zero.example.shiro.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.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.zero.example.shiro.matcher.CredentialMatcher; import org.zero.example.shiro.realm.AuthRealm; import java.util.LinkedHashMap; import java.util.Map; /** * @program: shiro * @description: shiro配置类 * @author: 01 * @create: 2018-09-08 16:34 **/ @Configuration public class ShiroConfiguration { @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); // 登陆的url bean.setLoginUrl("/login"); // 登陆成功后跳转的url bean.setSuccessUrl("/index"); // 权限拒绝时跳转的url bean.setUnauthorizedUrl("/unauthorize"); // 定义请求拦截规则,key是正则表达式用于匹配访问的路径,value则用于指定使用什么拦截器进行拦截 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 拦截index接口,authc表示须要认证才能访问 filterChainDefinitionMap.put("/index", "authc"); // anon表示不拦截 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/loginUser", "anon"); // 指定admin接口只容许admin角色的用户访问 filterChainDefinitionMap.put("/admin", "roles[admin]"); // 用户在登陆后能够访问全部的接口 filterChainDefinitionMap.put("/**", "user"); bean.setFilterChainDefinitionMap(filterChainDefinitionMap); return bean; } @Bean("securityManager") public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) { // 设置自定义的SecurityManager DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(authRealm); return manager; } @Bean("authRealm") public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) { // 设置自定义的Realm AuthRealm authRealm = new AuthRealm(); authRealm.setCredentialsMatcher(matcher); return authRealm; } @Bean("credentialMatcher") public CredentialMatcher credentialMatcher() { // 设置自定义密码校验规则 return new CredentialMatcher(); } // =========== spring 与 shiro 关联的相关配置 ============ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) { // 设置spring在对shiro进行处理的时候,使用的SecurityManager为咱们自定义的SecurityManager AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { // 设置代理类 DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } }
新建一个 DemoController,用于提供外部访问的接口。代码以下:
package org.zero.example.shiro.controller; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.zero.example.shiro.model.User; import javax.servlet.http.HttpSession; /** * @program: shiro * @description: shiro demo * @author: 01 * @create: 2018-09-08 18:01 **/ @Slf4j @Controller public class DemoController { @RequestMapping("/login") public String login() { return "login"; } @RequestMapping("/index") public String index() { return "index"; } @RequestMapping("/logout") public String logout() { Subject subject = SecurityUtils.getSubject(); if (subject != null) { subject.logout(); } return "login"; } @RequestMapping("/admin") @ResponseBody public String admin() { return "success admin"; } @RequestMapping("/unauthorize") public String unauthorize() { return "unauthorize"; } @RequestMapping("/loginUser") public String loginUser(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); User user = (User) subject.getPrincipal(); session.setAttribute("user", user); return "index"; } catch (Exception e) { log.error("验证不经过: {}", e.getMessage()); return "login"; } } }
在配置文件中,配置jsp文件所在的路径:
spring: mvc: view: prefix: /pages/ suffix: .jsp
因为须要跳转jsp,因此还需配置项目的web resource路径:
配置好后会生成一个webapp目录,在该目录下建立pages目录,并新建jsp文件。其中login.jsp文件内容以下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Login</title> </head> <body> <h1>欢迎登陆</h1> <form action="/loginUser" method="post"> <input type="text" name="username"/><br/> <input type="text" name="password"/><br/> <input type="submit" value="登陆"/> </form> </body> </html>
index.jsp文件内容以下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Home</title> </head> <body> <h1>欢迎登陆, ${user.username}</h1> </body> </html>
unauthorize.jsp文件内容以下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Unauthorize</title> </head> <body> <h2>无权限访问!</h2> </body> </html>
启动项目,在没有登陆的状况下访问index接口,会跳转到登陆页面上:
用户成功后,就会跳转到index页面上:
若使用user用户访问admin接口,则会跳转到权限拒绝页面上,这符合咱们定义的规则:
只有admin用户才能够访问全部接口:
若是咱们要实现某个接口须要某个权限才能访问的话,能够在ShiroConfiguration类的shiroFilter方法中,关于定义请求拦截规则那一块去配置。例如我但愿edit只能由拥有edit权限的用户才能访问,则添加以下代码便可:
// 设置用户须要拥有edit权限才能够访问edit接口 filterChainDefinitionMap.put("/edit", "perms[edit]");
若是须要开启权限缓存的话,能够在配置 AuthRealm 的时候进行定义。例如我这里使用Shiro自带的权限缓存,以下:
@Bean("authRealm") public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) { // 设置自定义的Realm AuthRealm authRealm = new AuthRealm(); authRealm.setCredentialsMatcher(matcher); // 设置缓存 authRealm.setCacheManager(new MemoryConstrainedCacheManager()); return authRealm; }
优势:
缺点: