shiro的整合“心路历程”

shiro的整合“心路历程”

1.准备数据

用户-角色-权限 RBAC模型

用户角色权限
luo 用户管理员 对后台用户的CRU
zhou 仓库管理员 对仓库数据的CRU
admin 超级管理员 所有库中的权限

业务描述:

当用户访问首页时,尽请访问
当用户查看用户列表时,需要登录、需要有该权限
当用户查看仓库列表时,需要有仓库权限
当用户删除用户时,需要有超级管理员角色

 

2.springboot项目

2.1 引入依赖

2.2 pojo

2.3 DAO

是用mybatis plus
https://mp.baomidou.com/guide/
spring:
datasource:
  url: jdbc:mysql:///shiro_perm?characterEncoding=utf8&serverTimezone=Asia/Shanghai
  username: root
  password: root
  driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
  map-underscore-to-camel-case: true

接口继承BaseMapper<T>

public interface AdministratorMapper extends BaseMapper<Administrator> {
}

pojo添加注解

@Data
@TableName("tb_admin")
public class Administrator implements Serializable {

   @TableId(type = IdType.AUTO)
   private Integer id;

   private String username;

   private String password;

   private String realname;

   private String gender;

   private String privateSalt; //私有盐,用户密码加密

   private String tel;

   private String userStatus;

   @TableField(exist = false)
   private List<Role> roleList;
}

引导类添加扫描

@MapperScan("com.itheima.shiro.mapper")

2.4 service

public interface AdminService {
}

@Service
@Transactional
public class AdminServiceImpl implements AdminService {
   @Autowired
   private AdministratorMapper adminMapper;
}

controller

省略...

视图

<!--使用thymeleaf 首先完成一个登陆页面-->
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org/">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
    <!--<h5 th:text="${err_msg}"></h5>-->
    <form action="/backend/login" method="post">
        <input name="username"/><br>
        <input name="password"/><br>
        <input type="submit" value="登录"/>
    </form>
</body>
</html>

 

3.shiro配置

3.1 用户访问路径测试

需求:用户未登录时,访问/user/all路径,告诉用户调到登录页面

添加shiro配置:安全管理器、realm、shiroFilter

@Configuration
public class ShiroConfig {

    //0.配置shiroFilter
    @Bean
    public ShiroFilterFactoryBean shiroFilter(){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        shiroFilterFactoryBean.setLoginUrl("/backend/toLogin");
        Map filterChainMap = new LinkedHashMap<String,String>();
        filterChainMap.put("/backend/toLogin","anon"); //跳转登录页面放行
        filterChainMap.put("/backend/login","anon"); //登录请求 放行
        filterChainMap.put("/**","authc"); //认证
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        return shiroFilterFactoryBean;
    }

    //1.配置安全管理器
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    //2.配置realm
    @Bean
    public Realm myShiroRealm(){
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        return myShiroRealm;
    }
}

 

4.认证(登录)

需求:用户新增时,密码进行加密(md5+随机盐加密): MD5(明文密码+随机salt)

用户创建

public void saveAdmin(Administrator admin) {
    String password = admin.getPassword();
    String salt = RandomStringUtils.randomNumeric(6,8);
    admin.setPrivateSalt(salt);
    Md5Hash md5Hash = new Md5Hash(password,salt); //模拟md5加密一次
    admin.setPassword(md5Hash.toString());
    admin.setUserStatus("1");
    adminMapper.insert(admin);
}

登录配置、测试、访问

@RequestMapping("/login")
public String login(@RequestParam String username, @RequestParam String password){
    //登录
    try{
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        subject.login(token);
    }catch (Exception e){
        e.printStackTrace();
    }
    return "success";
}

配置、开发realm

//realm需要密码匹配器设置
public CredentialsMatcher myMd5Matcher(){
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    matcher.setHashAlgorithmName("md5");
    matcher.setHashIterations(1);
    return matcher;
}

realm的认证信息完善:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("经过认证用户的获取");
    UsernamePasswordToken loginToken = (UsernamePasswordToken)token;
    String username = loginToken.getUsername();
    //根据用户名查询用户
    Administrator admin = adminService.findAdminByUsername(username);
    if(admin == null){
        return null; //框架自动抛出位置账户异常
    }else{
        ByteSource saltBS = new SimpleByteSource(admin.getPrivateSalt());
        return new SimpleAuthenticationInfo(admin,admin.getPassword(),saltBS,getName());
    }
}

退出

filterChainMap.put("/backend/logout","logout");
//也可以准备一个controller方法,使用Subject的方法进行退出
Subject subject = SecurityUtils.getSubject();
subject.logout();

 

5.授权

当用户查看用户列表时,需要登录、需要有该权限
filterChainMap.put("/user/all","perms[user:select]"); //查询所有用户 需要认证(登录)
//当用户查看仓库列表时,需要有仓库权限
filterChainMap.put("/storage/all","perms[storage:select]");
//当用户删除用户时,需要有超级管理员角色
filterChainMap.put("/user/del/*","roles[role_superman]");

 

权限控制:角色、权限

filterChainMap.put("/user/all","perms[user:select]"); //需要权限 user:select
filterChainMap.put("/user/*","roles[role_user]"); //需要角色 role_user

赋权:

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    System.out.println("经过权限获取");
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    //从数据库查询该用户的权限列表
    Administrator principal = (Administrator) principals.getPrimaryPrincipal();
    String password = principal.getPassword();
    simpleAuthorizationInfo.addStringPermission("user:select"); //为当前登录用户主体赋权
    return simpleAuthorizationInfo;
}

数据库数据赋权:

private void addPerms(String username,SimpleAuthorizationInfo simpleAuthorizationInfo){
    Set<String> roleSet = adminService.findRolesByUsername(username);
    if(roleSet != null && roleSet.size() >0){
        simpleAuthorizationInfo.addRoles(roleSet);
    }
    Set<String> permissionSet = adminService.findPermissionsByUsername(username);
    if(permissionSet != null && permissionSet.size() >0){
        simpleAuthorizationInfo.addStringPermissions(permissionSet);
    }
}

6.注解权限控制

@RequiresPermissions("page:storage")
@RequiresRoles("role_superman")

只是用注解是不生效的,需要添加配置

/**
     * 注解支持:
     */
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
    defaultAAP.setProxyTargetClass(true);
    return defaultAAP;
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    return authorizationAttributeSourceAdvisor;
}

7.页面标签权限控制

需要引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

配置标签支持

@Bean
public ShiroDialect shiroDialect(){
    return new ShiroDialect();
}

在页面中使用标签

<shiro:principal property="username"></shiro:principal>

 

8.会话管理(redis)

自定义会话管理器

@Bean
public SessionManager sessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionDAO(redisSessionDAO());

    //设置会话过期时间
    sessionManager.setGlobalSessionTimeout(3*60*1000); //默认半小时
    sessionManager.setDeleteInvalidSessions(true); //默认自定调用SessionDAO的delete方法删除会话
    //设置会话定时检查
    //        sessionManager.setSessionValidationInterval(180000); //默认一小时
    //        sessionManager.setSessionValidationSchedulerEnabled(true);
    return sessionManager;
}
@Bean
public SessionDAO redisSessionDAO(){
    ShiroRedisSessionDao redisDAO = new ShiroRedisSessionDao();
    return redisDAO;
}

 

自定义CachingSessionDao

public class ShiroRedisSessionDao extends CachingSessionDAO {

    public static final String SHIRO_SESSION_KEY = "shiro_session_key";

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    protected void doUpdate(Session session) {
        this.saveSession(session);
    }

    @Override
    protected void doDelete(Session session) {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return ;
        }
        //根据session id删除session
        redisTemplate.boundHashOps(SHIRO_SESSION_KEY).delete(session.getId());
    }


    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        this.saveSession(session);
        return sessionId;
    }


    @Override
    protected Session doReadSession(Serializable sessionId) {
        if(sessionId == null){
            logger.error("传入的 session id is null");
            return null;
        }
        return (Session)redisTemplate.boundHashOps(SHIRO_SESSION_KEY).get(sessionId);
    }

    /**
     * 将session 保存进redis 中
     * @param session 要保存的session
     */
    private void saveSession(Session session) {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return ;
        }
        redisTemplate.boundHashOps(SHIRO_SESSION_KEY).put(session.getId(),session);
    }
}

交给安全管理器

@Bean
public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setSessionManager(sessionManager());
    securityManager.setRealm(myRealm());
    return securityManager;
}

 

9.缓存管理(redis)

每次访问带有权限相关的判断的请求时,都会执行doGetAuthorizationInfo()方法
可以缓存授权权限信息,不需要每次都查询数据库赋权
其实,shiro默认支持的缓存是ehcache(java语言开发的本地缓存技术,依赖jvm)

自定义缓存管理器

public class MyRedisCacheManager implements CacheManager {
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        return new ShiroRedisCache(name,redisTemplate);
    }
}

自定义redis缓存

package com.itheima.shiroConfig;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 *
 */
public class ShiroRedisCache<K, V> implements Cache<K, V> {
    private static Logger LOGGER = LogManager.getLogger(ShiroRedisCache.class);

    /**
     * key前缀
     */
    private static final String REDIS_SHIRO_CACHE_KEY_PREFIX = "shiro_cache_key_";

    /**
     * cache name
     */
    private String name;

    /**
     * jedis 连接工厂
     */

    private RedisTemplate redisTemplate;

    /**
     * 序列化工具
     */
    private RedisSerializer serializer = new JdkSerializationRedisSerializer();

    /**
     * 存储key的redis.list的key值
     */
    private String keyListKey;

    private RedisConnection getConnection(){
        return this.redisTemplate.getConnectionFactory().getConnection();
    }

    public ShiroRedisCache(String name,RedisTemplate redisTemplate) {
        this.name = name;
        this.redisTemplate = redisTemplate;
        this.keyListKey = REDIS_SHIRO_CACHE_KEY_PREFIX + name;
    }

    @Override
    public V get(K key) throws CacheException {
        LOGGER.debug("shiro redis cache get.{} K={}", name, key);
        RedisConnection redisConnection = null;
        V result = null;
        try {
            redisConnection = getConnection();
            result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key))));
        } catch (Exception e) {
            LOGGER.error("shiro redis cache get exception. ", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
        return result;
    }

    @Override
    public V put(K key, V value) throws CacheException {
        LOGGER.debug("shiro redis cache put.{} K={} V={}", name, key, value);
        RedisConnection redisConnection = null;
        V result = null;
        try {
            redisConnection = getConnection();
            result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key))));

            redisConnection.set(serializer.serialize(generateKey(key)), serializer.serialize(value));

            redisConnection.lPush(serializer.serialize(keyListKey), serializer.serialize(generateKey(key)));
        } catch (Exception e) {
            LOGGER.error("shiro redis cache put exception. ", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
        return result;
    }

    @Override
    public V remove(K key) throws CacheException {
        LOGGER.debug("shiro redis cache remove.{} K={}", name, key);
        RedisConnection redisConnection = null;
        V result = null;
        try {
            redisConnection = getConnection();
            result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key))));

            redisConnection.expireAt(serializer.serialize(generateKey(key)), 0);

            redisConnection.lRem(serializer.serialize(keyListKey), 1, serializer.serialize(key));
        } catch (Exception e) {
            LOGGER.error("shiro redis cache remove exception. ", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
        return result;
    }

    @Override
    public void clear() throws CacheException {
        LOGGER.debug("shiro redis cache clear.{}", name);
        RedisConnection redisConnection = null;
        try {
            redisConnection = getConnection();

            Long length = redisConnection.lLen(serializer.serialize(keyListKey));
            if (0 == length) {
                return;
            }

            List<byte[]> keyList = redisConnection.lRange(serializer.serialize(keyListKey), 0, length - 1);
            for (byte[] key : keyList) {
                redisConnection.expireAt(key, 0);
            }

            redisConnection.expireAt(serializer.serialize(keyListKey), 0);
            keyList.clear();
        } catch (Exception e) {
            LOGGER.error("shiro redis cache clear exception.", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
    }

    @Override
    public int size() {
        LOGGER.debug("shiro redis cache size.{}", name);
        RedisConnection redisConnection = null;
        int length = 0;
        try {
            redisConnection = getConnection();
            length = Math.toIntExact(redisConnection.lLen(serializer.serialize(keyListKey)));
        } catch (Exception e) {
            LOGGER.error("shiro redis cache size exception.", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
        return length;
    }

    @Override
    public Set keys() {
        LOGGER.debug("shiro redis cache keys.{}", name);
        RedisConnection redisConnection = null;
        Set resultSet = null;
        try {
            redisConnection = getConnection();

            Long length = redisConnection.lLen(serializer.serialize(keyListKey));
            if (0 == length) {
                return resultSet;
            }

            List<byte[]> keyList = redisConnection.lRange(serializer.serialize(keyListKey), 0, length - 1);
            resultSet = keyList.stream().map(bytes -> serializer.deserialize(bytes)).collect(Collectors.toSet());
        } catch (Exception e) {
            LOGGER.error("shiro redis cache keys exception.", e);
        } finally {
            if (null != redisConnection) {
                redisConnection.close();
            }
        }
        return resultSet;
    }

    @Override
    public Collection values() {
        RedisConnection redisConnection = getConnection();
        Set keys = this.keys();

        List<Object> values = new ArrayList<Object>();
        for (Object key : keys) {
            byte[] bytes = redisConnection.get(serializer.serialize(key));
            values.add(serializer.deserialize(bytes));
        }
        return values;
    }

    /**
     * 重组key
     * 区别其他使用环境的key
     *
     * @param key
     * @return
     */
    private String generateKey(K key) {
        return REDIS_SHIRO_CACHE_KEY_PREFIX + name + "_" + key;
    }

    private byte[] getByteKey(K key) {
        if (key instanceof String) {
            String preKey = generateKey(key);
            return preKey.getBytes();
        }
        return serializer.serialize(key);
    }
}

可以只在realm中设置缓存管理器

//    
@Bean
public Realm myShiroRealm(){
    MyShiroRealm myShiroRealm = new MyShiroRealm();
    myShiroRealm.setCredentialsMatcher(myMd5Matcher());

    myShiroRealm.setAuthorizationCacheName("perms");
    myShiroRealm.setAuthorizationCachingEnabled(true);

    myShiroRealm.setAuthenticationCachingEnabled(false);
    //设置缓存管理器
    myShiroRealm.setCacheManager(cacheManager());

    return myShiroRealm;
}

//缓存管理
@Bean
public CacheManager cacheManager(){
    MyRedisCacheManager cacheManager = new MyRedisCacheManager();
    return cacheManager;
}

 

注意,我在此处做得会话和缓存管理没有对过期的缓存数据进行定时清理!!!

 

有一个已经第三方框架做了对shiro和redis的整合:

https://github.com/alexxiyang/shiro-redis
-- 把会话管理和缓存管理都整合好了,直接依赖即可
<dependency>
  <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.1.0</version>
</dependency>

10.异常处理

可以使用全局异常处理器来捕获权限异常

@ControllerAdvice
public class GloableExceptionResolver {

    @ExceptionHandler(UnauthorizedException.class)
    public void calUnauthorizedException(UnauthorizedException e){
        PrintWriter writer = null;
        try{
            //判断是否是异步请求
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = requestAttributes.getRequest();
            HttpServletResponse response = requestAttributes.getResponse();
            String header = request.getHeader("X-Requested-With");
            if(StringUtils.isNoneBlank(header) && "XMLHttpRequest".equalsIgnoreCase(header)){
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json; charset=utf-8");
                writer = response.getWriter();
//                {"status":401,"message":"无权访问"}
//                String respStr = ""
                writer.write("{"status":401,"message":"无权访问"}");
            }else{
                String contextPath = request.getContextPath();
                if("/".equals(contextPath))
                    contextPath = "";
                response.sendRedirect(request.getContextPath() + "/backend/toDenied");
            }
        }catch (IOException io){
            io.printStackTrace();
        }finally {
            if(writer != null)
                writer.close();
        }
    }

}
原文地址:https://www.cnblogs.com/juddy/p/13568970.html