目录
一、创建权限表,生成基础代码
-- init.sys_menu_authority definition
CREATE TABLE `sys_menu_authority` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL COMMENT '权限名称',
`cascade_code` varchar(256) DEFAULT NULL COMMENT '层级全路径 示例 1#2#3',
`parent_id` bigint DEFAULT NULL COMMENT '父权限id',
`level` int NOT NULL COMMENT '级别从1开始',
`permission_code` varchar(64) DEFAULT NULL COMMENT '权限编码字符串',
`sort` int DEFAULT NULL COMMENT '排序',
`button_flag` tinyint DEFAULT NULL COMMENT '是否是按钮 0:菜单 1:按钮',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`delete_flag` tinyint DEFAULT '0' COMMENT ' 是否删除 0否 1是 ',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=312 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='菜单权限表';
-- init.sys_role definition
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名称',
`role_type` tinyint DEFAULT NULL COMMENT '1是系统用户 2 普通用户',
`permission` json DEFAULT NULL COMMENT '角色权限',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '描述',
`create_by` bigint NOT NULL COMMENT '创建人',
`update_by` bigint NOT NULL COMMENT '修改人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '修改时间',
`delete_flag` tinyint NOT NULL DEFAULT '0' COMMENT ' 是否删除 0否 1是',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='角色表';
-- init.sys_role_authority definition
CREATE TABLE `sys_role_authority` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_id` bigint DEFAULT NULL COMMENT '角色id',
`authority_id` bigint DEFAULT NULL COMMENT '菜单权限id',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` bigint DEFAULT NULL COMMENT '创建人',
`update_by` bigint DEFAULT NULL COMMENT '更新人',
`delete_flag` tinyint DEFAULT '0' COMMENT '是否删除 0否 1是',
PRIMARY KEY (`id`) USING BTREE,
KEY `index_role_id` (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=24600 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='角色权限表';
-- init.sys_user_role definition
CREATE TABLE `sys_user_role` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`user_id` bigint unsigned DEFAULT NULL COMMENT '用户主键Id',
`user_type` tinyint unsigned DEFAULT NULL COMMENT '用户类型:1是系统用户 2 普通用户',
`dept_id` bigint unsigned NOT NULL COMMENT '部门id',
`dept_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '部门名称',
`role_id` bigint unsigned DEFAULT NULL COMMENT '角色主键Id',
`create_by` bigint NOT NULL COMMENT '创建人',
`update_by` bigint NOT NULL COMMENT '更新人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
`delete_flag` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否删除 0否 1是 ',
PRIMARY KEY (`id`) USING BTREE,
KEY `index_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='用户角色表';
INSERT INTO init.sys_menu_authority
(id, name, cascade_code, parent_id, `level`, permission_code, sort, button_flag, create_time, delete_flag)
VALUES(1, '系统管理', '1', 0, 1, NULL, 3, 0, '2022-03-03 14:44:17', 0);
INSERT INTO init.sys_menu_authority
(id, name, cascade_code, parent_id, `level`, permission_code, sort, button_flag, create_time, delete_flag)
VALUES(2, '角色管理', '1#2', 1, 2, NULL, 1, 0, '2022-03-03 14:44:17', 0);
INSERT INTO init.sys_menu_authority
(id, name, cascade_code, parent_id, `level`, permission_code, sort, button_flag, create_time, delete_flag)
VALUES(3, '人员管理', '1#3', 1, 2, NULL, 2, 0, '2022-03-03 14:44:17', 0);
INSERT INTO init.sys_menu_authority
(id, name, cascade_code, parent_id, `level`, permission_code, sort, button_flag, create_time, delete_flag)
VALUES(4, '添加、编辑', '1#2#4', 2, 3, NULL, 1, 1, '2022-03-03 14:44:17', 0);
INSERT INTO init.sys_menu_authority
(id, name, cascade_code, parent_id, `level`, permission_code, sort, button_flag, create_time, delete_flag)
VALUES(5, '删除', '1#2#5', 2, 3, NULL, 2, 1, '2022-03-03 14:44:17', 0);
INSERT INTO init.sys_menu_authority
(id, name, cascade_code, parent_id, `level`, permission_code, sort, button_flag, create_time, delete_flag)
VALUES(6, '添加', '1#3#6', 3, 3, NULL, 1, 1, '2022-03-03 14:44:17', 0);
INSERT INTO init.sys_menu_authority
(id, name, cascade_code, parent_id, `level`, permission_code, sort, button_flag, create_time, delete_flag)
VALUES(7, '设置角色', '1#3#7', 3, 3, NULL, 2, 1, '2022-03-03 14:44:17', 0);
INSERT INTO init.sys_menu_authority
(id, name, cascade_code, parent_id, `level`, permission_code, sort, button_flag, create_time, delete_flag)
VALUES(8, '删除', '1#3#8', 3, 3, NULL, 3, 1, '2022-03-03 14:44:17', 0);
INSERT INTO init.sys_role
(id, role_name, role_type, permission, remark, create_by, update_by, create_time, update_time, delete_flag)
VALUES(1, '超级管理员', 0, '[{"code": "REGULAR:WORK_GROUP", "message": "工作组"}, {"code": "REGULAR:OUTSOURCING_USER", "message": "外协人员名单"}, {"code": "REGULAR:PUNCH_RECORD", "message": "打卡记录"}, {"code": "REGULAR:PUNCH_SUMMARY", "message": "打卡汇总"}, {"code": "ADMIN:PUNCH_CONFIG", "message": "打卡设置"}, {"code": "ADMIN:ROLE_MANAGE", "message": "角色管理"}, {"code": "ADMIN:USER_MANAGE", "message": "用户管理"}]', '超级管理员', 0, 0, '2021-12-31 00:00:00', '2021-12-31 00:00:00', 0);
INSERT INTO init.sys_role_authority
(id, role_id, authority_id, update_time, create_time, create_by, update_by, delete_flag)
VALUES(24569, 1, 1, '2022-03-03 15:29:42', '2022-03-03 15:29:38', 0, 0, 0);
INSERT INTO init.sys_role_authority
(id, role_id, authority_id, update_time, create_time, create_by, update_by, delete_flag)
VALUES(24570, 1, 2, '2022-03-03 15:29:42', '2022-03-03 15:29:38', 0, 0, 0);
INSERT INTO init.sys_role_authority
(id, role_id, authority_id, update_time, create_time, create_by, update_by, delete_flag)
VALUES(24571, 1, 3, '2022-03-03 15:29:42', '2022-03-03 15:29:38', 0, 0, 0);
INSERT INTO init.sys_role_authority
(id, role_id, authority_id, update_time, create_time, create_by, update_by, delete_flag)
VALUES(24572, 1, 4, '2022-03-03 15:29:42', '2022-03-03 15:29:38', 0, 0, 0);
INSERT INTO init.sys_role_authority
(id, role_id, authority_id, update_time, create_time, create_by, update_by, delete_flag)
VALUES(24573, 1, 5, '2022-03-03 15:29:42', '2022-03-03 15:29:38', 0, 0, 0);
INSERT INTO init.sys_role_authority
(id, role_id, authority_id, update_time, create_time, create_by, update_by, delete_flag)
VALUES(24574, 1, 6, '2022-03-03 15:29:42', '2022-03-03 15:29:38', 0, 0, 0);
INSERT INTO init.sys_role_authority
(id, role_id, authority_id, update_time, create_time, create_by, update_by, delete_flag)
VALUES(24575, 1, 7, '2022-03-03 15:29:42', '2022-03-03 15:29:38', 0, 0, 0);
INSERT INTO init.sys_role_authority
(id, role_id, authority_id, update_time, create_time, create_by, update_by, delete_flag)
VALUES(24576, 1, 8, '2022-03-03 15:29:42', '2022-03-03 15:29:38', 0, 0, 0);
INSERT INTO init.sys_user_role
(id, user_id, user_type, dept_id, dept_name, role_id, create_by, update_by, create_time, update_time, delete_flag)
VALUES(1, 1, 0, 1, '中华人民共和国', 1, 1, 1, '2021-12-31 00:00:00', '2021-12-31 00:00:00', 0);
生成代码
二、spring boot整合shiro
2.1创建权限管理功能模块(shiro)
2.2引入依赖pom.xml(shiro)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lyj.initMode</groupId>
<artifactId>initMode-function</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>initMode-function-shiro</artifactId>
<description>权限管理功能模块</description>
<dependencies>
<dependency>
<groupId>com.lyj.initMode</groupId>
<artifactId>initMode-function-mysql</artifactId>
</dependency>
<dependency>
<groupId>com.lyj.initMode</groupId>
<artifactId>initMode-function-redis</artifactId>
</dependency>
<dependency>
<groupId>com.lyj.initMode</groupId>
<artifactId>initMode-common-base</artifactId>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
<!--jjwt工具类 生成、解析和校验Json Web Token-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</dependency>
</dependencies>
</project>
pom.xml(initMode)
<!--shiro 安全认证框架-->
<shiro.verison>1.6.0</shiro.verison>
<!--jjwt工具类 生成、解析和校验Json Web Token-->
<jjwt.version>0.11.2</jjwt.version>
<dependency>
<groupId>com.lyj.initMode</groupId>
<artifactId>initMode-function-shiro</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.verison}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
pom.xml(function)
<module>shiro</module>
2.3配置shiro
LoginTypeEnum.java(base)
package com.lyj.common.base.enums;
import lombok.Getter;
/**
* 登录方式枚举
*/
@Getter
public enum LoginTypeEnum {
/**
* 登录方式
*/
PC_LOGIN("PC", "PC端登录"),
APP_LOGIN("APP", "APP/小程序端登录");
/**
* 登录方式码
*/
private final String code;
/**
* 登录方式描述
*/
private final String message;
LoginTypeEnum(String code, String message) {
this.code = code;
this.message = message;
}
public static LoginTypeEnum getByCode(String code){
for (LoginTypeEnum type: LoginTypeEnum.values()){
if(type.getCode().equals(code)){
return type;
}
}
return null;
}
}
AdminFlagEnum.java(base)
package com.lyj.service.enums;
import lombok.Getter;
/**
* 管理员枚举
*/
@Getter
public enum AdminFlagEnum {
/**
* 管理员标记
*/
NO(0, "非管理员"),
YES(1, "管理员");
private final Integer code;
private final String message;
AdminFlagEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
SecurityConstant.java(base)
package com.lyj.service.constant;
/**
* Shiro常量池
*/
public class SecurityConstant {
private SecurityConstant() {
}
/**
* anon字符串
*/
public static final String ANON_TAG = "anon";
/**
* logout字符串
*/
public static final String LOGOUT_TAG = "logout";
/**
* token字符串
*/
public static final String TOKEN_TAG = "token";
/**
* token请求头字段码
*/
public static final String HEADER_KEY_AUTH_TOKEN = "Authorization";
/**
* 用户认证信息 key
*/
public static final String USER_AUTH_INFO_KEY = "USER_AUTH_INFO_KEY";
/**
* 用户ID key
*/
public static final String USER_ID_KEY = "USER_ID_KEY";
/**
* 用户账号 key
*/
public static final String USER_ACCOUNT_KEY = "USER_ACCOUNT_KEY";
/**
* 用户名 key
*/
public static final String USER_NAME_KEY = "USER_NAME_KEY";
/**
* 用户部门ID key
*/
public static final String USER_DEPT_ID_KEY = "USER_DEPT_ID_KEY";
/**
* 用户部门名称 key
*/
public static final String USER_DEPT_NAME_KEY = "USER_DEPT_NAME_KEY";
/**
* 用户权限列表 key
*/
public static final String USER_PERMISSION_KEY = "USER_PERMISSION_KEY";
}
AuthInfoDTO.java(domain)
package com.lyj.common.domain.sysUser.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 用户登录认证信息
*/
@Data
public class AuthInfoDTO implements Serializable {
private static final long serialVersionUID = 3767898741324992335L;
/**
* 用户id
*/
private Long userId;
/**
* 用户名
*/
private String account;
/**
* 姓名
*/
private String name;
/**
* 昵称
*/
private String nickName;
/**
* 用户登录时选择的部门id
*/
private Long departId;
/**
* 用户登录时选择的部门名字
*/
private String departName;
/**
* 用户登录时选择的角色
*/
private Long roleId;
/**
* 角色名称
*/
private String roleName;
/**
* 是不是超级管理员 0 不是 1 是
*/
private Integer adminFlag;
}
生成权限service(shiro)
SysUserRoleService.java(shiro)
package com.lyj.function.shiro.service.sysUserRole;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lyj.common.domain.sysUserRole.entity.SysUserRole;
import java.util.List;
/**
* 用户角色表(SysUserRole)表服务接口
*
* @author lyj
* @since 2024-07-04 17:48:24
*/
public interface SysUserRoleService extends IService<SysUserRole> {
/**
* 查询用户所有的角色id
*
* @param userId 用户id
* @return 角色id
*/
List<Long> getRoleListByUser(Long userId);
}
SysUserRoleServiceImpl.java(shiro)
package com.lyj.function.shiro.service.sysUserRole.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lyj.function.mysql.dao.sysUserRole.SysUserRoleMapper;
import com.lyj.common.domain.sysUserRole.entity.SysUserRole;
import com.lyj.function.shiro.service.sysUserRole.SysUserRoleService;
import org.springframework.stereotype.Service;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户角色表(SysUserRole)表服务实现类
*
* @author lyj
* @since 2024-07-04 17:48:24
*/
@Service
@Slf4j
@AllArgsConstructor
public class SysUserRoleServiceImpl extends ServiceImpl<SysUserRoleMapper, SysUserRole> implements SysUserRoleService {
private SysUserRoleMapper sysUserRoleMapper;
@Override
public List<Long> getRoleListByUser(Long userId) {
List<SysUserRole> sysUserRoleList = lambdaQuery().eq(SysUserRole::getUserId, userId).list();
return sysUserRoleList.stream().map(SysUserRole::getRoleId).collect(Collectors.toList());
}
}
SysRoleAuthorityService.java(shiro)
package com.lyj.function.shiro.service.sysRoleAuthority;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lyj.common.domain.sysMenuAuthority.entity.SysMenuAuthority;
import com.lyj.common.domain.sysRoleAuthority.entity.SysRoleAuthority;
import java.util.List;
/**
* 角色权限表(SysRoleAuthority)表服务接口
*
* @author lyj
* @since 2024-07-04 17:48:24
*/
public interface SysRoleAuthorityService extends IService<SysRoleAuthority> {
/**
* 根据roleId获取权限列表,如果不传则获取全部
*
* @param roleId 角色id
* @return List<SysMenuAuthorityVo>
*/
List<SysMenuAuthority> authorityListById(List<Long> roleId);
}
SysRoleAuthorityServiceImpl.java(shiro)
package com.lyj.service.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lyj.service.entity.SysMenuAuthority;
import com.lyj.service.entity.SysRoleAuthority;
import com.lyj.service.vo.SysMenuAuthorityVO;
import java.util.List;
/**
* 角色权限表(SysRoleAuthority)表服务接口
*
* @author makejava
* @since 2024-06-26 18:38:41
*/
public interface SysRoleAuthorityService extends IService<SysRoleAuthority> {
/**
* 根据roleId获取权限列表,如果不传则获取全部
*
* @param roleId 角色id
* @return List<SysMenuAuthorityVo>
*/
List<SysMenuAuthorityVO> authorityTreeById(Long roleId);
/**
* 根据roleIdList获取权限列表,如果不传则获取全部
*
* @param roleIdList 角色id
* @return List<SysMenuAuthorityVo>
*/
List<SysMenuAuthorityVO> authorityTreeByRoleList(List<Long> roleIdList);
/**
* 根据roleId获取权限列表,如果不传则获取全部
*
* @param roleId 角色id
* @return List<SysMenuAuthorityVo>
*/
List<SysMenuAuthority> authorityListById(Long roleId);
/**
* 根据roleId获取权限列表,如果不传则获取全部
*
* @param roleId 角色id
* @return List<SysMenuAuthorityVo>
*/
List<SysMenuAuthority> authorityListById(List<Long> roleId);
/**
* 更新角色权限
*
* @param roleId 角色id
* @return List<SysMenuAuthorityVo>
*/
void update(Long roleId, List<Long> authorityIdList);
}
ShiroProperties.java(shiro)
package com.lyj.function.shiro.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Collections;
import java.util.List;
/**
* Shiro属性配置类
* @author faminefree
*/
@Data
@ConfigurationProperties("shiro")
public class ShiroProperties {
/**
* 登录接口地址
*/
private String loginUrl = "/api/v1/security/login";
/**
* 登出接口地址
*/
private String logoutUrl = "/api/v1/security/logout";
/**
* 注册接口地址
*/
private String registerUrl = "/api/v1/security/register";
private String feignUrl = "/api/v1/punch_summary_month/summary";
/**
* 登录成功重定向地址
*/
private String successUrl = "/";
/**
* 未授权失败地址
*/
private String unauthorizedUrl = "/unauthorized";
/**
* session默认失效时间(单位秒)
*/
private long globalSessionTimeout = 86400;
/**
* 授权后方可访问资源列表
*/
private List<String> authcResources = Collections.singletonList("/**");
/**
* 未授权即可访问资源列表
*/
private List<String> anonResources = Collections.emptyList();
/**
* token默认失效时间(单位秒)
*/
private long globalTokenTimeout = 604800;
/**
* token默认盐
*/
private String globalTokenSalt = "b0s16nqqwq19534xucgd3n0wbsz6w0o1";
/**
* Opms地址
*/
private String opmsUrl = "http://10.10.48.31:30080";
/**
* Opms调用密钥
*/
private String opmsSecurityKey = "b9a798e61eeadf2d98fb2b3fd19c6971";
/**
* Opms应用ID
*/
private String opmsAppId = "HN202108271706091431181229691510786";
/**
* Opms链接-获取组织树
*/
private String opmsUrlDeptTree = "/api/v1/find/org/structure";
/**
* Opms链接-获取组织列表
*/
private String opmsUrlDeptList = "/api/v1/find/org/organize/2";
/**
* Opms链接-用户详情
*/
private String opmsUrlUserDetail = "/api/v1/system/user/userEcho/{userId}";
/**
* Opms链接-根据oa账号获取用户详情
*/
private String opmsUrlUserDetailByOa = "/api/v1/system/user/findUserByOa/{oaAccount}";
/**
* Opms连接-根据部门ID获取用户列表
*/
private String opmsUrlUserListByDept = "/api/v1/user/info/dept/{deptId}";
/**
* Opms链接-用户登录接口
*/
private String opmsUrlUserLogin = "/api/v2/user/login";
}
StatelessAuthToken.java(shiro)
package com.lyj.function.shiro.authc;
import lombok.Getter;
import lombok.Setter;
import org.apache.shiro.authc.AuthenticationToken;
/**
*身份验证实体类 用于收集用户在过滤器TokenFilter中提交的token信息
*/
public class StatelessAuthToken implements AuthenticationToken {
private static final long serialVersionUID = -2827334463469969309L;
@Getter
@Setter
private String token;
@Override
public Object getPrincipal() {
return this.token;
}
@Override
public Object getCredentials() {
return this.token;
}
}
TokenRealm.java(shiro)
package com.lyj.service.realm;
import com.lyj.service.authc.StatelessAuthToken;
import com.lyj.service.constant.SecurityConstant;
import com.lyj.service.dto.AuthInfoDTO;
import com.lyj.service.entity.SysMenuAuthority;
import com.lyj.service.enums.AdminFlagEnum;
import com.lyj.service.enums.ErrorCodeEnum;
import com.lyj.service.service.SysMenuAuthorityService;
import com.lyj.service.service.SysRoleAuthorityService;
import com.lyj.service.service.SysUserRoleService;
import com.lyj.service.util.BizExceptionUtil;
import com.lyj.service.util.DateUtil;
import com.lyj.service.util.JwtTokenUtil;
import com.lyj.service.util.RedisUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
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.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
*Realm:域,Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,
* Shiro 会从应用配置的 Realm 中查找用户及其权限信息。
* 从这个意义上讲,Realm 实质上是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro 。
* 当配置 Shiro时,你必须至少指定一个 Realm ,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。
* Shiro 内置了可以连接大量安全数据源(又名目录)的 Realm,如 LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等。
* 如果缺省的 Realm 不能满足需求,你还可以插入代表自定义数据源的自己的 Realm 实现。
*/
@Slf4j
@Component
@AllArgsConstructor
public class TokenRealm extends AuthorizingRealm {
private final SysRoleAuthorityService sysRoleAuthorityService;
private final SysMenuAuthorityService sysMenuAuthorityService;
private final SysUserRoleService userRoleService;
//权限获取 获取指定身份的权限,并返回相关信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String token = (String) principals.getPrimaryPrincipal();
String account = JwtTokenUtil.getUsernameFromToken(token);
if (StringUtils.isBlank(account)) {
log.error("TokenRealm|doGetAuthorizationInfo|error|token={}|message={}", token, "获取用户token失败");
BizExceptionUtil.bizException(ErrorCodeEnum.LACK_TOKEN);
return null;
}
AuthInfoDTO authInfo = RedisUtil.get(JwtTokenUtil.getAuthInfoKey(account, token), AuthInfoDTO.class);
if (authInfo == null || StringUtils.isBlank(authInfo.getAccount())) {
log.error("TokenRealm|doGetAuthorizationInfo|error|token={}|message={}", token, "用户未认证");
BizExceptionUtil.bizException(ErrorCodeEnum.NO_LOGGED_IN);
return null;
}
List<String> roleList = new ArrayList<>();
if (this.isAdmin(authInfo)) {
List<SysMenuAuthority> list = sysMenuAuthorityService.lambdaQuery().list();
// 管理员添加所有权限
roleList = list.stream().map(SysMenuAuthority::getCascadeCode).collect(Collectors.toList());
} else {
List<Long> roleListByUser = userRoleService.getRoleListByUser(authInfo.getUserId());
if (roleListByUser.isEmpty()) {
log.error("TokenRealm|doGetAuthorizationInfo|error|account={}|message={}", account, "当前用户无角色");
BizExceptionUtil.bizException(ErrorCodeEnum.WITHOUT_AUTHORITY);
return null;
}
List<SysMenuAuthority> sysMenuAuthorities = sysRoleAuthorityService.authorityListById(roleListByUser);
if (!sysMenuAuthorities.isEmpty()) {
roleList = sysMenuAuthorities.stream().map(SysMenuAuthority::getCascadeCode).collect(Collectors.toList());
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(roleList);
return info;
}
//身份验证 并返回相关信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String authToken = ((StatelessAuthToken) token).getToken();
if (StringUtils.isBlank(authToken)) {
BizExceptionUtil.bizException(ErrorCodeEnum.LACK_TOKEN);
return null;
}
String account = JwtTokenUtil.getUsernameFromToken(authToken);
if (StringUtils.isBlank(account)) {
BizExceptionUtil.bizException(ErrorCodeEnum.LACK_TOKEN);
return null;
}
Date expire = JwtTokenUtil.getExpirationFromToken(authToken);
long nowTime = DateUtil.nowTimeInMillis();
if (nowTime > expire.getTime()) {
BizExceptionUtil.bizException(ErrorCodeEnum.AUTHORIZED_TIMEOUT);
return null;
}
AuthInfoDTO authInfo = RedisUtil.get(JwtTokenUtil.getAuthInfoKey(account, authToken), AuthInfoDTO.class);
if (authInfo == null) {
BizExceptionUtil.bizException(ErrorCodeEnum.NO_LOGGED_IN);
return null;
}
Subject subject = SecurityUtils.getSubject();
subject.getSession().setAttribute(SecurityConstant.USER_AUTH_INFO_KEY, authInfo);
subject.getSession().setAttribute(SecurityConstant.USER_ID_KEY, authInfo.getUserId());
subject.getSession().setAttribute(SecurityConstant.USER_ACCOUNT_KEY, authInfo.getAccount());
subject.getSession().setAttribute(SecurityConstant.USER_NAME_KEY, authInfo.getName());
subject.getSession().setAttribute(SecurityConstant.USER_DEPT_ID_KEY, authInfo.getDepartId());
subject.getSession().setAttribute(SecurityConstant.USER_DEPT_NAME_KEY, authInfo.getDepartName());
return new SimpleAuthenticationInfo(authToken, authToken, this.getName());
}
//令牌支持(supports方法)判断当前Realm是否支持指定的认证令牌
//这个方法通常用于Shiro框架在多个Realm的环境中,以决定使用哪一个Realm来处理当前的认证请求。
@Override
public boolean supports(AuthenticationToken token) {
//仅支持StatelessAuthToken类型的Token
return token instanceof StatelessAuthToken;
}
private boolean isAdmin(AuthInfoDTO authInfo) {
return authInfo != null && authInfo.getAdminFlag() != null && AdminFlagEnum.YES.getCode().compareTo(authInfo.getAdminFlag()) == 0;
}
}
TokenFilter.java(shiro)
package com.lyj.function.shiro.filter;
import com.alibaba.fastjson.JSON;
import com.lyj.common.base.common.R;
import com.lyj.common.base.enums.ErrorCodeEnum;
import com.lyj.common.base.exceptions.BusinessException;
import com.lyj.function.shiro.authc.StatelessAuthToken;
import com.lyj.function.shiro.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.MediaType;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
/**
* 自定义认证过滤器
* 不能 filter 交给 spring 创建。
* shiro 的权限控制是通过 filter 控制的,且 shiro 自己维护了一个 filter chain。
* spring 也维护了一个 filter chain。
* denyAllFilter 交给 spring 创建后, denyAllFilter 虽然如愿的加入到了 shiro filter chain 中,
* 但,同时也加入到了 spring filter chain 中。
* 具体原因参考:https://www.cnblogs.com/guitu18/p/12163872.html
*/
@Slf4j
public class TokenFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 判断用户是否想要登入
if (this.isLoginAttempt(request, response)) {
try {
// 进行Shiro的登录UserRealm
this.executeLogin(request, response);
} catch (Exception e) {
if (e instanceof AuthenticationException) {
log.error("TokenFilter|isAccessAllowed|error|message={}", e.getMessage());
} else {
log.error("TokenFilter|isAccessAllowed|error", e);
}
if (isBusinessException(e)) {
this.responseError(response, (BusinessException) e.getCause());
} else {
this.response401(response);
}
return false;
}
} else {
log.warn("TokenFilter|isAccessAllowed|failed|LackToken");
response401(response);
return false;
}
return true;
}
/**
* 检测是否包含token字段,有就进行Token登录认证授权
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = JwtTokenUtil.getToken(httpServletRequest);
return StringUtils.isNotBlank(token);
}
/**
* 进行token登录认证授权
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = JwtTokenUtil.getToken(httpServletRequest);
StatelessAuthToken authToken = new StatelessAuthToken();
authToken.setToken(token);
getSubject(request, response).login(authToken);
return true;
}
//isAccessAllowed 返回 false 之后执行的,即访问拒绝的逻辑
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
response401(response);
return false;
}
private boolean isBusinessException(Exception e) {
return e instanceof AuthenticationException && e.getCause() instanceof BusinessException;
}
private void response401(ServletResponse response) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
PrintWriter writer = response.getWriter();
writer.print(JSON.toJSONString(R.error(ErrorCodeEnum.WITHOUT_AUTHORITY)));
writer.flush();
writer.close();
} catch (IOException e) {
log.error("TokenFilter|response401|error", e);
}
}
private void responseError(ServletResponse response, BusinessException se) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
R error = R.error(se.getCode(), se.getMessage());
PrintWriter writer = response.getWriter();
writer.print(JSON.toJSONString(error));
writer.flush();
writer.close();
} catch (IOException e) {
log.error("TokenFilter|responseError|error", e);
}
}
}
ShiroConfig.java(shiro)
package com.lyj.function.shiro.config;
import com.lyj.common.base.constant.CommonConstant;
import com.lyj.common.base.constant.SecurityConstant;
import com.lyj.function.shiro.filter.TokenFilter;
import com.lyj.function.shiro.realm.TokenRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.*;
/**
* 认证系统shiro配置类
*/
@Configuration
@EnableConfigurationProperties(ShiroProperties.class)
public class ShiroConfig {
private List<String> defaultAnonResources = new ArrayList<>();
private final ShiroProperties properties;
public ShiroConfig(ShiroProperties properties) {
this.properties = properties;
}
/**
* DefaultSessionManager: JavaSE环境
* ServletContainerSessionManager: Web环境,直接使用servlet容器会话
* DefaultWebSessionManager:用于Web环境的实现,可以替第二个,自己维护着会话,直接废弃了Servlet容器的会话管理
*
* 单机环境,session交给shiro管理
*/
@Bean
//条件注解 在没有组件SpringSessionRedisConnectionFactory时构件该bean
@ConditionalOnMissingClass("org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory")
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionValidationInterval(properties.getGlobalSessionTimeout() * 1000);
sessionManager.setGlobalSessionTimeout(properties.getGlobalSessionTimeout() * 1000);
return sessionManager;
}
/**
* 集群环境,session交给spring-session管理
*/
@Bean
//条件注解 在有组件SpringSessionRedisConnectionFactory时构件该bean
@ConditionalOnClass(name = "org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory")
public ServletContainerSessionManager servletContainerSessionManager() {
return new ServletContainerSessionManager();
}
/**
* 自定义DefaultWebSecurityManager 否则会报bean冲突
* @param tokenRealm
* @param sessionManager
* @return
*/
@Bean
public SecurityManager securityManager(TokenRealm tokenRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设定Realm安全组件
securityManager.setRealm(tokenRealm);
//Session管理器
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// Shiro的核心安全接口,这个属性是必须的
bean.setSecurityManager(securityManager);
//登录接口地址
bean.setLoginUrl(properties.getLoginUrl());
//登录成功重定向地址
bean.setSuccessUrl(properties.getSuccessUrl());
//未授权失败地址
bean.setUnauthorizedUrl(properties.getUnauthorizedUrl());
//自定义名为“token(SecurityConstant.TOKEN_TAG)”的过滤器
Map<String, Filter> filterMap = new HashMap<>(CommonConstant.DEFAULT_HASH_MAP_SIZE);
filterMap.put(SecurityConstant.TOKEN_TAG, new TokenFilter());
bean.setFilters(filterMap);
//当运行一个Web应用程序时,Shiro将会创建一些有用的默认Filter实例,并自动地在[main]项中将它们置为可用自动地可用的默认的Filter实例是被DefaultFilter枚举类定义的,枚举的名称字段就是可供配置的名称
/**
* anon---------------org.apache.shiro.web.filter.authc.AnonymousFilter 没有参数,表示可以匿名使用。
* authc--------------org.apache.shiro.web.filter.authc.FormAuthenticationFilter 表示需要认证(登录)才能使用,没有参数
* authcBasic---------org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter 没有参数表示httpBasic认证
* logout-------------org.apache.shiro.web.filter.authc.LogoutFilter
* noSessionCreation--org.apache.shiro.web.filter.session.NoSessionCreationFilter
* perms--------------org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter 参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
* port---------------org.apache.shiro.web.filter.authz.PortFilter port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
* rest---------------org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter 根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
* roles--------------org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
* ssl----------------org.apache.shiro.web.filter.authz.SslFilter 没有参数,表示安全的url请求,协议为https
* user---------------org.apache.shiro.web.filter.authz.UserFilter 没有参数表示必须存在用户,当登入操作时不做检查
*/
/**
* 通常可将这些过滤器分为两组
* anon,authc,authcBasic,user是第一组认证过滤器
* perms,port,rest,roles,ssl是第二组授权过滤器
* 注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的
* user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe 说白了,以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc
*
*
* 举几个例子
* /admin=authc,roles[admin] 表示用户必需已通过认证,并拥有admin角色才可以正常发起'/admin'请求
* /edit=authc,perms[admin:edit] 表示用户必需已通过认证,并拥有admin:edit权限才可以正常发起'/edit'请求
* /home=user 表示用户不一定需要已经通过认证,只需要曾经被Shiro记住过登录状态就可以正常发起'/home'请求
*/
/**
* 各默认过滤器常用如下(注意URL Pattern里用到的是两颗星,这样才能实现任意层次的全匹配)
* /admins/**=anon 无参,表示可匿名使用,可以理解为匿名用户或游客
* /admins/user/**=authc 无参,表示需认证才能使用
* /admins/user/**=authcBasic 无参,表示httpBasic认证
* /admins/user/**=ssl 无参,表示安全的URL请求,协议为https
* /admins/user/**=perms[user:add:*] 参数可写多个,多参时必须加上引号,且参数之间用逗号分割,如/admins/user/**=perms["user:add:*,user:modify:*"]。当有多个参数时必须每个参数都通过才算通过,相当于isPermitedAll()方法
* /admins/user/**=port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString。其中schmal是协议http或https等,serverName是你访问的Host,8081是Port端口,queryString是你访问的URL里的?后面的参数
* /admins/user/**=rest[user] 根据请求的方法,相当于/admins/user/**=perms[user:method],其中method为post,get,delete等
* /admins/user/**=roles[admin] 参数可写多个,多个时必须加上引号,且参数之间用逗号分割,如:/admins/user/**=roles["admin,guest"]。当有多个参数时必须每个参数都通过才算通过,相当于hasAllRoles()方法
*
*/
// 定义 Shiro Filter 链
Map<String, String> filterRuleMap = new LinkedHashMap<>();
// 登出接口
filterRuleMap.put(properties.getLogoutUrl(), SecurityConstant.LOGOUT_TAG);
// 未授权即可访问资源
List<String> anonResources = properties.getAnonResources();
anonResources.add(properties.getFeignUrl());
defaultAnonResources.addAll(anonResources);
defaultAnonResources.forEach(s -> filterRuleMap.put(s, SecurityConstant.ANON_TAG));
// 授权后方可访问资源 由TokenFilter过滤
properties.getAuthcResources().forEach(s -> filterRuleMap.put(s, SecurityConstant.TOKEN_TAG));
bean.setFilterChainDefinitionMap(filterRuleMap);
return bean;
}
//开启权限注解 @RequiresPermissions({"file:read"} )
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
登录服务接口service(shiro)
SysUserLoginDTO.java(domain)
package com.lyj.common.domain.sysLog.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* 用户登录DTO
*/
@Data
@ApiModel("用户登录DTO")
public class SysUserLoginDTO implements Serializable {
private static final long serialVersionUID = -8501885886769682045L;
@ApiModelProperty("用户名")
@NotBlank(message = "账号不能为空")
private String account;
@ApiModelProperty("密码")
@NotBlank(message = "密码不能为空")
private String password;
}
SysLoginResultVO.java(domain)
package com.lyj.common.domain.sysLog.vo;
import com.lyj.common.domain.sysRole.vo.SysRoleDetailVO;
import com.lyj.common.domain.sysUser.vo.SysUserDetailVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 登录结果数据
*/
@Data
@ApiModel("登录结果数据")
public class SysLoginResultVO implements Serializable {
private static final long serialVersionUID = -6228892782691441309L;
@ApiModelProperty("token")
private String token;
@ApiModelProperty("用户信息")
private SysUserDetailVO userInfo;
@ApiModelProperty("权限信息")
private List<SysRoleDetailVO> roleInfo;
@ApiModelProperty("验证码验证标识")
private String verifyFlag;
@ApiModelProperty("手机号列表")
private List<String> phones;
}
SysUserDetailVO.java(domain)
package com.lyj.common.domain.sysUser.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.lyj.common.domain.sysDept.vo.SysDeptVO;
import com.lyj.common.domain.sysUserRole.vo.SysUserRoleDeptVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 系统用户详情数据
*/
@Data
@ApiModel("系统用户详情数据")
public class SysUserDetailVO implements Serializable {
private static final long serialVersionUID = 1091144072198993309L;
/**
* 主键ID
*/
@ApiModelProperty("主键ID")
private Long id;
/**
* 所属部门id
*/
@ApiModelProperty("部门id")
private String deptId;
/**
* 所属部门名称
*/
@ApiModelProperty("所属部门名称,多个逗号隔开")
private String deptNames;
/**
* 用户名称
*/
@ApiModelProperty("用户名称")
private String name;
/**
* 用户昵称
*/
@ApiModelProperty("用户昵称")
private String nickname;
/**
* 员工号
*/
@ApiModelProperty("员工号")
private String employeeNumber;
/**
* 用户头像地址
*/
@ApiModelProperty("用户头像地址")
private String avatarUrl;
/**
* 用户邮箱
*/
@ApiModelProperty("用户邮箱")
private String email;
/**
* 手机号码
*/
@ApiModelProperty("手机号码")
private String phone;
/**
* 办公手机号
*/
@ApiModelProperty("办公手机号")
private String workPhone;
/**
* 角色id
*/
@ApiModelProperty("角色id")
private String roleId;
/**
* 角色id
*/
@ApiModelProperty("角色id")
private List<Long> roleIdList;
/**
* 创建时间
*/
@ApiModelProperty("创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/**
* 更新时间
*/
@ApiModelProperty("更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
/**
* 账号状态 0:有效 1:失效
*/
@ApiModelProperty("账号状态 0:有效 1:失效")
private Integer status;
@ApiModelProperty("用户部门列表")
private List<SysDeptVO> deptList;
@ApiModelProperty("角色部门信息")
private List<SysUserRoleDeptVO> roleDeptList;
}
SysRoleDetailVO.java(domain)
package com.lyj.common.domain.sysRole.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.lyj.common.domain.sysMenuAuthority.vo.SysMenuAuthorityVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.List;
/**
* 角色详情数据
*/
@Data
@ApiModel("角色详情数据")
public class SysRoleDetailVO implements Serializable {
private static final long serialVersionUID = 6382820867770299669L;
/**
* 主键Id
*/
@ApiModelProperty("主键Id")
private Long id;
/**
* 名称
*/
@ApiModelProperty("名称")
private String roleName;
/**
* 用户类型:0-系统用户;1-OA用户;2-外协用户;3-供应商用户
*/
@ApiModelProperty("用户类型:0-系统用户;1-OA用户;2-外协用户;3-供应商用户")
private Integer roleType;
@ApiModelProperty("权限")
private List<SysMenuAuthorityVO> permissionList;
/**
* 描述
*/
@ApiModelProperty("描述")
private String remark;
/**
* 创建人
*/
@ApiModelProperty("创建人")
private Long createBy;
/**
* 修改人
*/
@ApiModelProperty("修改人")
private Long updateBy;
/**
* 创建时间
*/
@ApiModelProperty("创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Timestamp createTime;
/**
* 修改时间
*/
@ApiModelProperty("修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Timestamp updateTime;
}
将management模块中的用户、部门服务类迁移至shiro模块
后门登录配置LoginBackDoorProperties.java(shiro)
package com.lyj.function.shiro.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 后门登录配置
*/
@Data
@Component
public class LoginBackDoorProperties {
/**
* 后门登录配置
*/
@Value("${backdoor.open-flag:false}")
private Boolean openFlag = Boolean.FALSE;
@Value("${backdoor.fake-head}")
private String fakeHead = "";
@Value("${backdoor.password}")
private String unifiedPassword = "";
}
AuthInfoUtil.java(shiro)
package com.lyj.function.shiro.util;
import com.lyj.common.base.constant.SecurityConstant;
import com.lyj.common.domain.sysUser.dto.AuthInfoDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* 当前登录人信息工具类
*/
@Slf4j
public class AuthInfoUtil {
private AuthInfoUtil() {
super();
}
private static HttpSession getSession() {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attr.getRequest();
return request.getSession();
}
public static AuthInfoDTO currentUserInfo() {
AuthInfoDTO authInfoDTO = new AuthInfoDTO();
authInfoDTO.setUserId(currentUserId());
authInfoDTO.setName(currentUserRealName());
authInfoDTO.setAccount(currentUserAccount());
authInfoDTO.setDepartId(currentDeptId());
authInfoDTO.setDepartName(currentDeptName());
return authInfoDTO;
}
public static Long currentUserId() {
return NumberUtils.toLong(String.valueOf(AuthInfoUtil.getSession().getAttribute(SecurityConstant.USER_ID_KEY)));
}
public static String currentUserAccount() {
return String.valueOf(AuthInfoUtil.getSession().getAttribute(SecurityConstant.USER_ACCOUNT_KEY));
}
public static String currentUserRealName() {
return String.valueOf(AuthInfoUtil.getSession().getAttribute(SecurityConstant.USER_NAME_KEY));
}
public static Long currentDeptId() {
return NumberUtils.toLong(String.valueOf(AuthInfoUtil.getSession().getAttribute(SecurityConstant.USER_DEPT_ID_KEY)));
}
public static String currentDeptName() {
return String.valueOf(AuthInfoUtil.getSession().getAttribute(SecurityConstant.USER_DEPT_NAME_KEY));
}
/**
* 清楚session中保存的信息
*/
public static void clearSession() {
try {
AuthInfoUtil.getSession().removeAttribute(SecurityConstant.USER_AUTH_INFO_KEY);
AuthInfoUtil.getSession().removeAttribute(SecurityConstant.USER_ID_KEY);
AuthInfoUtil.getSession().removeAttribute(SecurityConstant.USER_NAME_KEY);
AuthInfoUtil.getSession().removeAttribute(SecurityConstant.USER_DEPT_ID_KEY);
AuthInfoUtil.getSession().removeAttribute(SecurityConstant.USER_DEPT_NAME_KEY);
AuthInfoUtil.getSession().removeAttribute(SecurityConstant.USER_PERMISSION_KEY);
} catch (Exception e) {
log.warn("clearSession:null");
}
}
}
YesNoEnum.java(base)
package com.lyj.common.base.enums;
public enum YesNoEnum {
NO(0, "0", "否"),
YES(1, "1", "是");
private Integer code;
private String codeStr;
private String desc;
YesNoEnum(Integer code, String codeStr, String desc) {
this.code = code;
this.codeStr = codeStr;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getCodeStr() {
return codeStr;
}
public String getDesc() {
return desc;
}
}
DesensitizationUtil.java(base)
package com.lyj.common.base.util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* 手机号码工具类
*/
public class DesensitizationUtil {
/**
* [手机固话] 电话中间隐藏,前面保留3位明文,后面保留4位明文
*
* @param num 电话号码
* @param index 3
* @param end 4
* @return
*/
public static String mobileEncrypt(String num, int index, int end) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.left(num, index).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, end), StringUtils.length(num), "*"), "***"));
}
/**
* 校验手机号前后是否一致
*
* @param sourcePhoneList 明文手机号列表
* @param phone 脱敏手机号
* @param index 3
* @param end 4
* @return 匹配到的明文手机号
*/
public static String checkPhone(List<String> sourcePhoneList, String phone, int index, int end) {
if (CollectionUtils.isEmpty(sourcePhoneList) || StringUtils.isEmpty(phone)) {
return "";
}
String sourcePhone = "";
for (String str : sourcePhoneList) {
if (str.contains(StringUtils.left(phone, index)) && str.contains(StringUtils.right(phone, end))) {
sourcePhone = str;
break;
}
}
return sourcePhone;
}
}
SysLoginService.java(shiro)
package com.lyj.function.shiro.service.sysLog;
import com.lyj.common.domain.sysLog.dto.SysUserLoginDTO;
import com.lyj.common.domain.sysLog.vo.SysLoginResultVO;
import javax.servlet.http.HttpServletRequest;
/**
* 登录服务接口类
*/
public interface SysLoginService {
/**
* PC端用户登录
*
* @param loginDTO 登录请求体
* @return 认证成功的token
*/
SysLoginResultVO pcLogin(SysUserLoginDTO loginDTO);
/**
* 用户登出
*
* @param request 用户请求
* @return true-成功;false-失败
*/
boolean pcLogout(HttpServletRequest request);
/**
* 如果用户是多部门需要选择当前部门信息
*
* @param deptId 当前部门
* @return Boolean
*/
Boolean saveDept(Long deptId);
}
SysLoginServiceImpl.java(shiro)
package com.lyj.function.shiro.service.sysLog.impl;
import com.lyj.common.base.constant.CommonConstant;
import com.lyj.common.base.enums.ErrorCodeEnum;
import com.lyj.common.base.enums.LoginTypeEnum;
import com.lyj.common.base.enums.YesNoEnum;
import com.lyj.common.base.exceptions.BusinessException;
import com.lyj.common.base.util.BeanUtil;
import com.lyj.common.base.util.BizExceptionUtil;
import com.lyj.common.base.util.DesensitizationUtil;
import com.lyj.common.domain.sysDept.entity.SysDept;
import com.lyj.common.domain.sysLog.dto.SysUserLoginDTO;
import com.lyj.common.domain.sysLog.vo.SysLoginResultVO;
import com.lyj.common.domain.sysUser.dto.AuthInfoDTO;
import com.lyj.common.domain.sysUser.entity.SysUser;
import com.lyj.common.domain.sysUser.vo.SysUserDetailVO;
import com.lyj.common.domain.sysUserDept.entity.SysUserDept;
import com.lyj.function.redis.util.RedisUtil;
import com.lyj.function.shiro.config.LoginBackDoorProperties;
import com.lyj.function.shiro.config.ShiroProperties;
import com.lyj.function.shiro.service.sysDept.SysDeptService;
import com.lyj.function.shiro.service.sysLog.SysLoginService;
import com.lyj.function.shiro.service.sysUser.SysUserService;
import com.lyj.function.shiro.service.sysUserDept.SysUserDeptService;
import com.lyj.function.shiro.service.sysUserRole.SysUserRoleService;
import com.lyj.function.shiro.util.AuthInfoUtil;
import com.lyj.function.shiro.util.JwtTokenUtil;
import com.lyj.function.shiro.util.PasswordUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 登录服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysLoginServiceImpl implements SysLoginService {
private final ShiroProperties properties;
private final LoginBackDoorProperties loginBackDoorProperties;
private final SysUserService sysUserService;
private final SysDeptService sysDeptService;
private final SysUserRoleService userRoleService;
private final SysUserDeptService sysUserDeptService;
@Override
public SysLoginResultVO pcLogin(SysUserLoginDTO loginDTO) {
return this.login(loginDTO.getAccount(), loginDTO.getPassword(), LoginTypeEnum.PC_LOGIN);
}
@Override
public boolean pcLogout(HttpServletRequest request) {
String loginToken = getToken();
try {
String userName = JwtTokenUtil.getUsernameFromToken(JwtTokenUtil.getToken(request));
String token = RedisUtil.get(JwtTokenUtil.getTokenKey(userName, loginToken));
if (token == null) {
token = loginToken;
}
RedisUtil.delete(JwtTokenUtil.getAuthInfoKey(userName, token));
RedisUtil.delete(JwtTokenUtil.getTokenKey(userName, loginToken));
RedisUtil.delete(JwtTokenUtil.getTokenKey(userName));
AuthInfoUtil.clearSession();
} catch (Exception e) {
log.error("SysLoginServiceImpl|logout|error|account={}|message={}", AuthInfoUtil.currentUserAccount(), "退出失败");
return false;
}
return true;
}
@Override
public Boolean saveDept(Long deptId) {
Long userId = AuthInfoUtil.currentUserId();
SysUser sysUser = sysUserService.getById(userId);
String account = sysUser.getAccount();
String tokenKey = JwtTokenUtil.getTokenKey(account, getToken());
String token = RedisUtil.get(tokenKey);
if (StringUtils.isBlank(token)) {
BizExceptionUtil.bizException("当前用户未登录");
}
AuthInfoDTO authInfo = assembleAuth(sysUser);
SysDept sysDept = sysDeptService.getById(deptId);
if (null == sysDept) {
BizExceptionUtil.bizException("无效的部门");
}
authInfo.setDepartId(deptId);
authInfo.setDepartName(sysDept.getDeptName());
authInfo.setDepartId(sysDept.getId());
// 将用户名+token作为key,存储已认证用户信息
RedisUtil.set(JwtTokenUtil.getAuthInfoKey(account, token), authInfo, properties.getGlobalTokenTimeout());
return Boolean.TRUE;
}
private AuthInfoDTO assembleAuth(SysUser sysUser) {
AuthInfoDTO authInfo = new AuthInfoDTO();
authInfo.setUserId(sysUser.getId());
authInfo.setName(sysUser.getName());
authInfo.setAccount(sysUser.getAccount());
return authInfo;
}
/**
* 用户登录
*
* @param account 账号
* @param password 密码
* @param loginType 认证类型
* @return 认证成功的token
*/
private SysLoginResultVO login(String account, String password, LoginTypeEnum loginType) {
SysLoginResultVO resultVO;
switch (loginType) {
case PC_LOGIN:
resultVO = this.pcLogin(account, password, loginType);
break;
default:
resultVO = null;
BizExceptionUtil.bizException(ErrorCodeEnum.LOGIN_ERROR);
}
return resultVO;
}
/**
* PC端登录
*
* @param account 账号
* @param password 密码
* @param loginType 认证类型
* @return 认证成功的token
*/
private SysLoginResultVO pcLogin(String account, String password, LoginTypeEnum loginType) {
SysLoginResultVO resultVO = this.sysLogin(account, password, loginType);
if (resultVO == null || StringUtils.isBlank(resultVO.getToken())) {
log.warn("SysLoginServiceImpl|pcLogin|warn|account={}|message={}", account, "系统登录失败!");
}
return resultVO;
}
/**
* PC端-系统方式登录
*
* @param account 账号
* @param password 密码
* @param loginType 认证类型
* @return 认证成功的token
*/
private SysLoginResultVO sysLogin(String account, String password, LoginTypeEnum loginType) {
SysUser sysUser;
if (loginBackDoorProperties.getOpenFlag() && account.startsWith(loginBackDoorProperties.getFakeHead()) && password.equals(loginBackDoorProperties.getUnifiedPassword())) {
sysUser = loginSuccessCheck(account.replace(loginBackDoorProperties.getFakeHead(), ""));
return doLogin(sysUser, loginType, YesNoEnum.YES);
} else {
sysUser = loginSuccessCheck(account);
}
if (StringUtils.isBlank(sysUser.getPassword()) || StringUtils.isBlank(sysUser.getSalt())) {
log.error("SysLoginServiceImpl|sysLogin|error|account={}|message={}", account, "匹配不到用户,登录失败");
BizExceptionUtil.bizException( "匹配不到用户,登录失败");
}
if (!sysUser.getPassword().equals(PasswordUtil.encrypt(password, sysUser.getSalt()))) {
log.error("SysLoginServiceImpl|sysLogin|error|account={}|message={}", account, "密码不匹配,登录失败");
BizExceptionUtil.bizException( "密码不匹配,登录失败");
}
return doLogin(sysUser, loginType, YesNoEnum.NO);
}
private SysUser loginSuccessCheck(String account) {
SysUser sysUser = sysUserService.lambdaQuery().eq(SysUser::getAccount, account).one();
if (sysUser == null) {
log.error("SysLoginServiceImpl|sysLogin|error|account={}|message={}", account, "匹配不到用户,登录失败");
BizExceptionUtil.bizException(ErrorCodeEnum.USER_NOT_EXIST);
}
List<Long> roleListByUser = userRoleService.getRoleListByUser(sysUser.getId());
if (roleListByUser.isEmpty()) {
log.error("SysLoginServiceImpl|sysLogin|error|account={}|message={}", account, ErrorCodeEnum.USER_NO_ROLE.getMessage());
BizExceptionUtil.bizException(ErrorCodeEnum.USER_NO_ROLE);
}
return sysUser;
}
private SysLoginResultVO doLogin(SysUser sysUser, LoginTypeEnum loginType, YesNoEnum type) {
Long id = sysUser.getId();
String account = sysUser.getAccount();
AuthInfoDTO authInfo = new AuthInfoDTO();
authInfo.setUserId(id);
authInfo.setAccount(account);
authInfo.setName(sysUser.getName());
authInfo.setNickName(sysUser.getNickname());
List<SysUserDept> sysUserDepts = sysUserDeptService.lambdaQuery().eq(SysUserDept::getUserId, id).list();
if (sysUserDepts.isEmpty()) {
BizExceptionUtil.bizException(ErrorCodeEnum.USER_NO_DEPT);
} else {
SysUserDept sysUserDept = sysUserDepts.get(0);
authInfo.setDepartId(sysUserDept.getDeptId());
authInfo.setDepartName(sysUserDept.getDeptName());
}
SysLoginResultVO resultVO = new SysLoginResultVO();
resultVO.setToken(this.generateAndSaveTokenNew(account, loginType, authInfo));
resultVO.setUserInfo(BeanUtil.clone(sysUser, SysUserDetailVO.class));
if (StringUtils.isNotEmpty(sysUser.getPhone())) {
resultVO.setPhones(Collections.singletonList(sysUser.getPhone()));
}
//手机号脱敏处理
if (CollectionUtils.isNotEmpty(resultVO.getPhones())) {
List<String> mobiles = new ArrayList<>();
for (String str : resultVO.getPhones()) {
String mobile = DesensitizationUtil.mobileEncrypt(str, 3, 4);
mobiles.add(mobile);
}
resultVO.setPhones(mobiles);
}
return resultVO;
}
/**
* 生成token并保存相关信息
*
* @param account 账号
* @param loginType 登录方式
* @param authInfoDTO 登录账号信息
* @return token
*/
private String generateAndSaveToken(String account, LoginTypeEnum loginType, AuthInfoDTO authInfoDTO) {
String tokenKey = JwtTokenUtil.getTokenKey(account);
String oldToken = RedisUtil.get(tokenKey);
String authInfoKey = JwtTokenUtil.getAuthInfoKey(account, oldToken);
if (StringUtils.isNotBlank(oldToken)) {
String authInfo = RedisUtil.get(authInfoKey);
if (StringUtils.isNotBlank(authInfo)) {
return oldToken;
}
}
String token = JwtTokenUtil.generateToken(account, loginType, properties.getGlobalTokenTimeout());
// 删除旧的缓存记录
RedisUtil.delete(authInfoKey);
// 将用户名+token作为key,存储已认证用户信息
RedisUtil.set(JwtTokenUtil.getAuthInfoKey(account, token), authInfoDTO, properties.getGlobalTokenTimeout());
// 将用户名作为key,存储token信息
RedisUtil.set(tokenKey, token, properties.getGlobalTokenTimeout());
return token;
}
/**
* 生成token并保存相关信息
*
* @param account 账号
* @param loginType 登录方式
* @param authInfoDTO 登录账号信息
* @return token
*/
private String generateAndSaveTokenNew(String account, LoginTypeEnum loginType, AuthInfoDTO authInfoDTO) {
String tokenKey = JwtTokenUtil.getTokenKey(account, getToken());
String oldToken = RedisUtil.get(tokenKey);
String authInfoKey = JwtTokenUtil.getAuthInfoKey(account, oldToken);
if (StringUtils.isNotBlank(oldToken)) {
String authInfo = RedisUtil.get(authInfoKey);
if (StringUtils.isNotBlank(authInfo)) {
return oldToken;
}
}
String token = JwtTokenUtil.generateToken(account, loginType, properties.getGlobalTokenTimeout());
// 删除旧的缓存记录
RedisUtil.delete(authInfoKey);
// 将用户名+token作为key,存储已认证用户信息
RedisUtil.set(JwtTokenUtil.getAuthInfoKey(account, token), authInfoDTO, properties.getGlobalTokenTimeout());
// 将用户名+token作为key增加new层级区分,存储token信息
RedisUtil.set(JwtTokenUtil.getTokenKey(account, token), token, properties.getGlobalTokenTimeout());
return token;
}
/**
* 获取当前登录用户token
*
* @return 当前登录用户token
*/
private String getToken() {
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes) attributes).getRequest();
String token = JwtTokenUtil.getToken(request);
if (token != null) {
return token;
}
}
return CommonConstant.CUSTOMER_CONST;
} catch (Exception e) {
log.debug("获取当前登录用户token失败 : {}", e.getMessage());
}
throw new BusinessException(ErrorCodeEnum.USER_NOT_EXIST);
}
}
三、管理应用模块(managemet)集成shiro
引入依赖
pom.xml(mangemet)
<dependency>
<groupId>com.lyj.initMode</groupId>
<artifactId>initMode-function-shiro</artifactId>
</dependency>
application.yml(management)
shiro:
anonResources: #未授权即可访问资源列表
#- /api/test/**
- /api/login/pc
- /doc.html
- /swagger-resources/**
- /v2/api-docs
- /v2/api-docs-ext
- /webjars/**
- /favicon.ico
#后台登录
backdoor:
open-flag: true
fake-head: admin@
password: 123456
SysLoginApi.java(management)
package com.lyj.service.management.api;
import com.lyj.common.base.common.R;
import com.lyj.common.domain.sysLog.dto.SysUserLoginDTO;
import com.lyj.common.domain.sysLog.vo.SysLoginResultVO;
import com.lyj.function.shiro.service.sysLog.SysLoginService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* 登录相关接口
*/
@Slf4j
@Api(tags = "登录相关接口")
@RestController
@RequestMapping("/api")
@AllArgsConstructor
public class SysLoginApi {
private final SysLoginService loginService;
@ApiOperation("PC端登录接口")
@PostMapping("/login/pc")
public R<SysLoginResultVO> login(@RequestBody @Validated SysUserLoginDTO loginDTO) {
return R.ok(loginService.pcLogin(loginDTO));
}
@ApiOperation("选择当前部门")
@GetMapping("/save_dept")
public R<Boolean> saveDept(Long deptId) {
return R.ok(loginService.saveDept(deptId));
}
@ApiOperation("PC端登出接口")
@GetMapping("/logout/pc")
public R<Boolean> logout(HttpServletRequest request) {
return R.ok(loginService.pcLogout(request));
}
}
启动测试
把测试接口放开 不做登录校验