基于Shiro的认证授权

核心架构

在这里插入图片描述

Subject即主体,Subject记录了当前的操作用户信息,外部应用通过Subject向SecurityManager安全管理器进行认证和授权;

  • Principal :用户登录信息
  • Credential:凭证信息,如密码,证书等

SecurityManager即安全管理器,shiro通过SecurityManager来管理内部组件实例,通过来提供安全管理的各种服务;

  • Authenticator :认证管理器
  • Authorizer:授权管理器
  • Realm:域,SecurityManager进行安全认证需要通过Realm域来获取用户认证及权限数据信息(一个SecurityManager安全管理器中可以有多个Realm域)
  • SessionManager:会话管理,用来管理用户登录成功后的会话session
    • sessionDAO:即管理session的DAO接口,比如:保存session,删除session等
  • CacheManager:缓存管理,用于缓存用户认证信息及权限信息,提高系统性能;(可以集成第三方redis作为缓存管理器)

Cryptography即密码管理,提供了一套加密、解密的组件,比如MD5,散列算法等加密方式;


数据库设计

在这里插入图片描述

  • 用户表 sys_user 登录用户基本信息存储表
  • 角色表 sys_role 一组权限相同的人身份统称
  • 资源表 sys_resource 菜单、权限合集
  • 用户-角色关联表 sys_user_role_map 用户(一对多)角色
  • 角色-资源关联表 sys_role_resource_map 角色(一对多)资源

框架集成

pom坐标

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.7.1</version>
</dependency>

 <!-- shiro redis  -->
<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>2.8.24</version>
</dependency>

shiro-redis 组件,提供了shiro的登录会话,认证信息、权限信息的第三方redis的缓存支持(集成后便不用手动对登录会话、认证授权信息进行保存,移除、shiro-redis组件会自动处理)

配置类详解

Shiro-Redis缓存配置
/**
 * Redis链接信息配置
*/
@Bean
public RedisManager redisManager() {
    RedisManager redisManager = new RedisManager();
    redisManager.setHost(host);
    redisManager.setPort(port);
    redisManager.setExpire(1800);
    redisManager.setTimeout(0);
    return redisManager;
}

/**
 * shiro的realm域中认证和授权所支持的缓存管理器
*/
public RedisCacheManager cacheManager() {
    RedisCacheManager redisCacheManager = new RedisCacheManager();
    redisCacheManager.setRedisManager(redisManager());
    redisCacheManager.setKeySerializer(new StringSerializer());
    return redisCacheManager;
}
Realm域
@Bean
public ShiroUserRealm shiroUserRealm() {
    // 创建自定义的 userRealm 对象
    ShiroUserRealm userRealm = new ShiroUserRealm();
    // 设置 userRealm 的 CredentialsMatcher密码校验器
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    // 设置加密算法
    matcher.setHashAlgorithmName("md5");
    // 设置散列次数
    matcher.setHashIterations(6);
    userRealm.setCredentialsMatcher(matcher);
    userRealm.setCacheManager(cacheManager());
    // 开启redis缓存认证
    userRealm.setAuthenticationCachingEnabled(true);
    // 认证信息缓存key前缀
    userRealm.setAuthenticationCacheName("user:authentication");
    // 开启redis缓存授权
    userRealm.setAuthorizationCachingEnabled(true);
    // 权限信息缓存key前缀
    userRealm.setAuthorizationCacheName("user:authorization");
    return userRealm;
}
会话Session配置
 /**
 * redis session 会话的dao层实现 依赖 shiro-redis
 * 提供了一套关于会话的新增,移除等DAO层功能
 */
@Bean
public RedisSessionDAO redisSessionDAO() {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager());
    return redisSessionDAO;
}


/**
 * session 会话管理器 (在SecurityManager中指定会话管理器即可)
 */
@Bean
public SessionManager sessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionDAO(redisSessionDAO());
    // 配置会话监听
    Collection<SessionListener> listeners = new ArrayList<>();
    sessionManager.setSessionListeners(listeners);
    return sessionManager;
}
SecurityManager安全管理器配置

@Bean
public DefaultSecurityManager defaultSecurityManager(){
    DefaultSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(userRealm());
    defaultSecurityManager.setSessionManager(sessionManager());
    return defaultSecurityManager;
}

@Bean
public DefaultSecurityManager defaultSecurityManager(){
    DefaultSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
    defaultSecurityManager.setRealm(userRealm());
    defaultSecurityManager.setSessionManager(sessionManager());
    return defaultSecurityManager;
}


配置Realm域
  • 可以通过**DefaultWebSecurityManager**有参构造方法来设置Realm域(单个/多个)
    在这里插入图片描述

  • 也可以通过调用setRealm/setRealms方法来设置realm域
    在这里插入图片描述

Shiro自定义拦截器过滤链规则
 @Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultSecurityManager defaultSecurityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(defaultSecurityManager);
    /*
     * 设置Shiro 内置过滤器
     * anon: 无需认证(登陆)可以访问
     * authc: 必须认证才可以访问
     * user: 如果使用 rememberMe 的功能可以直接访问
     * perms: 该资源必须得到资源权限才可以访问
     * role: 该资源必须得到角色权限才可以访问
     */
    Map<String, String> filterMap = new LinkedHashMap<>();
    // 登录接口放行
    filterMap.put("/login", "anon");
    filterMap.put("/logout", "anon");
    filterMap.put("/send/email/*", "anon");
    /*
     * 查询当前系统所有的菜单权限,全部加入shiro进行权限认证
     * 比如:
     * /user/add : perms[user:add]
     * /log/view : perms[user:view]
     */
    List<SysResource> sysResources = sysResourceService.listResource();
    for (SysResource sysResource : sysResources) {
        // 菜单url:权限
        filterMap.put(sysResource.getRequestUrl(), "perms[" + sysResource.getPermission() + "]");
    }
    // 其余所有的接口都需要经过认证
    filterMap.put("/**", "authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
    // 未登录
    shiroFilterFactoryBean.setLoginUrl("/unLogin");
    // 未授权
    shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
    return shiroFilterFactoryBean;
}

Realm域

/**
 * 用户名、密码权限认证域
 * <p>
 * 在使用shiro-redis缓存时,
 * 1.登录成功:
 * - 开始保存缓存认证信息
 * - key :AuthenticationToken = UserNamePasswordToken.username 也就是用户名
 * - value :new SimpleAuthenticationInfo的第一个参数值(用户详细信息)
 * 2.退出登录:
 * - AuthenticatingRealm.clearCachedAuthenticationInfo()中清除用户认证缓存信息
 * - Object key = getAuthenticationCacheKey(principals); // key 取的是 new SimpleAuthenticationInfo的第一个参数
 * - 与我们缓存的认证信息key不一致,此时需要重写getAvailablePrincipal()方法来指定我们删除的key是什么
 *
 * </p>
 *
 * @author Sky
 * @date 2022/4/15
 */
public class ShiroUserRealm extends AuthorizingRealm {

    @Resource
    private ISysUserService sysUserService;
    @Resource
    private ISysRoleService sysRoleService;
    @Resource
    private ISysResourceService sysResourceService;


    /**
     * 授权
     *
     * @param principals 凭证
     * @return {@link AuthorizationInfo}
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 用户权限信息
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 获取当前用户的信息
        SysUser sysUser = (SysUser) SecurityUtils.getSubject().getPrincipal();
        // 初始化授权信息
        authorizationInfo(authorizationInfo, sysUser.getId(), Constants.SUPER_ADMIN.equals(sysUser.getSuperAdmin()), sysRoleService, sysResourceService);
        return authorizationInfo;
    }

    /**
     * 身份验证
     *
     * @param token 令牌
     * @return {@link AuthenticationInfo}
     * @throws AuthenticationException 身份验证异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken userDetail = (UsernamePasswordToken) token;
        // 根据用户名获取系统用户
        SysUser sysUser = sysUserService.getUserByUsername(userDetail.getUsername());
        if (sysUser == null) {
            // 用户不存在
            throw new UnknownAccountException();
        } else if (!sysUser.getEnabledFlag()) {
            // 账号被禁用
            throw new DisabledAccountException();
        }
        // 用户的密码,为了数据安全,密码不做缓存
        String password = sysUser.getPassword();
        sysUser.setPassword(null);
        return new SimpleAuthenticationInfo(sysUser,
                password,
                new CustomByteSource(sysUser.getSalt()),
                this.getName());
    }

    /**
     * 多个Realm域时,具体执行那个Realm通过此条件判断
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }
    
   /**
     * 解决退出登录时,当主体为对象,删除key与缓存key不一致问题
     * 指定缓存key
     *
     * @param principals 主要凭证 new SimpleAuthenticationInfo 第一个参数
     * @return 用户名 username
     */
    @Override
    protected Object getAvailablePrincipal(PrincipalCollection principals) {
        return ((SysUser) principals.getPrimaryPrincipal()).getUsername();
    }

   /**
     * 授权信息
     * 超级管理员拥有全部角色全部资源权限
     *
     * @param authorizationInfo 授权信息
     * @param userId            用户id
     * @param superAdmin        超级管理员
     */
    protected void authorizationInfo(SimpleAuthorizationInfo authorizationInfo, Long userId, Boolean superAdmin, ISysRoleService sysRoleService, ISysResourceService sysResourceService) {
        // 用户角色信息
        List<SysRole> roles = superAdmin ? sysRoleService.listRole() : sysRoleService.listRoleByUserId(userId);
        roles.forEach(e -> authorizationInfo.addRole(e.getRoleCode()));
        // 用户资源信息
        List<SysResource> sysResources = superAdmin ? sysResourceService.listResource() : sysResourceService.listResourceByUserId(userId);
        sysResources.forEach(e -> authorizationInfo.addStringPermission(e.getPermission()));
    }
}

认证时几种错误状态:

  • UnknownAccountException:用户名错误
  • IncorrectCredentialsException:密码错误
  • DisabledAccountException:账号被禁用
  • LockedAccountException:账号被锁定
  • ExcessiveAttemptsException:登录失败次数过多
  • ExpiredCredentialsException:凭证过期

Shiro工具类

  public class ShiroUtils {
      
    /**
     * 加密方式
     */
    public static final String ENCRYPTION_TYPE = "md5";

    /**
     * 加密次数
     */
    public static final Integer ENCRYPTION_NUM = 6;

    private ShiroUtils() {
    }


    /**
     * 生成加密后的密码
     *
     * @param password 密码
     * @param salt     盐
     * @return md5 散列6次的加密密码
     */
    public static String generate(String password, String salt) {
        CustomByteSource byteSalt = new CustomByteSource(salt);
        return new SimpleHash(ENCRYPTION_TYPE, password, byteSalt, ENCRYPTION_NUM).toString();
    }


    /**
     * 获得当前系统用户
     *
     * @return {@link SysUser}
     */
    public static SysUser getCurrentSysUser() {
        return (SysUser) SecurityUtils.getSubject().getPrincipal();
    }

    /**
     * 获取当前主体
     *
     * @return Subject
     */
    public static Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    /**
     * 是否登录
     *
     * @return true登录false未登录
     */
    public static boolean isLogin() {
        Subject subject = getSubject();
        if (subject != null) {
            // 登录状态下
            return subject.isAuthenticated();
        }
        return Boolean.FALSE;
    }

    /**
     * 注销
     */
    public static void logout() {
        getSubject().logout();
    }

  }

解决使用盐加密后缓存无序列化

异常信息:java.io.NotSerializableException:org.apache.shiro.util.SimpleByteSource

public class CustomByteSource implements ByteSource, Serializable {

    private static final long serialVersionUID = 1L;

    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public CustomByteSource() {
    }

    public CustomByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public CustomByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public CustomByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public CustomByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public CustomByteSource(File file) {
        this.bytes = (new CustomByteSource.BytesHelper()).getBytes(file);
    }

    public CustomByteSource(InputStream stream) {
        this.bytes = (new CustomByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public void setBytes(byte[] bytes) {
        this.bytes = bytes;
    }

    @Override
    public byte[] getBytes() {
        return this.bytes;
    }

    @Override
    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }
        return this.cachedHex;
    }

    @Override
    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    @Override
    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    @Override
    public String toString() {
        return this.toBase64();
    }

    @Override
    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource) o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值