SpringBoot整合Apache Shiro权限验证框架
比较常见的权限框架有两种,一种是Spring Security,另一种是Apache Shiro,两种框架各有优劣,个人感觉Shiro更容易使用,更加灵活,也更符合RABC规则,而且是java官方更推崇的安全验证框架。下面我将shiro的使用demo分享出来,能力所限,不到之处,请大家指正。
Shiro框架的核心就三个部分Subject、SecurityManager、ShiroRealm,理论内容请自行百度。
1、准备工作
db
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`pid` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
`url` varchar(128) DEFAULT NULL,
PRIMARY KEY (`pid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES ('1', 'add', '');
INSERT INTO `permission` VALUES ('2', 'delete', '');
INSERT INTO `permission` VALUES ('3', 'edit', '');
INSERT INTO `permission` VALUES ('4', 'query', '');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`rid` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
PRIMARY KEY (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'admin');
INSERT INTO `role` VALUES ('2', 'customer');
-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`rid` int(11) DEFAULT NULL,
`pid` int(11) DEFAULT NULL,
KEY `ids_rid` (`rid`),
KEY `ids_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES ('1', '1');
INSERT INTO `role_permission` VALUES ('1', '2');
INSERT INTO `role_permission` VALUES ('1', '3');
INSERT INTO `role_permission` VALUES ('1', '4');
INSERT INTO `role_permission` VALUES ('2', '1');
INSERT INTO `role_permission` VALUES ('2', '4');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL,
`password` varchar(32) NOT NULL,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '123');
INSERT INTO `user` VALUES ('2', 'demo', '123');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`uid` int(11) NOT NULL,
`rid` int(11) NOT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`),
KEY `ids_uid` (`uid`),
KEY `ids_rid` (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '2', '2');
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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.weitian</groupId> <artifactId>shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>shiro</name> <description>Demo project for Apache Shiro</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.17.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-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.25</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> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.properties
##database ## spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8 spring.datasource.username=root spring.datasource.password=root spring.jpa.show-sql=true spring.jpa.database=mysql spring.mvc.view.prefix=/pages/ spring.mvc.view.suffix=.jsp
2、采用jpa作为数据库持久层,将建表的任务交给框架自动完成,只需要在entity中写清楚对应关系即可。三个实体类,对应数据库中三个表(user,role,permission)
User.java
package com.weitian.model; import lombok.Data; import javax.persistence.*; import java.util.List; /** * Created by Administrator on 2018/11/20. */ @Entity @Data public class User { @Id @GeneratedValue private Integer uid; private String username; private String password; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name="user_role",joinColumns = {@JoinColumn(name="uid")},inverseJoinColumns = {@JoinColumn(name="rid")}) private List<Role> roleList; }
Role.java
package com.weitian.model; import lombok.Data; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.ArrayList; import java.util.List; /** * Created by Administrator on 2018/11/20. */ @Entity @Data public class Role { @Id @GeneratedValue private Integer rid; private String name; @ManyToMany(fetch= FetchType.EAGER) @JoinTable(name="role_permission",joinColumns = {@JoinColumn(name="rid")},inverseJoinColumns = {@JoinColumn(name="pid")}) private List<Permission> permissionList=new ArrayList<>( ); @ManyToMany @JoinTable(name="user_role",joinColumns = {@JoinColumn(name="rid")},inverseJoinColumns = {@JoinColumn(name="uid")}) private List<User> userList=new ArrayList<>( ); }
Permission.java
package com.weitian.model; import lombok.Data; import javax.persistence.*; import java.util.ArrayList; import java.util.List; /** * Created by Administrator on 2018/11/20. */ @Entity @Data public class Permission { @Id @GeneratedValue private Integer pid; private String name; private String url; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name="role_permission",joinColumns = {@JoinColumn(name="pid")},inverseJoinColumns = {@JoinColumn(name="rid")}) private List<Role> roleList=new ArrayList<Role>(); }
UserRepository
package com.weitian.repository; import com.weitian.entity.User; import org.springframework.data.jpa.repository.JpaRepository; /** * Created by Administrator on 2018/11/20. */ public interface UserRepository extends JpaRepository<User,Integer> { public User findByUsername(String userName); }
Controller
package com.weitian.controller; import com.weitian.entity.User; import com.weitian.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; 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.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * Created by Administrator on 2018/11/21. */ @Controller public class HomeController { @Autowired private UserService userService; @RequestMapping("/index") public String index(){ return "index"; } @RequestMapping("/admin") @ResponseBody public String admin(){ return "admin success"; } @RequestMapping("/login") public String login(@RequestParam(value = "username",defaultValue = "") String username, @RequestParam(value = "password",defaultValue = "") String password){ UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken( username,password ); Subject subject= SecurityUtils.getSubject(); //subject的login方法会调用自定义的验证器对登录进行验证 try { subject.login( usernamePasswordToken ); //登录成功后,将user放入session User user=userService.findByUserName( username ); subject.getSession().setAttribute( "user",user ); } catch (AuthenticationException e) { e.printStackTrace(); return "login"; } return "index"; } }
好了,至此准备工作已经完成,下面进入shiro框架核心部分
Shiro框架提供了两部分的安全核心,一部分是认证(Authentication),另一部分是验证(Authorization)。两个单词比较相似,认证的是身份信息,即登录登出使用,验证的是权限分配,即授权管理。
这两个单词第一次学习shiro框架时纠结了我好久,一方面是单词本身太相似,另一部分是被一些博客文章误导,最后查了好多源码才搞清楚具体的细节。
这一部分我是这样理解的:
用户来访问网站资源,我们使用shiro框架对其进行访问控制及权限管理,但是shiro框架如何得知这个用户的登录名是否正确,登录成功后又拥有何种的权限呢?所以我们需要获取这部分信息,并告诉shiro,具体做法是:继承AuthorizingRealm类,并重写其中的两个方法:
package com.weitian.auth;
import com.weitian.ResultRnum.ResultEnum;
import com.weitian.entity.Permission;
import com.weitian.entity.Role;
import com.weitian.entity.User;
import com.weitian.exception.ResultException;
import com.weitian.service.UserService;
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 org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* Created by Administrator on 2018/11/21.
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/*
该类用于保存身份认证信息,即登录信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
/*
token中保存了用户登录时的信息,查源码可以看出token.getPrincipal()方法返回用户名
*/
String username=(String)token.getPrincipal();
User user=userService.findByUserName( username );
if(null==user){
throw new ResultException( ResultEnum.USER_NOT_EXISTS);
}
/*
将从数据库中查询得到的用户信息保存在shiro框架的AuthenticationInfo中,准备与token中的用户登录信息进行校验
*/
SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo( user.getUsername(),user.getPassword(),this.getName() );
return authenticationInfo;
}
//权限信息
//该类用户保存登录用户的权限信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/*用户登录后,从session中取出用户权限、角色信息,填充到AuthorizationInfo对象中,进行后续验证*/
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo( );
String username=(String)principals.getPrimaryPrincipal();
User user=userService.findByUserName( username );
List<Role> roleList=user.getRoleList();
for(Role role:roleList){
//保存用户的角色信息
authorizationInfo.addRole( role.getName() );
List<Permission> permissionList=role.getPermissionList();
for(Permission permission:permissionList){
//保存用户的权限信息
authorizationInfo.addStringPermission( permission.getName() );
}
}
return authorizationInfo;
}
}
当用户登录时,我们以何种方式校验用户呢?可以直接将用户的登录信息与数据库的信息进行比较,也可以使用自定义的算法进行校验,Shiro框架提供了不同的校验方式,常用的有SimpleCredentialsMatcher类,HashedCredentialsMatcher(哈希散列校验),
package com.weitian.auth; import com.weitian.ResultRnum.ResultEnum; import com.weitian.exception.ResultException; 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; /** * Created by Administrator on 2018/11/21. */ public class CredentialMatcher extends SimpleCredentialsMatcher{ /* 验证器,在这里可以自定义登录校验规则 */ @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken) token; String loginUsername=usernamePasswordToken.getUsername(); String loginPassword=new String(usernamePasswordToken.getPassword()); String dbUserName=(String)info.getPrincipals().getPrimaryPrincipal(); String dbPassword=(String)info.getCredentials(); if(!(this.equals( loginUsername,dbUserName ) && this.equals( loginPassword,dbPassword ))){ throw new ResultException( ResultEnum.LOGIN_IS_ERROR ); } return true; } }
接下来,将我们上面定义的两个类与shiro框架结合在一起
package com.weitian.config; import com.weitian.auth.CredentialMatcher; import com.weitian.auth.ShiroRealm; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.cache.MemoryConstrainedCacheManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.mgt.SecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import java.util.HashMap; import java.util.Map; /** * Created by Administrator on 2018/11/21. */ @Configuration public class ShiroConfig { /* 4、根据业务需求配置授权过滤器链 */ @Bean public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager){ ShiroFilterFactoryBean factoryBean=new ShiroFilterFactoryBean(); factoryBean.setSecurityManager( securityManager ); //设置登录页面 factoryBean.setLoginUrl( "/login" ); //设置登录成功后的跳转页面 factoryBean.setSuccessUrl( "/index" ); //设置无权访问的跳转页面 factoryBean.setUnauthorizedUrl( "/unauthc" ); //配置校验规则 /* 校验规则为枚举类型,常用的有: (1)anon:匿名过滤器,表示通过了url配置的资源都可以访问,例:“/statics/**=anon”表示statics目录下所有资源都能访问 (2)authc:基于表单的过滤器,表示通过了url配置的资源需要登录验证,否则跳转到登录,例:“/unauthor.jsp=authc”如果用户没有登录访问unauthor.jsp则直接跳转到登录 (3)authcBasic:Basic的身份验证过滤器,表示通过了url配置的资源会提示身份验证,例:“/welcom.jsp=authcBasic”访问welcom.jsp时会弹出身份验证框 (4)perms:权限过滤器,表示访问通过了url配置的资源会检查相应权限,例:“/statics/**=perms["user:add:*,user:modify:*"]“表示访问statics目录下的资源时只有新增和修改的权限 (5)port:端口过滤器,表示会验证通过了url配置的资源的请求的端口号,例:“/port.jsp=port[8088]”访问port.jsp时端口号不是8088会提示错误 (6)rest:restful类型过滤器,表示会对通过了url配置的资源进行restful风格检查,例:“/welcom=rest[user:create]”表示通过restful访问welcom资源时只有新增权限 (7)roles:角色过滤器,表示访问通过了url配置的资源会检查是否拥有该角色,例:“/welcom.jsp=roles[admin]”表示访问welcom.jsp页面时会检查是否拥有admin角色 (8)ssl:ssl过滤器,表示通过了url配置的资源只能通过https协议访问,例:“/welcom.jsp=ssl”表示访问welcom.jsp页面如果请求协议不是https会提示错误 (9)user:用户过滤器,表示可以使用登录验证/记住我的方式访问通过了url配置的资源,例:“/welcom.jsp=user”表示访问welcom.jsp页面可以通过登录验证或使用记住我后访问,否则直接跳转到登录 (10)logout:退出拦截器,表示执行logout方法后,跳转到通过了url配置的资源,例:“/logout.jsp=logout”表示执行了logout方法后直接跳转到logout.jsp页面 过滤器分类: (1)认证过滤器:anon、authcBasic、auchc、user、logout (2)授权过滤器:perms、roles、ssl、rest、port URL模糊规则: (1)“?”:匹配一个字符,如”/admin?”,将匹配“ /admin1”、“/admin2”,但不匹配“/admin” (2)“*”:匹配零个或多个字符串,如“/admin*”,将匹配“ /admin”、“/admin123”,但不匹配“/admin/1” (3)“**”:匹配路径中的零个或多个路径,如“/admin/**”,将匹配“/admin/a”、“/admin/a/b” */ Map<String,String> filterChianDefinitionMap=new HashMap<String,String>( ); filterChianDefinitionMap.put( "/index","authc" ); filterChianDefinitionMap.put( "/login","anon" ); filterChianDefinitionMap.put( "/**","user" ); filterChianDefinitionMap.put( "/druid/**","anon" ); filterChianDefinitionMap.put("/admin", "roles[admin]"); filterChianDefinitionMap.put("/authc/renewable", "perms[Create,Update]"); filterChianDefinitionMap.put("/authc/removable/*", "perms[Delete]"); factoryBean.setFilterChainDefinitionMap( filterChianDefinitionMap ); return factoryBean; } /* 3、将shirorealm对象交给shiro框架的SecurityManager管理 SecurityManager对象,并将shirorealm纳入其中管理 */ @Bean("securityManager") public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm){ DefaultWebSecurityManager webSecurityManager=new DefaultWebSecurityManager( shiroRealm ); return webSecurityManager; } /* 2、告诉shirorealm,我们的校验规则 生成ShiroRealm对象,并为该对象设置自定义的校验规则 */ @Bean("shiroRealm") public ShiroRealm shiroRealm(@Qualifier("credentialMatcher") CredentialsMatcher credentialsMatcher){ ShiroRealm shiroRealm=new ShiroRealm(); shiroRealm.setCredentialsMatcher( credentialsMatcher ); //使用shiro缓存管理 shiroRealm.setCacheManager( new MemoryConstrainedCacheManager() ); return shiroRealm; } /* 1、由spring生成我们自己的校验规则对象 */ @Bean("credentialMatcher") public CredentialMatcher credentialMatcher(){ CredentialMatcher credentialMatcher=new CredentialMatcher(); return credentialMatcher; } }
这样,我们对在Springboot下如何使用Shiro的介绍就告一段落,有希望看到后续更加深入文章的小伙伴欢迎踊跃马克。另外,大部分代码我已经在文章中提供如果需要源码的小伙伴请自行下载
https://github.com/phoenixyouda/shiro.git