目录
- SpringBoot权限框架零基础入门到实战(shiro)
- 一、从零开始认识 shiro
- 1.1、shiro 简介
- 1.2、shiro 基本功能点
- 1.3、认证流程
- 1.4、授权流程
- 二、Spring Boot 集成 shiro 快速入门
- 2.1、idea Spring Initializr 快速创建项目
- 2.2、加入shiro 所需的依赖
- 2.3、shiro 用户认证
- 2.4、shiro 用户授权
- 三、SpringBoot 使用IniRealm进行认证授权
- 3.1、概述
- 3.2、配置 .ini 文件
- 3.3、测试
- 四、Spring Boot 使用 JdbcRealm 进行认证授权
- 4.1、概述
- 4.2、JdbcRealm 实践
- 4.2.1、数据库驱动
- 4.2.2、数据源
- 4.2.3、数据库表结构
- 4.2.4、创建 testJdbcRealm方法
- 4.2.5、测试
- 4.3、自定义SQL
- 4.3.1、首先把上面三张表分别修改一下表名
- 4.3.2、新建testNewJdbcRealm
- 4.3.3、输出
- 五、Spring Boot 使用自定义 Realm 进行认证授权
- 5.1、概述
- 5.2、自定义 Realm
- 5.2.1、创建 CustomRealm
- 5.2.2、创建 testCustomRealm 方法
- 5.2.3、测试
- 六、SpringBoot整合shiro之盐值加密认证详解
- 6.1、概述
- 6.2、不加盐认证
- 6.2.1、创建 testMatcher 方法
- 6.2.2、加入密码认证核心代码
- 6.2.3、修改 CustomRealm 新增获取密文的方法
- 6.2.4、修改 doGetAuthenticationInfo
- 6.2.5、测试
- 6.3、加盐认证
- 6.3.1、创建 testSaltMatcher 方法
- 6.3.2、加盐验证核心代码
- 6.3.3、修改 CustomRealm 新增获取加盐密文的方法
- 6.3.4、修改 doGetAuthenticationInfo
- 6.3.5、测试
- 七、 Spring Boot 2.x+shiro前后端分离实战-骨架搭建
- 7.1、创建工程
- 7.2、加入相关依赖
- 7.2.1、数据库相关
- 7.2.2、shiro相关
- 7.2.3、redis 相关
- 7.2.4、swagger 相关
- 7.2.5、其它有用的插件配置
- 7.2.6、完整的pom
- 7.3、配置swagger
- 7.3.1、swagger 配置
- 7.3.2、开关配置
- 八、 Spring Boot 2.x+shiro前后端分离实战-整合 redis
- 8.1、自定义 MyStringRedisSerializer 序列化
- 8.2、注入 RedisTempalet
- 8.3、自定义运行时异常
- 8.4、引入RedisService 工具类
- 8.5、配置redis连接池
- 九、Spring Boot 2.x+shiro前后端分离实战-整合mybatis
- 9.1、逆向生成代码
- 9.1.1、generatorConfig.xml
- 9.1.2、配置插件
- 9.2、数据库链接配置
- 9.3、mybatis 动态 sql 配置
- 9.4、扫描mapper
- 十、Spring Boot 2.x+shiro前后端分离实战-自定义 AccessControlFilter token校验
- 10.1、自定义 token
- 10.2、自定义 AccessControlFilter 用户凭证校验
- 10.3、自定义 CredentialsMatcher
- 十一、Spring Boot 2.x+shiro前后端分离实战-自定义 Realm
- 十二、Spring Boot 2.x+shiro前后端分离实战-shiro核心配置
- 十三、Spring Boot 2.x+shiro前后端分离实战-实现用户登录认证访问授权
- 13.1、登录接口业务
- 13.2、用户详情业务
- 13.3、RESTful 控制层接口
- 13.4、配置登录接口白名单
- 十四、接口业务分析
- 14.1、登录接口
- 14.2、获取用户详情接口
SpringBoot权限框架零基础入门到实战(shiro)
一、从零开始认识 shiro
1.1、shiro 简介
shiro是apache的一个开源框架,而且呢是一个权限管理的框架,用于实现用户认证、用户授权。spring 中也有一个权限框架 spring security (原名Acegi),它和 spring 依赖过于紧密,没有 shiro 使用简单。shiro 不依赖于 spring,shiro 不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。使用shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。
1.2、shiro 基本功能点
- subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
- security Manager:安全管理器,主体进行认证和授权都是通过securityManager进行。
- authenticator:认证器,主体进行认证最终通过authenticator进行的。
- authorizer:授权器,主体进行授权最终通过authorizer进行的。
- sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
- SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
- cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
- realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。
1.3、认证流程
- 1、构建SecurityManager环境
- 2、主体提交认证
- 3、SecurityManager 处理
- 4、流转到 Authenticator 执行认证
- 5、通过 Realm 获取相关的用户信息(获取验证数据进行验证)
1.4、授权流程
- 1、创建构建SecurityManager环境
- 2、主体提交授权认证
- 3、SecurityManager 处理
- 4、流转到 Authorizor 授权器执行授权认证
- 5、通过 Realm 从数据库或配置文件获取角色权限数据返回给授权器,进行授权。
二、Spring Boot 集成 shiro 快速入门
2.1、idea Spring Initializr 快速创建项目
New----->Project----->Spring Initializr 一路默认后就创建好一个以spring boot构建的web项目了
2.2、加入shiro 所需的依赖
- 加入jar包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
2.3、shiro 用户认证
- 在junit单元测试类创建shiro认证的测试方法
@Test
public void authentication(){
//构建SecurityManager环境
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//创建一个SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加一个测试账号(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("zhangsan", "123456");
//设置Realm
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码(用户输入的用户名密码)生成token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
//进行登入(提交认证)
subject.login(token);
}catch (IncorrectCredentialsException exception){
System.out.println("用户名密码不匹配");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated()); //登出logout
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
}
- 测试
- 当用户输入用户名密码为“zhangsan”、“123456” 程序输出
- 当用户输入的用户名密码为‘zhangsan“、”1234567“(密码错误) 程序输出
- 当用户输入的用户名密码为"zhangsan1"、“123456”(用户名错误) 程序输出
2.4、shiro 用户授权
上面的小 demo 是 shiro 实现认证的一个过程,做权限管理的时候,分为两块,一个认证,一个是授权,认证是判断用户是否账号密码正确,授权是判断用户登入以后有什么权限
- 在 junit 单元测试类创建 shiro 授权的测试方法
@Test
public void authorization(){
//构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//创建一个SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加一个测试账号、和所拥有的角色(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("zhangsan", "123456","admin","user");
//设置Realm
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码(用户输入的用户名密码)生成token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
//进行登入(提交认证)
subject.login(token);
subject.checkRoles("admin","user");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名密码不匹配");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
//登出logout
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
}
- 测试
- 当用户要 subject.checkRoles(“admin”,“user”); 检测是否拥有 admin、user 角色的时候 程序输出
- 当用户要 subject.checkRoles(“test”,“user”); 检测是否拥有 test、user 角色的时候 程序输出
- checkRoles 方法就是检测用户是否拥有传入的角色,即只要有一个不是用户所拥有的角色就会抛出异常。请看下列源码。
三、SpringBoot 使用IniRealm进行认证授权
我们利用 SimpleAccountRealm 在程序中写死了用户安全数据,接下来我们使用 .ini 将数据移到配置文件中。
3.1、概述
IniRealm是Shiro提供一种Realm实现。用户、角色、权限等信息集中在一个.ini文件那里。
3.2、配置 .ini 文件
- 在 resources 目录下创建一个 shiro.ini 文件
#账号信息
[users]
#账号=密码,角色
test=123456,test
admin=123456,admin
#角色信息
[roles]
test=user:list,user:deleted,user:edit
admin= *
3.3、测试
- 在 junit 单元测试类创建 testIniRealm 方法
@Test
public void testIniRealm(){
//配置文件中的用户权限信息,文件在类路径下
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("test");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:list");
System.out.println("有user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
- 当用户 test 进入到程序时候我们首先用 IniRealm 读取 shiro.ini 配置获得 test 用户的一些安全信息。
- subject.checkRoles(“test”):即是判断该用户是否拥有 test 角色。
- subject.checkPermissions(“user:list”):即是检查用户是否拥有
- user:list 的权限很明显test 这个用户都满足这些条件,最后程序输出
- 假如把 subject.checkRoles(“test”) 改成 subject.checkRoles(“amin”) 即验证用户 test 是否拥有 admin 角色呢?
- 这个时候很明显会抛出异常,因为我们在 shiro.ini 配置里面只给 test 用户 配置了 test 的角色。
- 假如是用户 admin 登录进来呢?
- 很明显不会抛异常,因为用户 admin 拥有了 admin 这个角色,而 admin 这角色设置了 ‘*’ 的通配符即拥有所有的权限所以不会抛出异常。
四、Spring Boot 使用 JdbcRealm 进行认证授权
4.1、概述
上一节课我们主要讲了把用户安全信息(相应的角色/权限)配置在 .ini 文件,使用 IniRealm 去读取 .ini 文件获得用户的安全信息。也是有局限性的。因为我们得事先把所有用户信息配置在.ini 文件,这样显然是行不通的,我们的系统用户都是动态的不固定的,它的一些用户信息权限信息都是变化的,所以固定在.ini 配置文件显然是行不通的。这些数据通常我们都是把它存入到DB 中,那shiro 有没有提供直接从DB读取用户安全信息的域呢 ?
4.2、JdbcRealm 实践
4.2.1、数据库驱动
- 很明显看到 jdbc 就明白是跟数据有关的吧,所以我们要引入 mysql 数据库驱动
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
4.2.2、数据源
- 有了mysql数据库驱动是不是得加个数据源呀?这里我们用阿里的druid
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
4.2.3、数据库表结构
下面我们创建一个名为 shiro 的数据库、分别创建三张表 users、user_roles、roles_permissions
CREATE DATABASE IF NOT EXISTS shiro DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
- users
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(25) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- user_roles
CREATE TABLE `user_roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_name` varchar(25) DEFAULT NULL,
`username` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- roles_permissions
CREATE TABLE `roles_permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`permission` varchar(255) DEFAULT NULL,
`role_name` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 分别插入如下几条数据
USE shiro;
INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('1', 'admin', '123456'); INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('2', 'test', '123456'); INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('1', 'admin', 'admin'); INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('2', 'test', 'test'); INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role name`) VALUES ('1', 'user:deleted', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role name`) VALUES ('2', 'user:list', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role name`) VALUES ('3', '*','admin');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role name`) VALUES ('4', 'user:edit', 'test');
4.2.4、创建 testJdbcRealm方法
@Test
public void testJdbcRealm(){
//配置数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
//配置文件中的用户权限信息,文件在类路径下
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
//使用JdbcRealm下面的值需要为true不然无法查询用户权限
jdbcRealm.setPermissionsLookupEnabled(true);
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("认证状态:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete");
System.out.println("有user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
4.2.5、测试
- 当用户 admin 登录进来的时候 程序输出
- 当用户 test 登录进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
4.3、自定义SQL
4.3.1、首先把上面三张表分别修改一下表名
USE shiro;
ALTER TABLE users RENAME sys_users;
ALTER TABLE user_roles RENAME sys_user_roles;
ALTER TABLE roles_permissions RENAME sys_roles_permissions;
4.3.2、新建testNewJdbcRealm
@Test
public void testNewJdbcRealm(){
//配置数据源
DruidDataSource dataSource = new DruidDataSource();
//如果使用的是新版的驱动 配置driver的时候要注意
dataSource.setUrl("jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
//配置文件中的用户权限信息,文件在类路径下
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
//使用JdbcRealm下面的值需要为true不然无法查询用户权限
jdbcRealm.setPermissionsLookupEnabled(true);
//使用自定义sql查询用户信息
String sql="select password from sys users where username = ?"; jdbcRealm.setAuthenticationQuery(sql);
String roleSql = "select role name from sys_user_roles where username = ?"; jdbcRealm.setUserRolesQuery(roleSql);
String permissionsSql = "select permission from sys_roles_permissions where role_name = ?"; jdbcRealm.setPermissionsQuery(permissionsSql);
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("认证状态:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete","user:edit","user:list","user:add");
System.out.println("有 user:add、user:list、user:edit、user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
4.3.3、输出
五、Spring Boot 使用自定义 Realm 进行认证授权
5.1、概述
虽然 jdbcRealm 已经实现了从数据库中获取用户的验证信息,但是 jdbcRealm灵活性也是稍差一些的,如果要实现自己的一些特殊应用时将不能支持,这个时候可以通过自定义realm来实现身份的认证功能。
通常自定义Realm只需要继承:AuthorizingRealm重写 doGetAuthenticationInfo(用户认证)、doGetAuthorizationInfo(用户授权) 这两个方法即可。
5.2、自定义 Realm
5.2.1、创建 CustomRealm
package com.yingxue.lesson.shiro;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomRealm extends AuthorizingRealm {
// 模拟数据库中的用户名和密码
private Map<String, String> userMap =new HashMap<>();
// 使用代码块初始化数据
{
userMap.put("admin", "123456");
userMap.put("test","123456");
}
/**
* 用户授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //这个就是SimpleAuthenticationInfo(username,password,getName()); 第一个参数
String username= (String) SecurityUtils.getSubject().getPrincipal(); System.out.println("开始执行*******doGetAuthorizationInfo方法啦****"); System.out.println(SecurityUtils.getSubject().getPrincipal()); String username = (String) principalCollection.getPrimaryPrincipal();
//从数据库或者缓存中获取角色数据
List<String> roles = getRolesByUsername(username);
//从数据库或者缓存中获取权限数据
List<String> permissions = getPerminssionsByUsername(username); //创建AuthorizationInfo,并设置角色和权限信息
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addStringPermissions(permissions); authorizationInfo.addRoles(roles);
return authorizationInfo;
}
/**
* 用户人证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("开始执行*******doGetAuthenticationInfo方法啦****");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
if (password == null) {
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,getName());
return authenticationInfo;
}
/**
* 模拟通过数据库获取权限数据
*/
private List<String> getPerminssionsByUsername(String username) { List<String> permissions = new ArrayList<>();
// 只有是 admin 用户才有 新增、删除权限
if(username.equals("admin")){
permissions.add("user:delete");
permissions.add("user:add");
}
permissions.add("user:edit");
permissions.add("user:list");
return permissions;
}
/**
* 模拟通过数据库获取用户角色信息
*/
private List<String> getRolesByUsername(String username) { List<String> roles = new ArrayList<>(); if(username.equals("admin")){
roles.add("admin");
}
roles.add("test");
return roles;
}
/**
* 通过用户名查询密码,模拟数据库查询
*/
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
}
5.2.2、创建 testCustomRealm 方法
@Test
public void testCustomRealm(){
CustomRealm customRealm=new CustomRealm();
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(customRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("认证状态:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete","user:edit","user:list","user:add"); System.out.println("有 user:add、user:list、user:edit、user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
5.2.3、测试
- 1、当用户 admin 登录进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
- 2、当用户 test 登录进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
- 3、当用户 dev 登陆进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("dev", "123456");
通过上述验证过程我们可以发现,shiro 更多的是帮助我们完成验证过程。我们需要从数据库查询当前用户的角色、权限,把这些信息告诉 shiro 框架。当我们执行用户认证的时候首先调用 doGetAuthenticationInfo 进行用户认证,当我们要校验权限的时候 就会执行doGetAuthorizationInfo 进行授权操作。
六、SpringBoot整合shiro之盐值加密认证详解
6.1、概述
自定义 Realm,里面的用户认证所使用的密码都是明文,这种方式是不可取的往往我们在实战开发中用户的密码都是以密文形势进行存储,并且要求加密算法是不可逆的,著名的加密算法有MD5、SHA1等。所以这节课我们主要介绍 Shiro 安全框架学习它的加密方案。这里我们采用md5的方式加密,然后呢。md5加密又分为加盐,和不加盐。
6.2、不加盐认证
6.2.1、创建 testMatcher 方法
@Test
public void testMatcher(){
CustomRealm customRealm=new CustomRealm();
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(customRealm); SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("dev", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("是否权限认证:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete","user:edit","user:list","user:add"); System.out.println("有 user:add、user:list、user:edit、user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
6.2.2、加入密码认证核心代码
//进行加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//采用md5加密
matcher.setHashAlgorithmName("md5");
//设置加密次数
matcher.setHashIterations(2);
customRealm.setCredentialsMatcher(matcher);
6.2.3、修改 CustomRealm 新增获取密文的方法
/**
* 获得密文密码
*/
private String getPasswordMatcher(String currentPassword){
return new Md5Hash(currentPassword, null,2).toString();
}
6.2.4、修改 doGetAuthenticationInfo
/**
* 用户人证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("开始执行*******doGetAuthenticationInfo方法啦****");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
if (password == null) {
return null;
}
String matcherPwd=getPasswordMatcher(password);
System.out.println(matcherPwd);
SimpleAuthenticationInfo authenticationInfo = new
SimpleAuthenticationInfo(username,matcherPwd,getName()); return authenticationInfo;
}
6.2.5、测试
- 1、当用户 admin 登录进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
6.3、加盐认证
当两个用户的密码相同时,单纯使用不加盐的MD5加密方式,会发现数据库中存在相同结构的密码,这样也是不安全的。我们希望即便是两个人的原始密码一样,加密后的结果也不一样。如何做到呢?其实就好像炒菜一样,两道一样的鱼香肉丝,加的盐不一样,炒出来的味道就不一样。MD5加密也是一样,需要进行盐值加密。
6.3.1、创建 testSaltMatcher 方法
@Test
public void testSaltMatcher(){
CustomRealm customRealm=new CustomRealm();
//进行加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//采用mdf加密
matcher.setHashAlgorithmName("md5");
//设置加密次数
matcher.setHashIterations(1);
customRealm.setCredentialsMatcher(matcher);
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(customRealm); SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("是否权限认证:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete","user:edit","user:list","user:add"); System.out.println("有 user:add、user:list、user:edit、user:delete权限"); //if no exception, that's it, we're done!
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
6.3.2、加盐验证核心代码
//设置盐的值
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
6.3.3、修改 CustomRealm 新增获取加盐密文的方法
/**
* 获得密文密码
*/
private String getPasswordMatcher(String currentPassword,String username){
return new Md5Hash(currentPassword, username).toString();
}
6.3.4、修改 doGetAuthenticationInfo
/**
* 用户人证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("开始执行*******doGetAuthenticationInfo方法啦****");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
if (password == null) {
return null;
}
// String matcherPwd=getPasswordMatcher(password);
// System.out.println(matcherPwd);
// SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,matcherPwd,getName());
// SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,getName());
// return authenticationInfo;
String matcherPwd=getPasswordMatcher(password,username);
System.out.println(matcherPwd); SimpleAuthenticationInfo authenticationInfo = new
SimpleAuthenticationInfo(username,matcherPwd,getName());
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return authenticationInfo;
}
6.3.5、测试
- 1、当用户 admin 登录进来 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
- 2、当用户 admin 登录但是密码却输成1234567 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("admin", "1234567");
- 3、当用户 admin 登录进来密码也输入正确了但是 验证的时候改用明文 程序输出
//把下面两个方法注释掉
// String matcherPwd=getPasswordMatcher(password);
// System.out.println(matcherPwd);
SimpleAuthenticationInfo authenticationInfo = new
SimpleAuthenticationInfo(username,password,getName());
shiro 加密就讲到这里,其实有的同学会有疑问,假如我们公司用 sessionID 作为登录状态的凭证那么我们该怎么办呢,怎么才能让用户通过认证呢?这里我们可以自定义密码匹配类继承 HashedCredentialsMatcher类 重写它的 doCredentialsMatch (密码匹配的方法)即可。后面我们讲解实战开发的时候会详细的讲解。
七、 Spring Boot 2.x+shiro前后端分离实战-骨架搭建
- 需求:
- SpringBoot+shiro+redis 整合起来实现用户认证授权实战
- 响应码约定:
- code=0:服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
- code=4010001:(授权异常) 请求要求身份验证。 客户端需要跳转到登录页面重新登录
- code=4010002:(凭证过期) 客户端请求刷新凭证接口
- code=4030001:没有权限禁止访问
- code=400xxxx:系统主动抛出的业务异常
- code=5000001:系统异常
7.1、创建工程
7.2、加入相关依赖
7.2.1、数据库相关
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
7.2.2、shiro相关
<!--shiro 相关-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
7.2.3、redis 相关
<!--redis 相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
7.2.4、swagger 相关
<!--swagger 相关-->
<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>
7.2.5、其它有用的插件配置
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
7.2.6、完整的pom
<?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.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yingxue.lesson</groupId>
<artifactId>shiro-combat</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro-combat</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>
</dependency>
<!--shiro 相关-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<!--redis 相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--swagger 相关-->
<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>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
7.3、配置swagger
7.3.1、swagger 配置
package com.yingxue.lesson.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@EnableSwagger2
@Configuration
public class SwaggerConfig {
@Value("${swagger2.enable}")
private boolean enable;
@Bean
public Docket createRestApi() {
// 这是为了我们在用 swagger 测试接口的时候添加头部信息
List<Parameter> pars = new ArrayList<Parameter>();
ParameterBuilder tokenPar = new ParameterBuilder();
tokenPar.name("sessionId").description("swagger测试用(模拟sessionId传入)非必填header").modelRef(new ModelRef("string")).parameterType("header").required(false);
// 多个的时候 就直接添加到 pars 就可以了
pars.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.yingxue.lesson.controller"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(pars)
.enable(enable);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("迎学教育--shiro 实战")
.description("迎学教育-spring boot 实战系列")
.termsOfServiceUrl("")
.version("1.0")
.build();
}
}
7.3.2、开关配置
#swagger 开关
swagger2.enable=true
八、 Spring Boot 2.x+shiro前后端分离实战-整合 redis
8.1、自定义 MyStringRedisSerializer 序列化
package com.yingxue.lesson.serializer;
import com.alibaba.fastjson.JSON;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class MyStringRedisSerializer implements RedisSerializer<Object> {
private final Charset charset;
public MyStringRedisSerializer() {
this(StandardCharsets.UTF_8);
}
public MyStringRedisSerializer(Charset charset) { Assert.notNull(charset, "Charset must not be null!"); this.charset = charset;
}
@Override
public String deserialize(byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public byte[] serialize(Object object) {
if (object == null) {
return new byte[0];
}
if(object instanceof String){
return object.toString().getBytes(charset);
}else {
String string = JSON.toJSONString(object);
return string.getBytes(charset);
}
}
}
8.2、注入 RedisTempalet
package com.yingxue.lesson.config;
import com.yingxue.lesson.serializer.MyStringRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
MyStringRedisSerializer myStringRedisSerializer=new MyStringRedisSerializer();
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
redisTemplate.setValueSerializer(myStringRedisSerializer);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(myStringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
return redisTemplate;
}
}
8.3、自定义运行时异常
package com.yingxue.lesson.exception;
public class BusinessException extends RuntimeException{
// 异常编号
private final int messageCode;
// 对messageCode 异常信息进行补充说明
private final String detailMessage;
public BusinessException(int messageCode, String message) {
super(message);
this.messageCode = messageCode;
this.detailMessage = message;
}
public int getMessageCode() {
return messageCode;
}
public String getDetailMessage() {
return detailMessage;
}
}
8.4、引入RedisService 工具类
- RedisService,由于代码过长,等我整理好把代码放出来。
8.5、配置redis连接池
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务?连接端?
spring.redis.port=6379
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=100
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=PT10S
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=30
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=1
# 链接超时时间
spring.redis.timeout=PT10S
九、Spring Boot 2.x+shiro前后端分离实战-整合mybatis
9.1、逆向生成代码
9.1.1、generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration>
<!--classPathEntry:数据库的JDBC驱动,换成你自己的驱动位置 -->
<classPathEntry location="F:\mvnrepository\mysql\mysql-connector-java\5.1.28\mysql-connector-java-5.1.28.jar" />
<!-- 一个数据库一个context -->
<!--defaultModelType="flat" 大数据字段,不分表 -->
<context id="MysqlTables" targetRuntime="MyBatis3" defaultModelType="flat"> <property name="autoDelimitKeywords" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<property name="javaFileEncoding" value="utf-8"/>
<plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!-- 注释 -->
<commentGenerator>
<!-- 是否取消注释 -->
<property name="suppressAllComments" value="true"/>
<!-- 是否生成注释代时间戳-->
<property name="suppressDate" value="true"/>
</commentGenerator>
<!-- jdbc连接 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/test_mybatis" userId="root"
password="root"/>
<!-- 类型转换 -->
<javaTypeResolver>
<!-- 是否使用bigDecimal, false可自动转化以下类型(Long, Integer, Short, etc.) -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 生成实体类地址H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\java (要改成你自己实际的目录) -->
<javaModelGenerator targetPackage="com.yingxue.lesson.entity"
targetProject="H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\java">
<property name="enableSubPackages" value="false"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 生成mapxml文件 H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\resources (要改成你自己实际的目录) -->
<sqlMapGenerator targetPackage="mapper" targetProject="H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\resources">
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!-- 生成mapxml对应client,也就是接口dao -->
<javaClientGenerator targetPackage="com.yingxue.lesson.mapper" targetProject="H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\java"
type="XMLMAPPER">
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<table tableName="sys_user" domainObjectName="SysUser"
enableCountByExample="false"
enableUpdateByExample="false"
enableDeleteByExample="false"
enableSelectByExample="false"
selectByExampleQueryId="true">
<columnOverride column="sex" javaType="java.lang.Integer"/>
<columnOverride column="status" javaType="java.lang.Integer"/>
<columnOverride column="create_where" javaType="java.lang.Integer"/>
<columnOverride column="deleted" javaType="java.lang.Integer"/>
</table>
</context>
</generatorConfiguration>
9.1.2、配置插件
<!--配置mybatis代码生成工具-->
<!--使用生成工具可以直接使用maven的命令提示符,
生成语句是mvn mybatis-generator:generate,
一旦数据库进行了更改,
都需使用这句代码重新生成bean、dao、mapper文件-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<configuration>
<configurationFile>
src/main/resources/generatorConfig.xml
</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<executions>
<execution>
<phase>deploy</phase>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
</dependencies>
</plugin>
9.2、数据库链接配置
#数据库配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver spring.datasource.druid.url=jdbc:mysql://localhost:3306/test_mybatis? useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.druid.username=root spring.datasource.druid.password=root
################## 连接池配置 ################
#连接池建立时创建的初始化连接数
spring.datasource.druid.initial-size=5
#连接池中最大的活跃连接数
spring.datasource.druid.max-active=20
#连接池中最小的活跃连接数
spring.datasource.druid.min-idle=5
# 配置获取连接等待超时的时间
spring.datasource.druid.max-wait=60000
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20 spring.datasource.druid.validation-query=SELECT 1 FROM DUAL spring.datasource.druid.validation-query-timeout=30000
#是否在获得连接后检测其可用性
spring.datasource.druid.test-on-borrow=false
#是否在连接放回连接池后检测其可用性
spring.datasource.druid.test-on-return=false
#是否在连接空闲一段时间后检测其可用性
spring.datasource.druid.test-while-idle=true
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
9.3、mybatis 动态 sql 配置
mybatis.mapper-locations=classpath:mapper/*.xml
9.4、扫描mapper
@MapperScan("com.yingxue.lesson.mapper")
十、Spring Boot 2.x+shiro前后端分离实战-自定义 AccessControlFilter token校验
10.1、自定义 token
package com.yingxue.lesson.shiro;
import org.apache.shiro.authc.UsernamePasswordToken;
public class CustomPasswordToken extends UsernamePasswordToken {
private String token;
public CustomPasswordToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
}
10.2、自定义 AccessControlFilter 用户凭证校验
package com.yingxue.lesson.shiro;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONPObject;
import com.yingxue.lesson.constants.Constant;
import com.yingxue.lesson.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class CustomAccessControlFilter extends AccessControlFilter {
/**
* 是否允许访问
* true:允许,交下一个Filter处理
* false:回往下执行onAccessDenied
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
return false;
}
/**
* 表示访问拒绝时是否自己处理,
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException {
HttpServletRequest request= (HttpServletRequest) servletRequest;
try {
log.info(request.getMethod());
log.info(request.getRequestURL().toString());
//获取用户凭证
String accessToken=request.getHeader(Constant.SESSION ID); if(StringUtils.isEmpty(accessToken)){
throw new BusinessException(4001002,"用户凭证为空请重新登录");
}
CustomPasswordToken customPasswordToken=new CustomPasswordToken(accessToken);
// 委托给Realm进行登录
getSubject(servletRequest, servletResponse).login(customPasswordToken); }catch (BusinessException e) {
customResponse(e.getMessageCode(),e.getDefaultMessage(),servletResponse); return false;
} catch (AuthenticationException e) {
if(e.getCause() instanceof BusinessException){
BusinessException exception= (BusinessException) e.getCause();
customResponse(exception.getMessageCode(),exception.getDefaultMessage(),servletResponse);
return false;
}else {
customResponse(4000001,"用户认证失败",servletResponse);
return false;
}
}catch (AuthorizationException e){
if(e.getCause() instanceof BusinessException){
BusinessException exception= (BusinessException) e.getCause();
customResponse(exception.getMessageCode(),exception.getDefaultMessage(),servletResponse); return false;
}else {
customResponse(4030001,"没有访问的权限",servletResponse);
return false;
}
}
catch (Exception e){
if(e.getCause() instanceof BusinessException){
BusinessException exception= (BusinessException) e.getCause();
customResponse(exception.getMessageCode(),exception.getDefaultMessage(),servletResponse); return false;
}else {
customResponse(5000001,"系统异常",servletResponse);
return false;
}
}
return true;
}
/**
* 自定义错误响应
*/
private void customRsponse(int code, String msg, ServletResponse response){
// 自定义异常的类,用户返回给客户端相应的JSON格式的信息 try {
Map<String,Object> result=new HashMap<>(); result.put("code",code); result.put("msg",msg);
response.setContentType("application/json; charset=utf-8"); response.setCharacterEncoding("UTF-8");
String userJson = JSON.toJSONString(result);
OutputStream out = response.getOutputStream();
out.write(userJson.getBytes("UTF-8"));
out.flush();
} catch (IOException e) { log.error("eror={}",e);
}
}
}
10.3、自定义 CredentialsMatcher
package com.yingxue.lesson.shiro;
import com.yingxue.lesson.exception.BusinessException;
import com.yingxue.lesson.service.RedisService;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; import org.springframework.beans.factory.annotation.Autowired;
public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {
@Autowired
private RedisService redisService;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
CustomPasswordToken customPasswordToken= (CustomPasswordToken) token;
String accessToken = (String) customPasswordToken.getPrincipal();
if(!redisService.hasKey(accessToken)){
throw new BusinessException(4001002,"授权信息信息无效请重新登录");
}
return true;
}
}
十一、Spring Boot 2.x+shiro前后端分离实战-自定义 Realm
package com.yingxue.lesson.shiro;
import com.yingxue.lesson.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
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.*;
@Slf4j
public class CustomRealm extends AuthorizingRealm {
@Autowired
private RedisService redisService;
/**
* 设置支持令牌校验
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof CustomPasswordToken;
}
/**
* 主要业务:
* 系统业务出现要验证用户的角色权限的时候,就会调用这个方法
* 来获取该用户所拥有的角色/权限
* 这个用户授权的方法我们可以缓存起来不用每次都调用这个方法。
* 后续的课程我们会结合 redis 实现它
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
String accessToken= (String) SecurityUtils.getSubject().getPrincipal();
String userId= (String) redisService.get(accessToken);
authorizationInfo.addRoles(getRolesByUserId(userId));
authorizationInfo.setStringPermissions(getPermissionByUserId(userId));
return authorizationInfo;
}
/**
* 主要业务:
* 当业务代码调用 subject.login(customPasswordToken); 方法后
* 就会自动调用这个方法 验证用户名/密码
* 这里我们改造成 验证 token 是否有效 已经自定义了 shiro 验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
CustomPasswordToken token= (CustomPasswordToken) authenticationToken;
SimpleAuthenticationInfo simpleAuthenticationInfo=new
SimpleAuthenticationInfo(token.getPrincipal(),token.getPrincipal(),getName());
return simpleAuthenticationInfo;
}
/**
* 获取用户的角色
* 这里先用伪代码代替
* 后面我们讲到权限管理系统后 再从 DB 读取
*/
private List<String> getRolesByUserId(String userId) {
List<String> roles = new ArrayList<>();
if(userId.equals("9a26f5f1-cbd2-473d-82db-1d6dcf4598f8")){
roles.add("admin");
}else {
roles.add("test");
}
return roles;
}
/**
* 获取用户的权限
* 这里先用伪代码代替
* 后面我们讲到权限管理系统后 再从 DB 读取
*/
private List<String> getPermissionByUserId(String userId) {
List<String> permissions = new ArrayList<>();
//只有是 admin 用户才拥有所有权限
if(userId.equals("9a26f5f1-cbd2-473d-82db-1d6dcf4598f8")){
permissions.add("*");
}else {
permissions.add("sys:user:edit");
permissions.add("sys:user:list");
}
return permissions;
}
}
十二、Spring Boot 2.x+shiro前后端分离实战-shiro核心配置
package com.yingxue.lesson.config;
import com.yingxue.lesson.shiro.CustomAccessControlFilter;
import com.yingxue.lesson.shiro.CustomHashedCredentialsMatcher;
import com.yingxue.lesson.shiro.CustomRealm;
import org.apache.shiro.mgt.SecurityManager;
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 javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* 自定义密码 校验
*/
@Bean
public CustomHashedCredentialsMatcher customHashedCredentialsMatcher(){ return new CustomHashedCredentialsMatcher();
}
/**
* 自定义域
*/
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm=new CustomRealm();
customRealm.setCredentialsMatcher(customHashedCredentialsMatcher());
return customRealm;
}
/**
* 安全管理
*/
@Bean
public SecurityManager securityManager(){
//构建 SecurityManager环境
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//自定义 Realm
securityManager.setRealm(customRealm());
return securityManager;
}
/**
* shiro过滤器,配置拦截哪些请求
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//自定义拦截器限制并发人数,参考博客:
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//用来校验token
filtersMap.put("token", new CustomAccessControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/api/user/login", "anon");
//放开swagger-ui地址
filterChainDefinitionMap.put("/swagger/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/favicon.ico", "anon");
filterChainDefinitionMap.put("/captcha.jpg", "anon");
filterChainDefinitionMap.put("/csrf","anon");
filterChainDefinitionMap.put("/**","token,authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new
DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}
十三、Spring Boot 2.x+shiro前后端分离实战-实现用户登录认证访问授权
13.1、登录接口业务
@Override
public LoginRespVO login(LoginReqVO vo) {
SysUser userByName = sysUserMapper.getUserByName(vo.getUsername()); if(userByName==null){
throw new BusinessException(4001004,"用户名密码不匹配");
}
if(userByName.getStatus()==2){
throw new BusinessException(4001004,"该账户已经被锁定,请联系系统管理员");
}
if(!PasswordUtils.matches(userByName.getSalt(),vo.getPassword(),userByName.getPassword())){
throw new BusinessException(4001004,"用户名密码不匹配");
}
LoginRespVO loginRespVO=new LoginRespVO();
loginRespVO.setId(userByName.getId());
String token= UUID.randomUUID().toString();
loginRespVO.setToken(token);
redisService.set(token,userByName.getId(),60, TimeUnit.MINUTES);
return loginRespVO;
}
/**
* 获得密文密码
*/
private String getPasswordMatcher(String currentPassword,String salt){ return new Md5Hash(currentPassword, salt).toString();
}
13.2、用户详情业务
@Override
public SysUser detail(String id) {
return sysUserMapper.selectByPrimaryKey(id);
}
13.3、RESTful 控制层接口
package com.yingxue.lesson.controller;
import com.yingxue.lesson.entity.SysUser;
import com.yingxue.lesson.service.UserService;
import com.yingxue.lesson.vo.req.LoginReqVO;
import com.yingxue.lesson.vo.resp.LoginRespVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@Api(tags = "用户模块")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/user/login")
@ApiOperation(value ="用户登录接口")
public Map<String, Object> login(@RequestBody LoginReqVO vo){
Map<String,Object> result=new HashMap<>();
result.put("code",0);
result.put("data",userService.login(vo));
return result;
}
@GetMapping("/user/{id}")
@ApiModelProperty(value = "查询用户详情接口")
@RequiresPermissions("sys:user:detail")
public Map<String, Object> detail(@PathVariable("id") @ApiParam(value = "用户Id") String id){
Map<String,Object> result=new HashMap<>();
result.put("code",0);
result.put("data",userService.detail(id));
return result;
}
}
13.4、配置登录接口白名单
- 修改com.yingxue.lesson.config.ShiroConfig#shiroFilterFactoryBean加入shiro 忽略拦截
filterChainDefinitionMap.put("/api/user/login", "anon");
十四、接口业务分析
14.1、登录接口
- 1、用 LoginReqVO 接收用户提交过来的用户名密码的数据
- 2、把 vo 传入业务层接口进行业务处理
- 3、登录业务处理
- 1、通过用户名去db查询用户信息
- 2、判断是否查询到用信息
- 3、判断是否被禁用
- 4、校验密码是否正确
- 5、生成token、把token做为key、userId作为value存入redis并设置过期时间为60分钟(后面认证需要)
- 6、封装返回数据LoginRespVO 返回前端
14.2、获取用户详情接口
这个接口有两个关键点,因为用户首次登录后,后续再访问我们的系统资源的时候,无需再传入用户密码进行验证只需要携带登录生成的token可以了,我们的后端会围绕token使用shiro进行一系列的认证。当用户通过了用户认证的时候还需要进行授权,因为用户详解接口设置了访问权限(@RequiresPermissions(“sys:user:detail”))所以我们还要对访问的用户进行授权。