(完整版)springboot2.X整合shiro,实现shiro-redis分布式session、用户登录和权限控制

公司新项目用的是shiro做权限控制,一直说写一篇shiro的文章,一直拖着没写。马上过年了, 这债该还了呀。。。

项目基于springboot(2.1.7.RELEASE) + mybatis-plus(3.2.0) + shiro-redis(3.2.3)


知识储备

原理参考:1、shiro框架详解。2、Shiro权限管理框架详解。

(完整版)springboot2.X整合shiro,实现shiro-redis分布式session、用户登录和权限控制_第1张图片

有些名词还是得先了解:

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager : 安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等;

FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator(authc):认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authorizer(authz):Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。

Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;

SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached,redis 服务器);

SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;也可以使用开源shiro-redis

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密


账号验证,用户授权

继承AuthorizingRealm抽象类,重写doGetAuthenticationInfo方法验证用户账号密码,以及doGetAuthorizationInfo授予用户权限。注意:doGetAuthenticationInfo方法不验证密码,这交给shiro内部去做,我们在后面设置密码验证方式就好。

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UUserMapper userMapper;

    /**
     * 赋予角色,权限
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取用户名
        UUser user = (UUser) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //设置用户角色
        Set rolesEntity = userMapper.getRolesByUsername(user.getEmail());
        //设置角色
        authorizationInfo.setRoles(rolesEntity.stream().map(URole::getName).collect(Collectors.toSet()));

        //设置权限
        Set roleIds = rolesEntity.stream().map(URole::getId).collect(Collectors.toSet());
        authorizationInfo.setStringPermissions(userMapper.getPermissionsByRoles(roleIds));

        return authorizationInfo;
    }

    /**
     * 账号密码
     * 获取账号凭证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        if (authenticationToken instanceof UsernamePasswordToken) {
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
            LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(UUser::getEmail, token.getUsername());
            UUser user = userMapper.selectOne(wrapper);
            //账号是否存在
            if (Objects.isNull(user)) {
                throw new AuthenticationException("账号或密码错误");
            } else {
                //账号是否被锁定
                if (!new Long(1).equals(user.getStatus())) {
                    throw new LockedAccountException("账号已被锁定");
                }
                //返回账号密码验证凭证信息
                //这里不验证密码,交给shiro内部去做
                return new SimpleAuthenticationInfo(user,
                        user.getPswd(),
                        ByteSource.Util.bytes(user.getSalt()),
                        getName());

            }


        } else {
            log.error("账号验证错误:{}", authenticationToken);
            throw new UnknownAccountException("账号验证错误");
        }

    }
}

配置shiro

配置上面一些shiro名词。提一下HashedCredentialsMatcher这个bean,这里就是设置密码校验方式为MD5(doGetAuthenticationInfo方法没有验证密码)。shiro还可以限制同一账号同时在线人数......

@Configuration
public class ShiroConfig implements EnvironmentAware {


    private Environment environment;

    //----------------------------配置缓存相关开源包shiro-redis------------------------------------------------
    //配置redisSessionDAO
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    //配置cacheManager
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    //配置redisManager
    public IRedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(environment.getProperty("shiro-redis.port"));
        String password = environment.getProperty("shiro-redis.password");
        if (StringUtils.isNotBlank(password)) {
            redisManager.setPassword(password);
        }
//        redisManager.setTimeout((int) EXPIRE_SECONDS);
        return redisManager;

    }

    //---------------------------------结束---------------------------------------------------------------------

    /**
     * 设置会话管理器
     *
     * @return
     */
    @Bean("sessionManager")
    public SessionManager mySessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setCacheManager(cacheManager());
        return sessionManager;

    }

    /**
     * 设置安全管理器
     *
     * @param sessionManager
     * @return
     */
    @Bean("securityManager")
    public SecurityManager securityManager(SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        securityManager.setSessionManager(sessionManager);

        return securityManager;
    }

    /**
     * 过滤器
     * 哪些接口/页面授权登录就能访问
     *
     * @param securityManager
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        shiroFilter.setLoginUrl("/u-user/login");
        shiroFilter.setUnauthorizedUrl("/");

        //anon 放行;authc,需要权限验证
        Map filterMap = new LinkedHashMap<>();
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/swagger-resources/**", "anon");

        filterMap.put("/statics/**", "anon");
        filterMap.put("/login.html", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/favicon.ico", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/u-user/login", "anon");

        filterMap.put("/**", "authc");

        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     * )
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(1);//散列的次数,比如散列两次,相当于 md5(md5(""));
//		hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * 安全实体数据源
     * 获取用户实体
     *
     * @return
     */
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());

        userRealm.setCachingEnabled(true);
        //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
        userRealm.setAuthenticationCachingEnabled(false);
        //缓存AuthenticationInfo信息的缓存名称
        userRealm.setAuthenticationCacheName("authenticationCache");
        //启用授权缓存,即缓存AuthorizationInfo信息,默认false
        userRealm.setAuthorizationCachingEnabled(true);
        //缓存AuthorizationInfo信息的缓存名称
        userRealm.setAuthorizationCacheName("authorizationCache");
        //设置缓存管理器
        userRealm.setCacheManager(cacheManager());
        return userRealm;
    }

//==================================以下为配置注解相关==============================================================


    /**
     * Shiro生命周期处理器
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    /**
     *  开启shiro aop注解支持.
     *  使用代理方式;所以需要开启代码支持;
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}

用户登录与权限控制

登录:使用Subject subject = SecurityUtils.getSubject();      subject.login(token);登录。

权限控制:使用注解@RequiresPermissions,@RequiresRoles,@RequiresUser,@RequiresGuest,@RequiresAuthentication来控制访问权限。至于数据权限控制,只能在sql代码里加限制了......。

@RestController
@RequestMapping("/u-user")
public class UUserController {

    @Autowired
    private IUUserService userService;

    /**
     * 用户登录
     *
     * @param params 请求参数
     * @return
     */
    @PostMapping("/login")
    public R login(@RequestBody LoginReqDto params) {
        UsernamePasswordToken token = new UsernamePasswordToken(params.getEmail(), params.getPassword());

        try {
            // 获取 subject 认证主体
            Subject subject = SecurityUtils.getSubject();
            subject.login(token);


            return R.ok().put("data", userService.getById(1));
        } catch (LockedAccountException e) {
            return R.error("账号被锁定");
        } catch (IncorrectCredentialsException e) {
            return R.error("账号或密码错误");
        } catch (AuthenticationException e) {
            return R.error("账户验证失败");
        }
    }

    /**
     * 查找所有用户信息list
     *
     * @return 响应实体
     */
    @GetMapping("/list")
    @RequiresPermissions(value = {"user:list"}, logical = Logical.OR)
    public R getAllUsers() {
        return R.ok().put("data", userService.list());
    }

}

截图演示

完整代码在:https://gitee.com/Canon_Canon/shirodemo.git,包含了sql文件。启动后进入swagger-ui控制台http://localhost:8080/swagger-ui.html

 

(完整版)springboot2.X整合shiro,实现shiro-redis分布式session、用户登录和权限控制_第2张图片 登录

 

(完整版)springboot2.X整合shiro,实现shiro-redis分布式session、用户登录和权限控制_第3张图片 查找所有用户

 

完~

 

你可能感兴趣的:(Java)