Apache Shiro 是一个强大并且易于使用的java安全框架,可以用与身份验证、授权、加密和会话管理。同样的框架还有spring security,spring security有很好的平台支持,和活跃的社区氛围,并且对spring完美兼容,但是使用难度上,远远超过shiro。
shiro上手快 ,控制粒度可糙可细 ,自由度高,可以独立运行。
类名 | 说明 |
---|---|
Subject | 可以理解为与shiro打交道的对象,该对象封装了一些对方的信息,shiro可以通过subject拿到这些信息 |
SecurityManager | Shiro的总经理,通过指使Authorizer和Authenticator等对subject进行授权和身份验证等工作 |
Realm | 管理着一些如用户、角色、权限等重要信息,Shiro中所需的这些重要信息都是从Realm这里获取的,Realm本质上就是一个重要信息的数据源 |
Authenticator | 认证器,负责Subject的认证操作,认证过程就是根据Subject提供的信息通过Realm查询到相关信息,然后做对比,支持扩展 |
Authorizer | 授权器,控制着Subject对服务资源的访问权限 |
SessionManager | 用于管理Session,这个Session可以是web的也可以不是web的 |
SessionDao | 把Session的 CRUD和存储介质联系起来的工具,存储介质可以是数据库,也可以是缓存,比如把session放到redis里面 |
CacheManager | 缓存控制器,Realm管理的数据(用户、角色、权限)可以放到缓存里由CacheManager管理,提高认证授权等的速度 |
Cryptography | 加密组件,Shiro提供了很多加解密算法的组件 |
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)代表Shiro应用安全的四大基石。
spring boot + shiro + swagger + mybatis
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.0.RELEASEversion>
<relativePath/>
parent>
<groupId>com.felton.springbootgroupId>
<artifactId>shrioartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>shrioname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<mysql.version>5.1.46mysql.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
<dependency>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-coreartifactId>
<version>1.3.5version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.2version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.3.5version>
<configuration>
<configurationFile>${basedir}/src/main/resources/mybatis-generator.xmlconfigurationFile>
<overwrite>trueoverwrite>
<verbose>trueverbose>
configuration>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
dependencies>
plugin>
plugins>
build>
project>
主要是数据库配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/shrio?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
#mybatis
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
数据库表结构,标准的RBAC用户角色权限结构。
/*
Navicat Premium Data Transfer
Source Server : 本地
Source Server Type : MySQL
Source Server Version : 50729
Source Host : localhost:3306
Source Schema : shrio
Target Server Type : MySQL
Target Server Version : 50729
File Encoding : 65001
Date: 16/06/2020 15:33:00
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`permission_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`permission_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称',
`resource_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源类型,[menu|button]',
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源路径',
`permission` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view',
`parent_id` int(11) NULL DEFAULT NULL COMMENT '父编号',
`available` int(255) NULL DEFAULT NULL COMMENT '是否可用,如果不可用将不会添加给用户',
PRIMARY KEY (`permission_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES (1, '用户管理', 'menu', 'user/userList', 'user:view', 0, 1);
INSERT INTO `permission` VALUES (2, '用户添加', 'button', 'user/userAdd', 'user:add', 1, 1);
INSERT INTO `permission` VALUES (3, '用户删除', 'button', 'user/userDel', 'user:del', 1, 1);
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色标识程序中判断使用,如\"admin\",这个是唯一的:',
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述,UI界面显示使用',
`available` int(255) NULL DEFAULT NULL COMMENT '是否可用,如果不可用将不会添加给用户',
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'admin', '管理员', 1);
-- ----------------------------
-- Table structure for role_permission_relation
-- ----------------------------
DROP TABLE IF EXISTS `role_permission_relation`;
CREATE TABLE `role_permission_relation` (
`permission_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`permission_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_permission_relation
-- ----------------------------
INSERT INTO `role_permission_relation` VALUES (1, 1);
INSERT INTO `role_permission_relation` VALUES (2, 1);
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '登录用户名',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称(昵称或者真实姓名,根据实际情况定义)',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`state` int(255) NULL DEFAULT NULL COMMENT '用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`expired_date` datetime(0) NULL DEFAULT NULL COMMENT '过期日期',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', '管理员', '597042ee35c3a0a937c9ef4afff842f2', 1, '2020-06-10 16:17:04', '[email protected]', '2020-06-17 16:17:16');
-- ----------------------------
-- Table structure for user_role_relation
-- ----------------------------
DROP TABLE IF EXISTS `user_role_relation`;
CREATE TABLE `user_role_relation` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role_relation
-- ----------------------------
INSERT INTO `user_role_relation` VALUES (1, 1);
SET FOREIGN_KEY_CHECKS = 1;
这里使用了mbg自动生成model、dao、mapper。
添加一个MyShiroRealm类,并继承AuthorizingRealm,并且需要实现两个方法。
doGetAuthenticationInfo:实现用户认证,通过服务加载用户信息并构造认证对象返回。
doGetAuthorizationInfo:实现权限认证,通过服务加载用户角色和权限信息设置进去。
package com.felton.springboot.shrio.config;
import com.felton.springboot.shrio.entity.Permission;
import com.felton.springboot.shrio.entity.Role;
import com.felton.springboot.shrio.entity.User;
import com.felton.springboot.shrio.service.PermissionService;
import com.felton.springboot.shrio.service.RoleService;
import com.felton.springboot.shrio.service.UserService;
import jdk.nashorn.internal.parser.Token;
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 org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* 类 名:com.felton.springboot.shrio.config.MyShrioRealm
* 类描述:todo
* 创建人:liurui
* 创建时间:2020/6/10 11:23
* 修改人:
* 修改时间:
* 修改备注:
*
* @author liurui
* @version 1.0
*/
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleservice;
@Autowired
private PermissionService permissionService;
/**
* 权限信息
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principalCollection.getPrimaryPrincipal();
List<Role> roleList = roleservice.getRoleListByUserName(user.getUserName());
List<Permission> permissionList = null;
if (roleList.size() > 0) {
//添加角色
for (Role role : roleList) {
authorizationInfo.addRole(role.getRole());
permissionList = permissionService.getPermissionListByRoleId(role.getRoleId());
for (Permission permission : permissionList) {
//添加权限
authorizationInfo.addStringPermission(permission.getPermission());
}
}
}
return authorizationInfo;
}
/**
* 身份认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("【MyShiroRealm】身份认证");
String userName = (String) authenticationToken.getPrincipal();
User user = userService.findByUserName(userName);
if (user == null) {
return null;
}
if (user.getState() == 0) {
// 用户被管理员锁定抛出异常
throw new AuthenticationException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user,
user.getPassword(),
getName()
);
return authenticationInfo;
}
}
这里使用的是spring boot,所以抛弃传统的xml配置,改为编写配置类ShiroConfig
。在项目启动的时候,实现方法会给注入到spring容器中。
package com.felton.springboot.shrio.config;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 类 名:com.felton.springboot.shrio.config.ShiroConfig
* 类描述:todo
* 创建人:liurui
* 创建时间:2020/6/10 11:24
* 修改人:
* 修改时间:
* 修改备注:
*
* @author liurui
* @version 1.0
*/
@Configuration
public class ShiroConfig {
/**
* 将自己的验证方式加入容器
* @return
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 权限管理,配置主要是realm的管理认证
* @return
*/
@Bean
DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myShiroRealm());
return manager;
}
/**
* 凭证匹配器(密码校验交给Shiro的SimpleAuthenticationInfo进行处理)
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 加密方式
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 加密两次
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager());
Map<String, String> filterMap = new HashMap<>();
// 登出
filterMap.put("/logout", "logout");
// 对所有用户认证
filterMap.put("/**", "authc");
filterMap.put("/swagger**/**", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/v2/**", "anon");
// 登录
bean.setLoginUrl("/login");
// 首页
bean.setSuccessUrl("/index");
// 未授权页面,认证不通过跳转
bean.setUnauthorizedUrl("/403");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
/**
* 开启shiro aop 注解支持
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
@Bean(name = "simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
System.out.println("错误");
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError");
mappings.setProperty("UnauthorizedException", "/403");
resolver.setExceptionMappings(mappings);
resolver.setDefaultErrorView("error");
resolver.setExceptionAttribute("exception");
return resolver;
}
}
package com.felton.springboot.shrio.service.impl;
import com.felton.springboot.shrio.model.LoginResult;
import com.felton.springboot.shrio.service.LoginService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;
/**
* 类 名:com.felton.springboot.shrio.service.impl.LoginServiceImpl
* 类描述:todo
* 创建人:liurui
* 创建时间:2020/6/10 11:08
* 修改人:
* 修改时间:
* 修改备注:
*
* @author liurui
* @version 1.0
*/
@Service
public class LoginServiceImpl implements LoginService {
@Override
public LoginResult login(String userName, String password) {
LoginResult result = new LoginResult();
if (userName == null || userName.isEmpty()) {
result.setLogin(false);
result.setResult("用户名为空");
return result;
}
String msg = "";
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
try {
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
Session session = currentUser.getSession();
// 设置会话session
session.setAttribute("userName", userName);
result.setLogin(true);
return result;
} catch (Exception e) {
e.printStackTrace();
}
result.setLogin(false);
result.setResult("");
return result;
}
@Override
public void logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
}
}
大致的代码都在这里,如果需要完整的项目地址可以到这里
代码量也不算很多,但是要真正的理解shiro的执行过程还需要对其源码进行分析。简单的走了一下代码的执行过程。
用户的每一次请求都会执行自定义的AuthorizingRealm
类,如果没有session记录的会执行身份认证doGetAuthenticationInfo
,如果已经登录并且有session会话记录,则直接执行权限认证doGetAuthorizationInfo
执行过程
如果在数据库中匹配到指定的权限,返回true值,由于每次访问接口都会触发doGetAuthorizationInfo
方法,从而会经常性的查找数据库,如果用户数量大,查找权限多,会导致接口响应慢,这里可以改造使用redis缓存数据库来缓存权限信息。
如果用户无权限,isPermitted
会返回false,ModularRealmAuthorizer
中会抛出无权限异常UnauthorizedException
,而在ShiroConfig.java中已配置UnauthorizedException
的异常捕获,并重定向到/403
接口中。
推荐一篇文章,里边说了很明白,地址
主要用到的是@RequiresPermissions,注解中的值分为三种形式
:
来分割字符串的内容,例如:@RequiresPermissions(“user:add”)