基于Shiro的认证授权

核心架构

基于Shiro的认证授权_第1张图片

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,散列算法等加密方式;


数据库设计

基于Shiro的认证授权_第2张图片

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

框架集成

pom坐标

<dependency>
  <groupId>org.apache.shirogroupId>
  <artifactId>shiro-springartifactId>
  <version>1.7.1version>
dependency>

 
<dependency>
    <groupId>org.crazycakegroupId>
    <artifactId>shiro-redisartifactId>
    <version>2.8.24version>
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域(单个/多个)
    基于Shiro的认证授权_第3张图片

  • 也可以通过调用setRealm/setRealms方法来设置realm域
    基于Shiro的认证授权_第4张图片

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域

/**
 * 用户名、密码权限认证域
 * 

* 在使用shiro-redis缓存时, * 1.登录成功: * - 开始保存缓存认证信息 * - key :AuthenticationToken = UserNamePasswordToken.username 也就是用户名 * - value :new SimpleAuthenticationInfo的第一个参数值(用户详细信息) * 2.退出登录: * - AuthenticatingRealm.clearCachedAuthenticationInfo()中清除用户认证缓存信息 * - Object key = getAuthenticationCacheKey(principals); // key 取的是 new SimpleAuthenticationInfo的第一个参数 * - 与我们缓存的认证信息key不一致,此时需要重写getAvailablePrincipal()方法来指定我们删除的key是什么 * *

* * @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);
        }
    }
}

你可能感兴趣的:(三,集成框架,java,数据库,服务器)