安全框架

什么是安全框架

安全框架通常包含:执行身份验证、授权、密码和会话管理等

什么是身份验证

身份验证(authentication),用户使用username和password登录认证的过程

什么是授权

授权(authorization),用户身份验证通过后,系统获取用户对应的权限信息进行相关的访问权限控制

RBAC介绍

RBAC是什么

RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便

RBAC介绍

RBAC 认为授权实际上是WhoWhatHow 三元组之间的关系,也就是WhoWhat 进行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->数据库

代码参考:【编程不良人】Shiro整合SpringBoot

  1. 实现自定义Realm,通过数据库查询先关信息
  2. 创建Bean:getRealm->DefaultWebSecurityManager->shiroFilterFactoryBean

JWT是什么

https://jwt.io/

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执行流程

  1. 通过登录接口认证用户,登录成功则返回token,这里走的是UsernamePasswordToken的UserRealm(实现缓存)
  2. 其它接口通过header携带token,JWTFilter实现过滤,并使用JWTToken的JWTRealm
  3. 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刷新生命周期

实现: 用户在线操作不掉线功能

  1. 登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
  2. 当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
  3. 当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算 (这里只是处理超时的token刷新,会不会将风险降低?)
  4. 当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。 用户过期时间 = Jwt有效时间 * 2。

参考档