天天看點

SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入

一、說明:

Shiro是一個安全架構,項目中主要用它做認證,授權,加密,以及使用者的會話管理,雖然Shiro沒有SpringSecurity功能更豐富,但是它輕量,簡單,在項目中通常業務需求Shiro也都能勝任.

二、項目環境:

  • MyBatis-Plus版本: 3.1.0
  • SpringBoot版本:2.1.5
  • JDK版本:1.8
  • Shiro版本:1.4
  • Shiro-redis插件版本:3.1.0

資料表(SQL檔案在項目中):資料庫中測試号的密碼進行了加密,密碼皆為123456

SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入

Maven依賴如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- AOP依賴,一定要加,否則權限攔截驗證不生效 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- lombok插件 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <!-- mybatisPlus 核心庫 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- 引入阿裡資料庫連接配接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>
        <!-- Shiro 核心依賴 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- Shiro-redis插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- StringUitlS工具 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
</dependencies>
           

配置如下:

# 配置端口
server:
  port: 8764
spring:
  # 配置資料源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
  # Redis資料源
  redis:
    host: localhost
    port: 6379
    timeout: 6000
    password: 123456
    jedis:
      pool:
        max-active: 1000  # 連接配接池最大連接配接數(使用負值表示沒有限制)
        max-wait: -1      # 連接配接池最大阻塞等待時間(使用負值表示沒有限制)
        max-idle: 10      # 連接配接池中的最大空閑連接配接
        min-idle: 5       # 連接配接池中的最小空閑連接配接
# mybatis-plus相關配置
mybatis-plus:
  # xml掃描,多個目錄用逗号或者分号分隔(告訴 Mapper 所對應的 XML 檔案位置)
  mapper-locations: classpath:mapper/*.xml
  # 以下配置均有預設值,可以不設定
  global-config:
    db-config:
      #主鍵類型 AUTO:"資料庫ID自增" INPUT:"使用者輸入ID",ID_WORKER:"全局唯一ID (數字類型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: auto
      #字段政策 IGNORED:"忽略判斷"  NOT_NULL:"非 NULL 判斷")  NOT_EMPTY:"非空判斷"
      field-strategy: NOT_EMPTY
      #資料庫類型
      db-type: MYSQL
  configuration:
    # 是否開啟自動駝峰命名規則映射:從資料庫列名到Java屬性駝峰命名的類似映射
    map-underscore-to-camel-case: true
    # 如果查詢結果中包含空值的列,則 MyBatis 在映射的時候,不會映射這個字段
    call-setters-on-nulls: true
    # 這個配置會将執行的sql列印出來,在開發或測試的時候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
           

三、編寫項目基礎類

使用者實體,Dao,Service等在這裡省略,請參考源碼

編寫Exception類來處理Shiro權限攔截異常

/**
 * @Description 自定義異常
 * @Author Sans
 * @CreateTime 2019/6/15 22:56
 */
@ControllerAdvice
public class MyShiroException {
    /**
     * 處理Shiro權限攔截異常
     * 如果傳回JSON資料格式請加上 @ResponseBody注解
     * @Author Sans
     * @CreateTime 2019/6/15 13:35
     * @Return Map<Object> 傳回結果集
     */
    @ResponseBody
    @ExceptionHandler(value = AuthorizationException.class)
    public Map<String,Object> defaultErrorHandler(){
        Map<String,Object> map = new HashMap<>();
        map.put("403","權限不足");
        return map;
    }
}
           

建立SHA256Util加密工具

/**
 * @Description Sha-256加密工具
 * @Author Sans
 * @CreateTime 2019/6/12 9:27
 */
public class SHA256Util {
    /**  私有構造器 **/
    private SHA256Util(){};
    /**  加密算法 **/
    public final static String HASH_ALGORITHM_NAME = "SHA-256";
    /**  循環次數 **/
    public final static int HASH_ITERATIONS = 15;
    /**  執行加密-采用SHA256和鹽值加密 **/
    public static String sha256(String password, String salt) {
        return new SimpleHash(HASH_ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
    }
}
           

建立Spring工具

/**
 * @Description Spring上下文工具類
 * @Author Sans
 * @CreateTime 2019/6/17 13:40
 */
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext context;
    /**
     * Spring在bean初始化後會判斷是不是ApplicationContextAware的子類
     * 如果該類是,setApplicationContext()方法,會将容器中ApplicationContext作為參數傳入進去
     * @Author Sans
     * @CreateTime 2019/6/17 16:58
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
    /**
     * 通過Name傳回指定的Bean
     * @Author Sans
     * @CreateTime 2019/6/17 16:03
     */
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}
           

建立Shiro工具

/**
 * @Description Shiro工具類
 * @Author Sans
 * @CreateTime 2019/6/15 16:11
 */
public class ShiroUtils {

    /** 私有構造器 **/
    private ShiroUtils(){}

    private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);

    /**
     * 擷取目前使用者Session
     * @Author Sans
     * @CreateTime 2019/6/17 17:03
     * @Return SysUserEntity 使用者資訊
     */
    public static Session getSession() {
        return SecurityUtils.getSubject().getSession();
    }

    /**
     * 使用者登出
     * @Author Sans
     * @CreateTime 2019/6/17 17:23
     */
    public static void logout() {
        SecurityUtils.getSubject().logout();
    }

    /**
    * 擷取目前使用者資訊
    * @Author Sans
    * @CreateTime 2019/6/17 17:03
    * @Return SysUserEntity 使用者資訊
    */
    public static SysUserEntity getUserInfo() {
      return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
    }

    /**
     * 删除使用者緩存資訊
     * @Author Sans
     * @CreateTime 2019/6/17 13:57
     * @Param  username  使用者名稱
     * @Param  isRemoveSession 是否删除Session
     * @Return void
     */
    public static void deleteCache(String username, boolean isRemoveSession){
        //從緩存中擷取Session
        Session session = null;
        Collection<Session> sessions = redisSessionDAO.getActiveSessions();
        SysUserEntity sysUserEntity;
        Object attribute = null;
        for(Session sessionInfo : sessions){
            //周遊Session,找到該使用者名稱對應的Session
            attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (attribute == null) {
                continue;
            }
            sysUserEntity = (SysUserEntity) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
            if (sysUserEntity == null) {
                continue;
            }
            if (Objects.equals(sysUserEntity.getUsername(), username)) {
                session=sessionInfo;
            }
        }
        if (session == null||attribute == null) {
            return;
        }
        //删除session
        if (isRemoveSession) {
            redisSessionDAO.delete(session);
        }
        //删除Cache,在通路受限接口時會重新授權
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        Authenticator authc = securityManager.getAuthenticator();
        ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
    }
}
           

建立Shiro的SessionId生成器

/**
 * @Description 自定義SessionId生成器
 * @Author Sans
 * @CreateTime 2019/6/11 11:48
 */
public class ShiroSessionIdGenerator implements SessionIdGenerator {
    /**
     * 實作SessionId生成
     * @Author Sans
     * @CreateTime 2019/6/11 11:54
     */
    @Override
    public Serializable generateId(Session session) {
        Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
        return String.format("login_token_%s", sessionId);
    }
}
           

四、編寫Shiro核心類

建立Realm用于授權和認證

/**
 * @Description Shiro權限比對和賬号密碼比對
 * @Author Sans
 * @CreateTime 2019/6/15 11:27
 */
public class ShiroRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    /**
     * 授權權限
     * 使用者進行權限驗證時候Shiro會去緩存中找,如果查不到資料,會執行這個方法去查權限,并放入緩存中
     * @Author Sans
     * @CreateTime 2019/6/12 11:44
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal();
        //擷取使用者ID
        Long userId =sysUserEntity.getUserId();
        //這裡可以進行授權和處理
        Set<String> rolesSet = new HashSet<>();
        Set<String> permsSet = new HashSet<>();
        //查詢角色和權限(這裡根據業務自行查詢)
        List<SysRoleEntity> sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId);
        for (SysRoleEntity sysRoleEntity:sysRoleEntityList) {
            rolesSet.add(sysRoleEntity.getRoleName());
            List<SysMenuEntity> sysMenuEntityList = sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());
            for (SysMenuEntity sysMenuEntity :sysMenuEntityList) {
                permsSet.add(sysMenuEntity.getPerms());
            }
        }
        //将查到的權限和角色分别傳入authorizationInfo中
        authorizationInfo.setStringPermissions(permsSet);
        authorizationInfo.setRoles(rolesSet);
        return authorizationInfo;
    }
    
    /**
     * 身份認證
     * @Author Sans
     * @CreateTime 2019/6/12 12:36
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //擷取使用者的輸入的賬号.
        String username = (String) authenticationToken.getPrincipal();
        //通過username從資料庫中查找 User對象,如果找到進行驗證
        //實際項目中,這裡可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘内不會重複執行該方法
        SysUserEntity user = sysUserService.selectUserByName(username);
        //判斷賬号是否存在
        if (user == null) {
            throw new AuthenticationException();
        }
        //判斷賬号是否被當機
        if (user.getState()==null||user.getState().equals("PROHIBIT")){
            throw new LockedAccountException();
        }
        //進行驗證
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,                                  //使用者名
                user.getPassword(),                    //密碼
                ByteSource.Util.bytes(user.getSalt()), //設定鹽值
                getName()
        );
        //驗證成功開始踢人(清除緩存和Session)
        ShiroUtils.deleteCache(username,true);
        return authenticationInfo;
    }
}
           

建立SessionManager類

/**
 * @Description 自定義擷取Token
 * @Author Sans
 * @CreateTime 2019/6/13 8:34
 */
public class ShiroSessionManager extends DefaultWebSessionManager {
    //定義常量
    private static final String AUTHORIZATION = "Authorization";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    //重寫構造器
    public ShiroSessionManager() {
        super();
        this.setDeleteInvalidSessions(true);
    }
    /**
     * 重寫方法實作從請求頭擷取Token便于接口統一
     * 每次請求進來,Shiro會去從請求頭找Authorization這個key對應的Value(Token)
     * @Author Sans
     * @CreateTime 2019/6/13 8:47
     */
    @Override
    public Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果請求頭中存在token 則從請求頭中擷取token
        if (!StringUtils.isEmpty(token)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            //否則按預設規則從cookie取token
            return super.getSessionId(request, response);
        }
    }
}
           

建立ShiroConfig配置類

/**
 * @Description Shiro配置類
 * @Author Sans
 * @CreateTime 2019/6/10 17:42
 */
@Configuration
public class ShiroConfig {

    private final String CACHE_KEY = "shiro:cache:";
    private final String SESSION_KEY = "shiro:session:";
    private final int EXPIRE = 1800;

    //Redis配置
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.password}")
    private String password;

    /**
     * 開啟Shiro-aop注解支援
     * @Attention 使用代理方式是以需要開啟代碼支援
     * @Author Sans
     * @CreateTime 2019/6/12 8:38
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * Shiro基礎配置
     * @Author Sans
     * @CreateTime 2019/6/12 8:42
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 注意過濾器配置順序不能颠倒
        // 配置過濾:不會被攔截的連結
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/userLogin/**", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        // 配置shiro預設登入界面位址,前後端分離中登入界面跳轉應由前端路由控制,背景僅傳回json資料
        shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 安全管理器
     * @Author Sans
     * @CreateTime 2019/6/12 10:34
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定義Ssession管理
        securityManager.setSessionManager(sessionManager());
        // 自定義Cache實作
        securityManager.setCacheManager(cacheManager());
        // 自定義Realm驗證
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

    /**
     * 身份驗證器
     * @Author Sans
     * @CreateTime 2019/6/12 10:37
     */
    @Bean
    public ShiroRealm shiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }

    /**
     * 憑證比對器
     * 将密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理,在這裡做比對配置
     * @Author Sans
     * @CreateTime 2019/6/12 10:48
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
        // 雜湊演算法:這裡使用SHA256算法;
        shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
        // 散列的次數,比如散列兩次,相當于 md5(md5(""));
        shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
        return shaCredentialsMatcher;
    }

    /**
     * 配置Redis管理器
     * @Attention 使用的是shiro-redis開源插件
     * @Author Sans
     * @CreateTime 2019/6/12 11:06
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * 配置Cache管理器
     * 用于往Redis存儲權限和角色辨別
     * @Attention 使用的是shiro-redis開源插件
     * @Author Sans
     * @CreateTime 2019/6/12 12:37
     */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(CACHE_KEY);
        // 配置緩存的話要求放在session裡面的實體類必須有個id辨別
        redisCacheManager.setPrincipalIdFieldName("userId");
        return redisCacheManager;
    }

    /**
     * SessionID生成器
     * @Author Sans
     * @CreateTime 2019/6/12 13:12
     */
    @Bean
    public ShiroSessionIdGenerator sessionIdGenerator(){
        return new ShiroSessionIdGenerator();
    }

    /**
     * 配置RedisSessionDAO
     * @Attention 使用的是shiro-redis開源插件
     * @Author Sans
     * @CreateTime 2019/6/12 13:44
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDAO.setKeyPrefix(SESSION_KEY);
        redisSessionDAO.setExpire(expire);
        return redisSessionDAO;
    }

    /**
     * 配置Session管理器
     * @Author Sans
     * @CreateTime 2019/6/12 14:25
     */
    @Bean
    public SessionManager sessionManager() {
        ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
        shiroSessionManager.setSessionDAO(redisSessionDAO());
        return shiroSessionManager;
    }
}
           

五、實作權限控制

Shiro可以用代碼或者注解來控制權限,通常我們使用注解控制,不僅簡單友善,而且更加靈活.Shiro注解一共有五個:

SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入

一般情況下我們在項目中做權限控制,使用最多的是RequiresPermissions和RequiresRoles,允許存在多個角色和權限,預設邏輯是AND,也就是同時擁有這些才可以通路方法,可以在注解中以參數的形式設定成OR

示例:

//擁有一個角色就可以通路
@RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
//擁有所有權限才可以通路
@RequiresPermissions(value={"sys:user:info","sys:role:info"},logical = Logical.AND)
           

使用順序:Shiro注解是存在順序的,當多個注解在一個方法上的時候,會逐個檢查,知道全部通過為止,預設攔截順序是:RequiresRoles->RequiresPermissions->RequiresAuthentication->

RequiresUser->RequiresGuest

示例:

//擁有ADMIN角色同時還要有sys:role:info權限
@RequiresRoles(value={"ADMIN")
@RequiresPermissions("sys:role:info")
           

建立UserRoleController角色攔截測試類

/**
 * @Description 角色測試
 * @Author Sans
 * @CreateTime 2019/6/19 11:38
 */
@RestController
@RequestMapping("/role")
public class UserRoleController {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private SysRoleMenuService sysRoleMenuService;

    /**
     * 管理者角色測試接口
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/getAdminInfo")
    @RequiresRoles("ADMIN")
    public Map<String,Object> getAdminInfo(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","這裡是隻有管理者角色能通路的接口");
        return map;
    }

    /**
     * 使用者角色測試接口
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/getUserInfo")
    @RequiresRoles("USER")
    public Map<String,Object> getUserInfo(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","這裡是隻有使用者角色能通路的接口");
        return map;
    }

    /**
     * 角色測試接口
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/getRoleInfo")
    @RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
    @RequiresUser
    public Map<String,Object> getRoleInfo(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","這裡是隻要有ADMIN或者USER角色能通路的接口");
        return map;
    }

    /**
     * 登出(測試登出)
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/getLogout")
    @RequiresUser
    public Map<String,Object> getLogout(){
        ShiroUtils.logout();
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","登出");
        return map;
    }
}
           

建立UserMenuController權限攔截測試類

/**
 * @Description 權限測試
 * @Author Sans
 * @CreateTime 2019/6/19 11:38
 */
@RestController
@RequestMapping("/menu")
public class UserMenuController {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private SysRoleMenuService sysRoleMenuService;
    
    /**
     * 擷取使用者資訊集合
     * @Author Sans
     * @CreateTime 2019/6/19 10:36
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/getUserInfoList")
    @RequiresPermissions("sys:user:info")
    public Map<String,Object> getUserInfoList(){
        Map<String,Object> map = new HashMap<>();
        List<SysUserEntity> sysUserEntityList = sysUserService.list();
        map.put("sysUserEntityList",sysUserEntityList);
        return map;
    }

    /**
     * 擷取角色資訊集合
     * @Author Sans
     * @CreateTime 2019/6/19 10:37
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/getRoleInfoList")
    @RequiresPermissions("sys:role:info")
    public Map<String,Object> getRoleInfoList(){
        Map<String,Object> map = new HashMap<>();
        List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
        map.put("sysRoleEntityList",sysRoleEntityList);
        return map;
    }

    /**
     * 擷取權限資訊集合
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/getMenuInfoList")
    @RequiresPermissions("sys:menu:info")
    public Map<String,Object> getMenuInfoList(){
        Map<String,Object> map = new HashMap<>();
        List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
        map.put("sysMenuEntityList",sysMenuEntityList);
        return map;
    }

    /**
     * 擷取所有資料
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/getInfoAll")
    @RequiresPermissions("sys:info:all")
    public Map<String,Object> getInfoAll(){
        Map<String,Object> map = new HashMap<>();
        List<SysUserEntity> sysUserEntityList = sysUserService.list();
        map.put("sysUserEntityList",sysUserEntityList);
        List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
        map.put("sysRoleEntityList",sysRoleEntityList);
        List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
        map.put("sysMenuEntityList",sysMenuEntityList);
        return map;
    }

    /**
     * 添加管理者角色權限(測試動态權限更新)
     * @Author Sans
     * @CreateTime 2019/6/19 10:39
     * @Param  username 使用者ID
     * @Return Map<String,Object> 傳回結果
     */
    @RequestMapping("/addMenu")
    public Map<String,Object> addMenu(){
        //添加管理者角色權限
        SysRoleMenuEntity sysRoleMenuEntity = new SysRoleMenuEntity();
        sysRoleMenuEntity.setMenuId(4L);
        sysRoleMenuEntity.setRoleId(1L);
        sysRoleMenuService.save(sysRoleMenuEntity);
        //清除緩存
        String username = "admin";
        ShiroUtils.deleteCache(username,false);
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","權限添加成功");
        return map;
    }
}
           

建立UserLoginController登入類

/**
 * @Description 使用者登入
 * @Author Sans
 * @CreateTime 2019/6/17 15:21
 */
@RestController
@RequestMapping("/userLogin")
public class UserLoginController {

    @Autowired
    private SysUserService sysUserService;

    /**
     * 登入
     * @Author Sans
     * @CreateTime 2019/6/20 9:21
     */
    @RequestMapping("/login")
    public Map<String,Object> login(@RequestBody SysUserEntity sysUserEntity){
        Map<String,Object> map = new HashMap<>();
        //進行身份驗證
        try{
            //驗證身份和登陸
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
            //驗證成功進行登入操作
            subject.login(token);
        }catch (IncorrectCredentialsException e) {
            map.put("code",500);
            map.put("msg","使用者不存在或者密碼錯誤");
            return map;
        } catch (LockedAccountException e) {
            map.put("code",500);
            map.put("msg","登入失敗,該使用者已被當機");
            return map;
        } catch (AuthenticationException e) {
            map.put("code",500);
            map.put("msg","該使用者不存在");
            return map;
        } catch (Exception e) {
            map.put("code",500);
            map.put("msg","未知異常");
            return map;
        }
        map.put("code",0);
        map.put("msg","登入成功");
        map.put("token",ShiroUtils.getSession().getId().toString());
        return map;
    }
    /**
     * 未登入
     * @Author Sans
     * @CreateTime 2019/6/20 9:22
     */
    @RequestMapping("/unauth")
    public Map<String,Object> unauth(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",500);
        map.put("msg","未登入");
        return map;
    }
}
           

六、 POSTMAN測試

登入成功後會傳回TOKEN,因為是單點登入,再次登陸的話會傳回新的TOKEN,之前Redis的TOKEN就會失效了

SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入

當第一次通路接口後我們可以看到緩存中已經有權限資料了,在次通路接口的時候,Shiro會直接去緩存中拿取權限,注意通路接口時候要設定請求頭.

SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入
SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入

ADMIN這個号現在沒有sys:info:all這個權限的,是以無法通路getInfoAll接口,我們要動态配置設定權限後,要清掉緩存,在通路接口時候,Shiro會去重新執行授權方法,之後再次把權限和角色資料放入緩存中

SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入

通路添權重限測試接口,因為是測試,我把增權重限的使用者ADMIN寫死在裡面了,權限添加後,調用工具類清掉緩存,我們可以發現,Redis中已經沒有緩存了

SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入
SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入

再次通路getInfoAll接口,因為緩存中沒有資料,Shiro會重新授權查詢權限,攔截通過

SpringBoot 整合 Shiro 實作動态權限加載更新 + Session共享 + 單點登入

七、項目源碼

https://gitee.com/liselotte/spring-boot-shiro-demo


https://github.com/xuyulong2017/my-java-demo
           

繼續閱讀