1. 程式人生 > >springboot2整合shiro

springboot2整合shiro

0.準備

SQL使用flyway管理, DAO層是用mybatis, 快取使用Redis, 具體看原始碼(本章末尾會貼出).下面主要是shiro部分

1.新增Maven依賴

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

2.自定義SpringbootDemoAuthorizingRealm繼承AuthorizingRealm

public class SpringbootDemoAuthorizingRealm extends AuthorizingRealm {
    @Autowired
    private IUserService userServiceImpl;
    @Autowired
    private IRoleService roleServiceImpl;
    @Autowired
    private IPermissionService permissionServiceImpl;

    @Override
    public String getName() {
        return "SpringbootDemoAuthorizingRealm";
    }
    
    // 認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        String password = new String(token.getPassword());

        if (StringUtils.isBlank(username)) {
            throw new UnknownAccountException();
        }
        if (StringUtils.isBlank(password)) {
            throw new IncorrectCredentialsException();
        }

        User user = userServiceImpl.getUserByUsername(username);
        if (Objects.isNull(user)) {
            throw new UnknownAccountException();
        }

        SimpleUser simpleUser = new SimpleUser();
        simpleUser.setId(user.getId());
        simpleUser.setUsername(user.getUsername());

        // 清除授權資訊
        clearAuthorizationInfoCache(simpleUser);
        return new SimpleAuthenticationInfo(simpleUser, user.getPassword(), ByteSource.Util.bytes(username), this.getName());
    }

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleUser simpleUser = (SimpleUser) principalCollection.getPrimaryPrincipal();
        List<Role> roleList = roleServiceImpl.getRoleListByUserId(simpleUser.getId());

        List<String> simpleRoles = new ArrayList<>(roleList.size());
        List<Long> roleIds = new ArrayList<>(roleList.size());
        for (Role role : roleList) {
            simpleRoles.add(role.getRole());
            roleIds.add(role.getId());
        }
        List<String> permissions = permissionServiceImpl.getPermissionsByRoleIds(roleIds);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(simpleRoles);
        simpleAuthorizationInfo.addStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }

    /**
     * 清除所有授權資訊
     */
    public void clearAuthorizationInfoCache() {
        Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
        if(Objects.nonNull(cache)) {
            cache.clear();
        }
    }


    private void clearAuthorizationInfoCache(SimpleUser simpleUser) {
        Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
        cache.remove(simpleUser.getId());
    }
}

3.自定義ShiroConfig

@Configuration
public class ShiroConfig {
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        hashedCredentialsMatcher.setHashIterations(3);
        return hashedCredentialsMatcher;
    }

    @Bean
    public SpringbootDemoAuthorizingRealm springbootDemoAuthorizingRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
        SpringbootDemoAuthorizingRealm realm = new SpringbootDemoAuthorizingRealm();
        realm.setCredentialsMatcher(hashedCredentialsMatcher);
        // 啟用快取
        realm.setCachingEnabled(true);
        // 啟用認證快取
        realm.setAuthorizationCachingEnabled(true);
        // 啟用授權快取
        realm.setAuthorizationCachingEnabled(true);
        return realm;
    }

    @Bean
    public SecurityManager securityManager(SpringbootDemoAuthorizingRealm springbootDemoAuthorizingRealm, SessionManager sessionManager, ShiroCacheManager shiroCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(springbootDemoAuthorizingRealm);
        securityManager.setSessionManager(sessionManager);
        securityManager.setCacheManager(shiroCacheManager);
        return securityManager;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/");
        shiroFilterFactoryBean.setUnauthorizedUrl("/forbidden.do");

        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 登入不攔截
        filterChainDefinitionMap.put("/login.do", "anon");
        // 其他介面都必須登入
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

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

    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        // 跳轉頁面刪除sessionId
        defaultWebSessionManager.setSessionIdUrlRewritingEnabled(false);
        // 30分鐘
        defaultWebSessionManager.setGlobalSessionTimeout(1800000);
        defaultWebSessionManager.setDeleteInvalidSessions(true);
        defaultWebSessionManager.setSessionValidationSchedulerEnabled(true);
        defaultWebSessionManager.setSessionIdCookieEnabled(true);
        return defaultWebSessionManager;
    }
}

4.自定義ShiroCacheManager, 使用Redis作為Shiro快取

@Component
public class ShiroCacheManager implements CacheManager {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

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

    public class ShiroRedisCache<K,V> implements Cache<K,V> {

        private String cacheKey;

        private ShiroRedisCache(String cacheKey) {
            this.cacheKey = cacheKey;
        }

        private Object hashKey(K key) {
            if(key instanceof PrincipalCollection) {
                PrincipalCollection principalCollection = (PrincipalCollection) key;
                SimpleUser simpleUser = (SimpleUser) principalCollection.getPrimaryPrincipal();
                return simpleUser.getId();
            }
            return key;
        }

        @Override
        public V get(K key) throws CacheException {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            Object realKey = hashKey(key);
            return boundHashOperations.get(realKey);
        }

        @Override
        public V put(K key, V value) throws CacheException {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            Object realKey = hashKey(key);
            boundHashOperations.put((K) realKey, value);
            return value;
        }

        @Override
        public V remove(K key) throws CacheException {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            Object realKey = hashKey(key);
            V value = boundHashOperations.get(realKey);
            boundHashOperations.delete(realKey);
            return value;
        }

        @Override
        public void clear() throws CacheException {
            redisTemplate.delete(cacheKey);
        }

        @Override
        public int size() {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            return boundHashOperations.size().intValue();
        }

        @Override
        public Set<K> keys() {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            return boundHashOperations.keys();
        }

        @Override
        public Collection<V> values() {
            BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
            return boundHashOperations.values();
        }
    }
}

5.使用

@RestController
public class LoginController {

    @RequestMapping(value="", method = RequestMethod.GET)
    public ResultMessage defaultPath(){
        return ResultMessage.fail("請先登入");
    }

    @RequestMapping(value="/forbidden.do", method = RequestMethod.GET)
    public ResultMessage forbidden(){
        return ResultMessage.fail("沒有許可權");
    }

    @RequestMapping(value="/login.do", method = RequestMethod.POST)
    public ResultMessage login(@RequestBody UserRequest request){
        UsernamePasswordToken token = new UsernamePasswordToken(request.getUsername(), request.getPassword(),false);
        Subject currentUser = SecurityUtils.getSubject();
        try {
            currentUser.login(token);
        } catch (UnknownAccountException e) {
            throw new UnknownAccountException("賬號不存在");
        } catch (IncorrectCredentialsException e) {
            throw new IncorrectCredentialsException("密碼不匹配");
        } catch (LockedAccountException e) {
            throw new LockedAccountException("賬號被鎖定");
        } catch (AuthenticationException e) {
            throw new AuthenticationException("其他認證錯誤");
        } catch (RuntimeException e) {
            throw new RuntimeException("登入未知錯誤");
        }
        return ResultMessage.success("登入成功");
    }

    @RequestMapping(value="/logout.do", method =RequestMethod.GET)
    public ResultMessage logout(){
        try {
            SecurityUtils.getSubject().logout();
        } catch (Exception e) {
            throw new RuntimeException("登出未知錯誤");
        }
        return ResultMessage.success("登出成功");
    }

    @RequestMapping(value="/other.do", method =RequestMethod.GET)
    public ResultMessage other(){
        return ResultMessage.success("other success.");
    }

    @RequestMapping(value="/addTeacher.do", method =RequestMethod.GET)
    @RequiresPermissions({"teacher:add"})
    public ResultMessage addTeacher(){
        return ResultMessage.success("permission success.");
    }

    @RequestMapping(value="/updateTeacher.do", method =RequestMethod.GET)
    @RequiresPermissions({"teacher:update"})
    public ResultMessage updateTeacher(){
        return ResultMessage.success("permission success.");
    }

    /**
     * 統一異常處理
     */
    @ExceptionHandler(RuntimeException.class)
    public ResultMessage exception(Exception ex) {
        if (ex instanceof AuthorizationException) {
            return ResultMessage.fail("未授權");
        }
        return ResultMessage.fail(ex.getMessage());
    }
}

6.總結

shiro相對比較簡單, 認證(可以理解為登入), 授權(就是許可權判斷), 會話管理, 加密等模組. 

下一節會講如何重寫註解RequiresPermissions, 並將選單入庫.

原始碼 https://gitee.com/jsjack_wang/springboot-demo dev-shiro分支