目录
shiro
Shiro的功能包括以下几点:
- Authentication: 认证 判断是否为合法用户 有时被称为 "登录”,这是证明用户合法
- Authorization: 授权 验证某个已认证的用户是否拥有某个权限
- Session Management: session会话管理
- Cryptography: 安全数据加解密
- Web Support: 支持 web
- Caching: 支持缓存
- Concurrency: 支持并发
- Testing: 支持测试
- "Run As": 用其他用户登录
- "Remember Me": 记住我
shiro概念:
中枢管理中心:DefaultWebSecurityManager类,我们所自定义的方法对象都会添加到这个管理中心去,会由它来调控我们所定义的方法从而代替系统自带的默认认证方法。其次是Realm对象,它翻译过来叫做"域",是由它充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。我们所自定义的方法需要写在继承了AuthorizingRealm的类中,才能添加到管理中心去。最后是Subject,通常我们会将Subject对象理解为一个用户,同样它也有可能是一个三方程序,它是一个抽象的概念,可以理解为任何与系统交互的“东西”都是Subject。
名词解释:
Realm : 域,提供数据的组件
- getAuthencationInfo 入参 Token 出参 Info
- Info 有 Principal + Credentials
AuthenticationToken:认证令牌,认证入参
AuthenticationInfo: 认证信息,用户信息
AuthorizationInfo: 授权信息
Principal: 经过认证的用户
Credentials: 凭证=密码
RBAC
role based access control 基于角色的访问控制
授权基于角色实现
role 角色
permission 权限
user 用户
都是多对多关系,所以需要建立关联表
一、 实现步骤:
1.1、创建数据库表:
上图中的五个表,并插入数据
create table edu_user(
user_id int auto_increment primary key ,
username varchar(256) not null comment '用户名',
password varchar(256) not null comment '密码',
status varchar(32) not null comment '状态 NORMAL | LOCKED | CANCELD'
)comment '用户表';
create table edu_user_role(
user_role_id int auto_increment primary key ,
user_id int not null comment '用户id',
role_id int not null comment '角色id'
)comment '用户角色关联表';
create table edu_role(
role_id int auto_increment primary key ,
role_name varchar(32) not null comment '角色'
)comment '角色表';
create table edu_role_perm(
role_perm_id int auto_increment primary key ,
role_id int not null comment '角色id',
perm_id int not null comment '权限id'
)comment '角色权限关联表';
create table edu_perm(
perm_id int auto_increment primary key ,
perm_name varchar(32) not null comment '权限名'
)comment '权限表';
#插入用户数据
insert into edu_user values
(default,'zhangsan','wohenshuai','NORMAL'),
(default,'wangwu','wohenchou','NORMAL'),
(default,'lisi','wohenku','NORMAL'),
(default,'maliu','wohenliu','NORMAL'),
(default,'tianqi','wohenniu','NORMAL');
#插入角色数据
insert into edu_role values
(default,'headmaster'),
(default,'teacher');
#插入用户角色关联数据
insert into edu_user_role values
(default,1,1),
(default,2,2),
(default,3,2),
(default,4,2),
(default,5,2);
#插入权限数据
insert into edu_perm values
(default,'fireEmployee'),
(default,'raiseSalary'),
(default,'recess'),
(default,'teach'),
(default,'fireStudent'),
(default,'manageStudents');
#插入角色权限关联数据
insert into edu_role_perm values
(default,1,1),
(default,1,2),
(default,1,3),
(default,2,4),
(default,2,5),
(default,2,6);
1.2、创建springboot项目
1.3、导入pom依赖
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.1</version>
</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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--集成mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--数据库-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.5</version>
</dependency>
<!--日志-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.1</version>
<scope>test</scope>
</dependency>
<!--日志打印-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
1.4、改配置
配置文件application.yml
#shiro默认跳转登录页面设置
shiro:
loginUrl:
/login.html
#设置端口号
server:
port:
8081
spring:
#数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/edu?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
password: 12345678
username: root
#日期格式转换
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mvc:
format:
date: yyyy-MM-dd HH:mm:ss
#mybatis配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
map-underscore-to-camel-case: true
type-aliases-package: com.woniuxy.shiroboot.model
mapper-locations: classpath:mappers/**/*.xml
#日志配置
logging:
level:
com.woniuxy.boo1: debug
1.5、 核心配置类
import com.woniuxy.shiroboot.components.ShiroRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SecurityConfig {
/**
* 自定义realm 提供数据源的,用来存储数据库获取的对象
* @return
*/
@Bean
public Realm shiroRealm(){
return new ShiroRealm();
}
/**
* shiro的过滤器链
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
//shiro默认的shiro过滤器链定义
DefaultShiroFilterChainDefinition sfcd = new DefaultShiroFilterChainDefinition();
/*定义 某个路径 使用哪个过滤器处理
*警告:过滤器定义有顺序
*过滤器列表参考:DefaultFilter
*/
//anon表示直接放过
sfcd.addPathDefinition("/","anon");
sfcd.addPathDefinition("/login","anon");
sfcd.addPathDefinition("/css/**","anon");
sfcd.addPathDefinition("/js/**","anon");
sfcd.addPathDefinition("/images/**","anon");
sfcd.addPathDefinition("/fonts/**","anon");
sfcd.addPathDefinition("/html/**","anon");
//登出
sfcd.addPathDefinition("/logout","logout");
//其它所有都需要认证
sfcd.addPathDefinition("/**","user");
return sfcd;
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
1.6、创建Realm域对象
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.woniuxy.shiroboot.dao.PermMapper;
import com.woniuxy.shiroboot.dao.RoleMapper;
import com.woniuxy.shiroboot.dao.UserMapper;
import com.woniuxy.shiroboot.model.Role;
import com.woniuxy.shiroboot.model.User;
import lombok.extern.slf4j.Slf4j;
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 javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Resource
private UserMapper userMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private PermMapper permMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User user = (User) principal.getPrimaryPrincipal();
//查找该用户所有的角色
List<Role> roleList = roleMapper.selectByUserId(user.getUserId());
Set<String> strRoles = roleList.stream().map(r -> r.getRoleName()).collect(Collectors.toSet());
List<String> permissions = new ArrayList<>();
if (roleList.size()>0){
//查询该用户所有的权限
permissions = permMapper.selectPermInRoleIds(roleList);
}
SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
authzInfo.setStringPermissions(new HashSet<>(permissions));
authzInfo.setRoles(strRoles);
return authzInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("执行ShiroRealm#doGetAuthenticationInfo{}",token);
//从被shiro封装成的token中取出我们传入的username
Object username = token.getPrincipal();
//根据用户名查询用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
User user = userMapper.selectOne(wrapper);
//返回一个新封装的认证实体,传入的是用户名,数据库查出来的密码,和当前Realm的名字
return new SimpleAuthenticationInfo(user,user.getPassword(),getClass().getName());
}
}
查询角色sql
public interface RoleMapper extends BaseMapper<Role> {
@Select("select r.* from edu_user u " +
"join edu_user_role eur on u.user_id = eur.user_id " +
"join edu_role r on r.role_id = eur.role_id where u.user_id = #{userId}")
List<Role> selectByUserId(int userId);//根据userId查询该用户所有角色,返回集合
}
根据用户角色id查询该用户所有权限
<mapper namespace="com.woniuxy.shiroboot.dao.PermMapper">
<select id="selectPermInRoleIds" resultType="java.lang.String">
select ep.perm_name
from edu_role_perm erp join edu_perm ep on erp.perm_id = ep.perm_id
where erp.role_id in
<foreach collection="roles" item="r" separator="," open="(" close=")">
#{r.roleId}
</foreach>
</select>
</mapper>
1.7、测试接口
import lombok.extern.java.Log;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class LoginController {
@PostMapping("/login")
public String login(String username,String password){
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username,password));
return "success";
}
@GetMapping("/pay")
public String pay(){
return "success";
}
@GetMapping("destroyEarth")
//权限注解,如果登录用户拥有该权限可正常访问,
//没有则报错AuthorizationException,无法访问
@RequiresPermissions("fireEmployee")
public String destroyEarth(){
return "success";
}
}
先访问未认证不能访问的/pay,会发现无法访问且会自动跳转至/login,登陆后再次访问/pay即可访问成功,然后再访问/destroyEarth,如果有@RequiresPermissions("fireEmployee")该权限可访问成功,否则报错AuthorizationException,无法访问
,
1.8、其它
shiro将账号和密码分成两个地方进行验证,如果我们不自己定义,那么就会调用shiro默认的校验方法;我们可以重新创建一个MyCredentialsMatcher类继承SimpleCredentialsMatcher来实现我们自定义的密码校验方法。