Java-Shiro入门学习
安全框架
什么是安全框架
安全框架通常包含:执行身份验证、授权、密码和会话管理等
什么是身份验证
身份验证(authentication),用户使用username和password登录认证的过程
什么是授权
授权(authorization),用户身份验证通过后,系统获取用户对应的权限信息进行相关的访问权限控制
RBAC介绍
RBAC是什么
RBAC 是基于角色的访问控制(Role-Based Access Control
)在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便
RBAC介绍
RBAC 认为授权实际上是Who
、What
、How
三元组之间的关系,也就是Who
对What
进行How
的操作,也就是“主体”对“客体”的操作
Who:是权限的拥有者或主体(如:User,Role)
What:是操作或对象(operation,object)
How:具体的权限(Privilege,正向授权与负向授权)
授权方式
-
基于角色的访问控制
-
RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
if (subject.hasRole("admin")){ // 操作资源 }
-
-
基于资源的访问控制
-
RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
// user:find:* 查询用户下的所有资源 // user:*:01 user实例下的01用户具有任意权限 // user:update:01 user实例下的01用户具有更新权限 if (subject.isPermission("user:*:create")){ // 对所有的用户具有创建权限 } if (subject.isPermission("user:update:01")){ // 资源实例 // } if (subject.isPermission("user:update:*")){ // 资源类型 // }
-
数据库设计
包含:用户表(users)、角色表(roles)、权限表(permissions)、用户与角色多对多的表(user_roles)、角色表与权限多对多的表(roles_permissions)
数据库具体实现
数据库规约参考:Java开发手册(嵩山版).pdf
表名不使用复数名词
create_time自动创建、update_time自动更新
id的bigint已经有长度(8字节)了,在mysql建表中的length,只是用于显示的位数,存储空间不变。
bigint 带符号的范围是-9223372036854775808到9223372036854775807。无符号的范围是0到18446744073709551615。
int 普通大小的整数。带符号的范围是-2147483648到2147483647。无符号的范围是0到4294967295。
UNSIGNED:代表无符号
-- ----------------------------
-- Table structure for user
-- password_salt是每个用户的随机盐,实现password加密算法:hash+salt+散列的md5算法
-- 实现明文密码一样,加密后的password也是不一样的,增加了社工破解的难度
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_time` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password_salt` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_time` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_name`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_time` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源路径',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_name`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_role
-- users和roles的多对多关联表
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_time` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`userid` bigint(20) UNSIGNED NOT NULL,
`roleid` bigint(20) UNSIGNED NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_userid`(`userid`) USING BTREE,
INDEX `idx_roleid`(`roleid`) USING BTREE,
UNIQUE INDEX `uk_userid_roleid`(`userid`, `roleid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for role_permission
-- roles和permissions的多对多关联表
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`update_time` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0),
`roleid` bigint(20) UNSIGNED NOT NULL,
`permissionid` bigint(20) UNSIGNED NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_roleid`(`roleid`) USING BTREE,
INDEX `idx_permissionid`(`permissionid`) USING BTREE,
UNIQUE INDEX `uk_roleid_permissionid`(`roleid`, `permissionid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
测试数据
-- 密码:123
INSERT INTO `user` (username,password,password_salt) VALUES ('zaza', '59f582a8cf5103c1a7d20b464a24c049', 'HIF4JYj*');
INSERT INTO `user` (username,password,password_salt) VALUES ('yaya', '240b4b724a129d3a49a53504d0e93b24', 'uKqNOihg');
-- 角色
INSERT INTO `role` (name) VALUES ('admin');
INSERT INTO `role` (name) VALUES ('user');
INSERT INTO `role` (name) VALUES ('order');
-- 资源权限
INSERT INTO `permission` (name,url) VALUES ('user:*', NULL);
INSERT INTO `permission` (name,url) VALUES ('order:get', NULL);
INSERT INTO `permission` (name,url) VALUES ('order:*:01', NULL);
-- 用户角色多对多
-- zaza 角色:admin
-- yaya 角色:user,order
INSERT INTO `user_role` (userid,roleid) VALUES (1, 1);
INSERT INTO `user_role` (userid,roleid) VALUES (2, 2);
INSERT INTO `user_role` (userid,roleid) VALUES (2, 3);
-- 角色资源多对多
-- 角色admin的资源权限:'user:*'
-- 角色user的资源权限:'order:get'
-- 角色order的资源权限:'order:*:01'
INSERT INTO `role_permission` (roleid,permissionid) VALUES (1, 1);
INSERT INTO `role_permission` (roleid,permissionid) VALUES (2, 2);
INSERT INTO `role_permission` (roleid,permissionid) VALUES (3, 3);
数据库查询
-- 查询用户的角色信息
SELECT
u.id uid,
u.username,
r.id rid,
r.`name` rname
FROM
user u
LEFT JOIN user_role ur ON u.id = ur.userid
LEFT JOIN role r ON ur.roleid = r.id
WHERE
u.username = 'yaya';
-- 查询角色的权限信息
SELECT
p.id,
p.`name`,
p.url,
r.name
FROM
role r
LEFT JOIN role_permission rp ON r.id = rp.roleid
LEFT JOIN permission p ON rp.permissionid = p.id
WHERE
r.id = 1;
Shiro介绍
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
Shiro架构
https://shiro.apache.org/get-started.html
Shiro示例
需要配置文件:shiro.ini
public class TesIniShiro {
public static void main(String[] args) {
// 1、创建安全管理器对象
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 2、创建 iniRealm 实例
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
// IniRealm iniRealm1 = new IniRealm("classpath:shiro-jdbc-realm.ini");
// 3、给安全管理器设置realm
defaultSecurityManager.setRealm(iniRealm);
// 引用自定义的 Realm
// defaultSecurityManager.setRealm(new DbRealm());
// 4、给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 5、通过安全管理工具,得到subject
Subject subject = SecurityUtils.getSubject();
// 6、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
// 7、登录,即身份验证,对应:Authentication
try {
// 登录,即身份验证
subject.login(token);
} catch (UnknownAccountException uae) {
System.out.println("用户不存在:" + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
System.out.println("密码错误:" + token.getPrincipal());
} catch (LockedAccountException lae) {
System.out.println("账号禁用:" + token.getPrincipal());
} catch (AuthenticationException ae) {
throw ae;
}
System.out.println("登录成功:" + subject.getPrincipal());
// 授权验证:Authorization
// Realm realm;
// 退出
subject.logout();
}
}
Shiro与Spring整合核心
每个请求进来时,Shiro作为拦截器(ShiroFilter)进行拦截:认证和授权
ShiroFilter->SecurityManager->Realm->Mybatis->jdbc->数据库
- 实现自定义Realm,通过数据库查询先关信息
- 创建Bean:getRealm->DefaultWebSecurityManager->shiroFilterFactoryBean
JWT是什么
JSON Web Tokens,主要用于单点登录,系统之间信息交换
用于前后端分离系统,集群系统访问
用户认证通过后,后端生成令牌,返回给前端,令牌由客户端保存
格式:Header.Payload.Signature
Payload:不要存放敏感信息,如密码
Signature:基于Header和Payload数据生成的签名,HMACSHA256(Header, Payload, sign),sign存在于服务器的秘钥
JWT示例
maven: com.auth0 / java-jwt / 3.3.0
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
public class Test {
public static void main(String[] args) {
String secret = "KFbNAYVaLHgHiedIbVDk";
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 90);
// 生成令牌
String token = JWT.create()
.withClaim("username", "zaza") // 设置自定义用户名
.withExpiresAt(instance.getTime()) // 设置过期时间
.sign(Algorithm.HMAC256(secret)); // 设置私钥,保密,密码需要复杂
System.out.println(token);
// 创建验证令牌对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
// 验证通过后,生成解码后的DecodedJWT对象
DecodedJWT decodedJWT = jwtVerifier.verify(token);
System.out.println("用户名:" + decodedJWT.getClaim("username").asString());
System.out.println("过期时间:" + decodedJWT.getExpiresAt());
}
}
Shiro和JWT整合
Shiro和JWT执行流程
- 通过登录接口认证用户,登录成功则返回token,这里走的是UsernamePasswordToken的UserRealm(实现缓存)
- 其它接口通过header携带token,JWTFilter实现过滤,并使用JWTToken的JWTRealm
- ShiroConfig同时配置两个realms
禁用session
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
常见过滤器
shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置过滤指定url的访问权限。
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问 |
authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https请求才能访问 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
自定义JWTToken
参考:UsernamePasswordToken实现了HostAuthenticationToken, RememberMeAuthenticationToken HostAuthenticationToken扩展了AuthenticationToken,所以JWTToken实现AuthenticationToken即可
UsernamePasswordToken实现了默认参数处理,可以学习一下
// UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
// JWTToken只有token字符串,所以只返回字符串即可?
public class JWTToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
// Key
private final String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return JWTUtils.getUsername(token);
}
@Override
public Object getCredentials() {
return token;
}
}
Filter执行流程
执行流程:preHandle->
isAccessAllowed->
isLoginAttempt->
executeLogin
自定义的JWTFilter
参考:FormAuthenticationFilter
FormAuthenticationFilter -> AuthenticatingFilter
BasicHttpAuthenticationFilter->HttpAuthenticationFilter->AuthenticatingFilter
继承 AuthenticatingFilter 需要实现:createToken、onAccessDenied 继承 BasicHttpAuthenticationFilter 不需要实现:createToken、onAccessDenied
public class JWTFilter extends BasicHttpAuthenticationFilter {
......
}
调用自定义的JWTFilter
// map.put("/**", "authc"); 这个代表调用自带的过滤链:authc
// 添加自己的过滤器并且取名为jwt
LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", jwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 过滤链定义,从上向下顺序执行,一般将放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
多realm加载
public class ShiroConfig {
// 这个声明集合后,自动加载声明的所有Realm
public DefaultWebSecurityManager defaultWebSecurityManager(Collection<Realm> realms) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 给安全管理器设置 Realm
defaultWebSecurityManager.setRealms(realms);
......
}
@Bean
// 名字没有影响,实际上类型更重要,通过类型来发现Realm
public Realm userRealm() {
UserRealm userRealm = new UserRealm();
......
return userRealm;
}
@Bean
// 名字没有影响,实际上类型更重要,通过类型来发现Realm
public Realm JWTRealm() {
// JWTRealm
JWTRealm jwtRealm = new JWTRealm();
......
return jwtRealm;
}
}
JWTToken刷新生命周期
实现: 用户在线操作不掉线功能
- 登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
- 当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
- 当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算 (这里只是处理超时的token刷新,会不会将风险降低?)
- 当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。 用户过期时间 = Jwt有效时间 * 2。
参考档
- 【编程不良人】2020最新版Shiro教程,整合SpringBoot项目实战教程
- 【编程不良人】完整代码
- 多realm处理
- springboot整合shiro多验证登录功能的实现
- Shiro整合JWT:解决jwt注销和续签的问题-非常不错
- https://developer.aliyun.com/article/206244
- https://programmer.group/spring-boot-plus-integrates-springboot-shiro-jwt-privilege-management.html
- shiro-jwt-整合
- http://www.andrew-programming.com/2019/01/23/springboot-integrate-with-jwt-and-apache-shiro/
- 原文作者:zaza
- 原文链接:https://zazayaya.github.io/2022/03/15/java-shiro-started.html
- 说明:转载本站文章请标明出处,部分资源来源于网络,如有侵权请及时与我联系!