SpringBoot整合Apache Shiro权限验证框架

时间:2023-03-09 04:55:44
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