Springboot 整合 Shiro 轻量级权限框架,从数据库设计开始带你快速上手 shiro
sys_permissions AS per,
role_per,
user_role
WHERE user.userName='adminHong'
AND user.userId=user_role.userId
AND user_role.roleId=role.roleId
AND role_per.roleId=role.roleId
AND role_per.perId=per.perId
查询出来的结果是:
根据用户登录帐号查询出这个帐号的角色身份,权限,结果非常清晰。
再查下另一个帐号,也是非常清晰:
ok,这么啰嗦地终于从数据库的设计 以及 模拟帐号注册,创建角色,创建权限,给帐号分批角色,给角色分批权限生成的数据
的层面场景,大致给这套系统呈现了个漂浮的使用场景。
代码实现
接下来就是看咱们怎么结合这个数据库去实现了。
先来看看我们的最后项目结构(里面的 controller 就是模拟的各个功能模块,日志功能,登录功能,导出功能):
创建一个 springboot 项目,
然后在 pom.xml 里,加入我们需要使用到的 jar 包:
<?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.6.RELEASE</version>
</parent>
<groupId>com.jc</groupId>
<artifactId>shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>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-web</artifactId>
</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>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</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>
然后是 application.yml 文件:
server:
port: 8077
spring:
datasource:
druid:
username: root
password: root
url: jdbc:mysql://localhost:3306/my_system?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
接下来是对照数据库表结构,创建三个 pojo:
SysUser.java
import lombok.Data;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@Data
public class SysUser {
private Integer userId;
private String userName;
private String password;
private String userRemarks;
}
SysRole.class
import lombok.Data;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@Data
public class SysRole {
private String roleId;
private String roleName;
private String roleRemarks;
}
SysPermissions.java
import lombok.Data;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@Data
public class SysPermissions {
private Integer perId;
private String permissionsName;
private String perRemarks;
}
实体类创建完毕,先不用管那些操作表的增删改查。
核心使用环节, 创建 ShiroConfig.java :
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.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@Configuration
public class ShiroConfig {
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
//将自己的验证方式加入容器
@Bean
public UserRealm myShiroRealm() {
UserRealm userRealm = new UserRealm();
return userRealm;
}
//权限管理,配置主要是 Realm 的管理认证
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
//对 url 的过滤筛选
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//成功登录后跳转的 url
//shiroFilterFactoryBean.setSuccessUrl("/xxxx");
//错误页面,认证不通过跳转
// shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
创建自定义的权限审核处理类,UserRealm.java(涉及到数据库的查询文章后面有写):
import com.jc.shiro.pojo.SysUser;
import com.jc.shiro.service.LoginService;
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;
import java.util.Map;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
public class UserRealm extends AuthorizingRealm {
@Autowired
private LoginService loginService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String userName = (String) principalCollection.getPrimaryPrincipal();
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<Map<String, Object>> powerList = loginService.getUserPower(userName);
System.out.println(powerList.toString());
for (Map<String, Object> powerMap : powerList) {
//添加角色
simpleAuthorizationInfo.addRole(String.valueOf(powerMap.get("roleName")));
//添加权限
simpleAuthorizationInfo.addStringPermission(String.valueOf(powerMap.get("permissionsName")));
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//加这一步的目的是在 Post 请求的时候会先进认证,然后在到请求
if (authenticationToken.getPrincipal() == null) {
return null;
}
//获取用户信息
String userName = authenticationToken.getPrincipal().toString();
//根据用户名去数据库查询用户信息
SysUser sysUser = loginService.queryUser(userName);
if (sysUser == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证 authenticationToken 和 simpleAuthenticationInfo 的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, sysUser.getPassword().toString(), getName());
return simpleAuthenticationInfo;
}
}
}
然后是我们的登录接口,这里融合自己项目的帐号加密方法,LoginController.java:
import com.jc.shiro.service.LoginService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@Controller
public class LoginController {
@Autowired
LoginService loginService;
@ResponseBody
@GetMapping("/login")
public String login(@RequestParam("userName") String userName, @RequestParam("password") String password) {
//添加用户认证信息
Subject subject = SecurityUtils.getSubject();
//自己系统的密码加密方式 ,这里简单示例一下 MD5
String md5Password = DigestUtils.md5DigestAsHex(password.getBytes());
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName, md5Password);
try {
//进行验证,AuthenticationException 可以 catch 到,但是 AuthorizationException 因为我们使用注解方式,是 catch 不到的,
所以后面使用全局异常捕抓去获取
subject.login(usernamePasswordToken);
} catch (AuthenticationException e) {
e.printStackTrace();
return "账号或密码错误!";
} catch (AuthorizationException e) {
e.printStackTrace();
return "没有权限";
}
return "login success";
}
}
其中用到了一个根据用户登录帐号去查询对应的角色权限信息,也就是文章开头我们设计的查询。
mapper 层:
LoginMapper.java
import com.jc.shiro.pojo.SysUser;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Map;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@Mapper
public interface LoginMapper {
SysUser queryUser(String userName );
List<Map<String,Object>> getUserPower(String userName);
}
loginMapper.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="com.jc.shiro.mapper.LoginMapper">
<select id="queryUser" resultType="com.jc.shiro.pojo.SysUser" parameterType="String">
SELECT *
FROM sys_user
WHERE userName=#{userName}
</select>
<select id="getUserPower" resultType="java.util.HashMap" parameterType="String">
SELECT user.userId ,user.userName,role.roleName,role.roleId,per.permissionsName ,per.perId,per.perRemarks
FROM sys_user AS user,
sys_role AS role,
sys_permissions AS per,
role_per,
user_role
WHERE user.userName=#{userName}
AND user.userId=user_role.userId
AND user_role.roleId=role.roleId
AND role_per.roleId=role.roleId
AND role_per.perId=per.perId
</select>
</mapper>
然后是 service 层:
LoginService.java
import com.jc.shiro.pojo.SysUser;
import java.util.List;
import java.util.Map;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
public interface LoginService {
SysUser queryUser(String userName );
List<Map<String,Object>> getUserPower(String userName );
}
LoginServiceImpl.java
import com.jc.shiro.mapper.LoginMapper;
import com.jc.shiro.pojo.SysUser;
import com.jc.shiro.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
LoginMapper loginMapper;
@Override
public SysUser queryUser(String userName) {
return loginMapper.queryUser(userName);
}
@Override
public List<Map<String, Object>> getUserPower(String userName) {
return loginMapper.getUserPower(userName);
}
}
最后再补上一个异常全局控制器,MyExceptionHandler.java:
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@ControllerAdvice
@Slf4j
public class MyExceptionHandler {
@ExceptionHandler
@ResponseBody
public String ErrorHandler(AuthorizationException e) {
log.error("权限校验失败!", e);
return "您暂时没有权限,请联系管理员!";
}
}
到这里,已经整合完毕,接下来是我们的使用,后端的校验我们采取 shiro 提供的注解的方式去使用:
@RequiresRoles("xxx")
@RequiresPermissions("xxx")
我们简单模拟导出数据功能模块,ExportController.java:
暂时只提供了一个导出接口,而这个导出接口需要用户拥有 admin 角色 以及?exportUserInfo 权限,也就是代码里注解的参数
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@RestController
public class ExportController {
@RequiresRoles("admin")
@RequiresPermissions("exportUserInfo")
@ResponseBody
@RequestMapping("/export")
public String export() {
return "u can export !";
}
}
再来模拟日志功能模块,LogController.java:
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
@Author : JCccc
@CreateTime : 2020/4/24
@Description :
**/
@RestController
public class LogController {
//注解验角色和权限
@RequiresRoles("common")
@ResponseBody
@RequestMapping("/querySystemLog")
public String queryLog() {
return "u can queryLog !";
}
}
测试效果
好,接下来我们从登录开始,测试下整体的效果:
评论