spring boot实战之shiro

作者: 思与学 | 来源:发表于2017-10-02 15:41 被阅读320次

    有很长一段时间都觉得自己添加个filter,基于RBAC模型,就能很轻松的实现权限控制,没必要引入shiro,spring-security这样的框架增加系统的复杂度。事实上也的确这样,如果你的需求仅仅是控制用户能否访问某个url,使用框架和自己实现filter效果基本一致,区别在于使用shiro和spring-security能够提供更多的扩展,集成了很多实用的功能,整体结构更加规范。
    shiro和spring-security有哪些更多功能,这里不再展开,感兴趣的同学可以自行百度,我们这里以shiro为例,讲述spring-boot项目如何整合shiro实现权限控制。

    1、添加maven依赖

    <!--shiro-core -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.3.2</version>
    </dependency>
    
    <!-- 整合ehcache,减少数据库查询次数 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.3.2</version>
    </dependency>
    

    2、添加shiro配置

    创建ShiroConfigration.java

    @Configuration
    public class ShiroConfigration {
        private static final Logger logger = LoggerFactory.getLogger(ShiroConfigration.class);
    
        private static Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    
    
        @Bean
        public SimpleCookie rememberMeCookie() {
            SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
            simpleCookie.setMaxAge(7 * 24 * 60 * 60);//保存10天
            return simpleCookie;
        }
    
        /**
         * cookie管理对象;
         */
        @Bean
        public CookieRememberMeManager rememberMeManager() {
            logger.debug("ShiroConfiguration.rememberMeManager()");
            CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
            cookieRememberMeManager.setCookie(rememberMeCookie());
            cookieRememberMeManager.setCipherKey(Base64.decode("kPv59vyqzj00x11LXJZTjJ2UHW48jzHN"));
            return cookieRememberMeManager;
        }
    
    
        @Bean(name = "lifecycleBeanPostProcessor")
        public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
    
        @Bean
        public FilterRegistrationBean filterRegistrationBean() {
            FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
            DelegatingFilterProxy proxy = new DelegatingFilterProxy("shiroFilter");
            //  该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
            proxy.setTargetFilterLifecycle(true);
            filterRegistration.setFilter(proxy);
    
            filterRegistration.setEnabled(true);
            //filterRegistration.addUrlPatterns("/*");// 可以自己灵活的定义很多,避免一些根本不需要被Shiro处理的请求被包含进来
            return filterRegistration;
        }
    
        @Bean
        public MyShiroRealm myShiroRealm() {
            MyShiroRealm myShiroRealm = new MyShiroRealm();
            return myShiroRealm;
        }
        
        @Bean(name="securityManager")  
        public DefaultWebSecurityManager securityManager() {  
            DefaultWebSecurityManager manager = new DefaultWebSecurityManager();  
            manager.setRealm(myShiroRealm()); 
            manager.setRememberMeManager(rememberMeManager());
            manager.setCacheManager(ehCacheManager());  
            return manager;  
        }  
        
    
        /**
         * ShiroFilterFactoryBean 处理拦截资源文件问题。
         * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
         * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
         * <p>
         * Filter Chain定义说明
         * 1、一个URL可以配置多个Filter,使用逗号分隔
         * 2、当设置多个过滤器时,全部验证通过,才视为通过
         * 3、部分过滤器可指定参数,如perms,roles
         */
        @Bean(name = "shiroFilter")
        public ShiroFilterFactoryBean getShiroFilterFactoryBean() {
            logger.debug("ShiroConfigration.getShiroFilterFactoryBean()");
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            // 必须设置 SecurityManager
            shiroFilterFactoryBean.setSecurityManager(securityManager());
            
            HashMap<String, javax.servlet.Filter> loginFilter = new HashMap<>();
            loginFilter.put("loginFilter", new LoginFilter());
            shiroFilterFactoryBean.setFilters(loginFilter);
    
    
            filterChainDefinitionMap.put("/login/submit", "anon");
            filterChainDefinitionMap.put("/logout", "anon");
            filterChainDefinitionMap.put("/img/**", "anon");
            filterChainDefinitionMap.put("/js/**", "anon");
            filterChainDefinitionMap.put("/css/**", "anon");
            filterChainDefinitionMap.put("/test/**", "anon");
            
            // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
            shiroFilterFactoryBean.setLoginUrl("/login");
    
            //配置记住我或认证通过可以访问的地址
            filterChainDefinitionMap.put("/", "user");
            //未授权界面;
            shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
            filterChainDefinitionMap.put("/**", "loginFilter");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }   
    
        /**
         * shiro缓存管理器;
         * 需要注入对应的其它的实体类中:
         * 1、安全管理器:securityManager
         * 可见securityManager是整个shiro的核心;
         *
         * @return
         */
        @Bean
        public EhCacheManager ehCacheManager() {
            EhCacheManager cacheManager = new EhCacheManager();
            cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
            return cacheManager;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
    }
    

    shiroFilter是配置的重点,

    • anon表示允许匿名访问
    • shiroFilterFactoryBean.setFilters(loginFilter)来设置自定义的过滤器,如本处设置了LoginFilter用于添加登录拦截
    • filterChainDefinitionMap.put("/**", "loginFilter");用于指定loginFilter的作用范围

    3、添加自定义realm

    创建类MyShiroRealm.java

    public class MyShiroRealm extends AuthorizingRealm {
        private static final Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
    
        @Autowired
        private UserService userService;
        
        @Autowired
        private UserRoleService userRoleService;
        
        @Autowired
        private RoleService roleService;
        
        @Autowired
        private RolePermissionService rolePermissionService;
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
            //获取用户的输入的账号.
            String idObj = (String) token.getPrincipal();
            Integer id = NumberUtils.toInt(idObj);
            User user = userService.findById(id);
    
            if (user == null) {
                // 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到unauthorizedUrl指定的地址
                return null;
            }
    
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getId(),
                    user.getPwd(), getName());
    
            return authenticationInfo;
    
        }
    
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
                /*
             * 当没有使用缓存的时候,不断刷新页面的话,这个代码会不断执行,
             * 当其实没有必要每次都重新设置权限信息,所以我们需要放到缓存中进行管理;
             * 当放到缓存中时,这样的话,doGetAuthorizationInfo就只会执行一次了,
             * 缓存过期之后会再次执行。
             */
            logger.debug("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
    
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            authorizationInfo.addRole("ACTUATOR");
    
            Integer userId = Integer.parseInt(principals.getPrimaryPrincipal().toString());
            //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
    
            Set<Integer> roleIds = userRoleService.findRoleIds(userId);
            Set<Role> roles = roleService.findByIds(roleIds);
            for(Role role : roles){
                authorizationInfo.addRole(role.getCode());
            }
    
            //设置权限信息.
            List<Permission> permissions = rolePermissionService.getPermissions(roleIds);
            Set<String> set = new HashSet<String>(permissions.size()*2);
            for(Permission permission : permissions){
                if(StringUtils.isNotBlank(permission.getCode())){
                    set.add(permission.getCode());
                }
            }
            authorizationInfo.setStringPermissions(set);
            return authorizationInfo;
        }
    
    }
    
    • doGetAuthenticationInfo用于验证用户账号信息,可根据具体业务来调整认证策略
    • doGetAuthorizationInfo用于获取用户拥有的角色和权限

    4、创建登录拦截器

    public class LoginFilter implements Filter {
    
        @Override
        public void destroy() {}
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {
            Subject currentUser = SecurityUtils.getSubject();
            if (!currentUser.isAuthenticated()) {
                HttpServletRequest req = (HttpServletRequest) request;
                HttpServletResponse res = (HttpServletResponse) response;
                AjaxResponseWriter.write(req, res, ServiceStatusEnum.UNLOGIN, "请登录");
                return;
            }
            chain.doFilter(request, response);
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {}
    
    }
    
    public class AjaxResponseWriter {
    
        /**
         * 写回数据到前端
         * @param request
         * @param response
         * @param status {@link ServiceStatusEnum} 
         * @param message 返回的描述信息
         * @throws IOException
         */
        public static void write(HttpServletRequest request,HttpServletResponse response,ServiceStatusEnum status,String message) throws IOException{
            String contentType = "application/json";
            response.setContentType(contentType);
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
            
            Map<String, String> map = Maps.newLinkedHashMap();
            map.put("code", status.code);
            map.put("msg", message);
            String result = JacksonHelper.toJson(map);
            PrintWriter out = response.getWriter();
            try{
                out.print(result);
                out.flush();
            } finally {
                out.close();
            }
        }
    }
    
    /**
     * 全局性状态码
     * @author yangwk
     */
    public enum ServiceStatusEnum {
        UNLOGIN("0001"), //未登录
        ILLEGAL_TOKEN("0002"),//非法的token
        ;
        public String code;
        
        private ServiceStatusEnum(String code){
            this.code = code;
        }
    }
    
    • 用户登录状态拦截器,不允许匿名访问的url会经过该filter,如果未登录,则返回未登录提示(未登录处理可根据具体业务进行调整)

    5、添加登录、退出功能

    @Api(value="用户登录",tags={"用户登录"})
    @RestController
    public class LoginController {
        private static Logger logger = LoggerFactory.getLogger(LoginController.class);
    
        @Value("${server.session.timeout}")
        private String serverSessionTimeout;
    
        /**
         * 用户登录接口 通过用户名和密码进行登录
         */
        @ApiOperation(value = "用户登录接口 通过用户名和密码进行登录", notes = "用户登录接口 通过用户名和密码进行登录")
        @ApiImplicitParams({
                @ApiImplicitParam(paramType = "query", name = "username", value = "用户名", required = true, dataType = "String"),
                @ApiImplicitParam(paramType = "query", name = "pwd", value = "密码", required = true, dataType = "String"),
                @ApiImplicitParam(paramType = "query", name = "autoLogin", value = "自动登录", required = true, dataType = "boolean")})
        @RequestMapping(value = "/login/submit",method={RequestMethod.GET,RequestMethod.POST})
        public Map<String, String> subm(HttpServletRequest request,HttpServletResponse response,
                String username,String pwd,@RequestParam(value = "autoLogin", defaultValue = "false") boolean autoLogin) {
            Map<String, String> map = Maps.newLinkedHashMap();
            Subject currentUser = SecurityUtils.getSubject();
            User user = userService.findByUsername(username);
            if (user == null) {
                map.put("code", "-1");
                map.put("description", "账号不存在");
                return map;
            }
            if (user.getEnable() == 0) { //账号被禁用
                map.put("code", "-1");
                map.put("description", "账号已被禁用");
                return map;
            }
    
            String salt = user.getSalt();
            UsernamePasswordToken token = null;
            Integer userId = user.getId();
            token = new UsernamePasswordToken(userId.toString(),SaltMD5Util.encode(pwd, salt));
            token.setRememberMe(autoLogin);
    
            loginValid(map, currentUser, token);
    
            // 验证是否登录成功
            if (currentUser.isAuthenticated()) {
                map.put("code","1");
                map.put("description", "ok");
                map.put("id", String.valueOf(userId));
                map.put("username", user.getUsername());
                map.put("name", user.getName());
                map.put("compnay_id", String.valueOf(user.getCompanyId()));
                String uuidToken = UUID.randomUUID().toString();
                map.put("token", uuidToken);
                
                currentUser.getSession().setTimeout(NumberUtils.toLong(serverSessionTimeout, 1800)*1000);
                request.getSession().setAttribute("token",uuidToken );
            } else {
                map.put("code", "-1");
                token.clear();
            }
            return map;
        }
        
        @RequestMapping(value="logout",method=RequestMethod.GET)
            public Map<String, String> logout() {
                Map<String, String> map = Maps.newLinkedHashMap();
                Subject currentUser = SecurityUtils.getSubject();
                currentUser.logout();
                map.put("code", "logout");
                return map;
            }
        
        @RequestMapping(value="unauth",method=RequestMethod.GET)
            public Map<String, String> unauth() {
                Map<String, String> map = Maps.newLinkedHashMap();
                map.put("code", "403");
                map.put("msg", "你没有访问权限");
                return map;
            }
    
        private boolean loginValid(Map<String, String> map,Subject currentUser, UsernamePasswordToken token) {
            String username = null;
            if (token != null) {
                username = (String) token.getPrincipal();
            }
    
            try {
                // 在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
                // 每个Realm都能在必要时对提交的AuthenticationTokens作出反应
                // 所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
                currentUser.login(token);
                return true;
            } catch (UnknownAccountException | IncorrectCredentialsException ex) {
                map.put("description", "账号或密码错误");
            } catch (LockedAccountException lae) {
                map.put("description","账户已锁定");
            } catch (ExcessiveAttemptsException eae) {
                map.put("description", "错误次数过多");
            } catch (AuthenticationException ae) {
                // 通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
                map.put("description", "登录失败");
                logger.warn(String.format("对用户[%s]进行登录验证..验证未通过", username),ae);
            }
            return false;
        }
        
        @Autowired
        private UserService userService;
    }
    
    • 以上代码是比较通用的登录、退出功能,如果没有特殊需求,可直接使用上述功能

    6、在接口上添加权限限制

    以UserController为例:

    @ApiOperation(value="获取用户详细信息", notes="根据ID查找用户")
    @ApiImplicitParam(paramType="query",name = "id", value = "用户ID", required = true,dataType="int")
    @RequiresPermissions(value={"user:get"}) 
    @RequestMapping(value="/get",method=RequestMethod.GET)
    public User get(int id){
        User entity = userService.findById(id);
        entity.setPwd(null);
        entity.setSalt(null);
        return entity;
    }
    
    @ApiOperation(value="修改密码", notes="修改密码")
    @ApiImplicitParams({
        @ApiImplicitParam(paramType = "query", name = "oldPwd", value = "旧密码", required = true, dataType = "String"),
        @ApiImplicitParam(paramType = "query", name = "pwd", value = "新密码", required = true, dataType = "String"),
        @ApiImplicitParam(paramType = "query", name = "confirmPwd", value = "新密码(确认)", required = true, dataType = "String")})
    @RequiresPermissions(value={"user:reset-pwd"})
    @RequestMapping(value="/reset-pwd",method=RequestMethod.POST)
    public Return resetPwd(String oldPwd,String pwd,String confirmPwd){
        if(StringUtils.isBlank(oldPwd) || StringUtils.isBlank(pwd)
                || StringUtils.isBlank(confirmPwd) || !pwd.equals(confirmPwd)) {
            return Return.fail("非法参数");
        }
        
        Subject currentUser = SecurityUtils.getSubject();
        Integer userId=(Integer) currentUser.getPrincipal();
        User entity = userService.findById(userId);
        if(!entity.getPwd().equals(SaltMD5Util.encode(oldPwd, entity.getSalt()))){
            return Return.fail("原始密码错误");
        }
        return userService.changePwd(entity,pwd);
    }
    
    • @RequiresPermissions 和 @RequiresRoles分别用于限制该方法可访问的权限和角色,两者如果同时使用,默认是“&”关系;两者的value参数都可以设置为数组,数组元素间的关系可以通过logical属性来设置,有Logical.AND,Logical.OR两个值可选择

    小结

    spring-boot整合shiro的步骤如下:

    1. 添加maven依赖
    2. 添加ShiroConfigration配置,指定shiro的核心配置
    3. 添加MyShiroRealm,指定账户认证策略和角色权限获取方式
    4. 添加LoginFilter,即登录拦截器
    5. 添加登录、退出功能
    6. 通过注解添加接口调用权限限制

    权限控制基于RBAC模型,涉及的表有:用户(user)、角色(role)、用户角色关系(user_role)、权限(permission)、角色权限关系(role_permission),具体代码可参考github内的示例项目。

    本人搭建好的spring boot web后端开发框架已上传至GitHub,欢迎吐槽!
    https://github.com/q7322068/rest-base,已用于多个正式项目,当前可能因为版本问题不是很完善,后续持续优化,希望你能有所收获!

    相关文章

      网友评论

        本文标题:spring boot实战之shiro

        本文链接:https://www.haomeiwen.com/subject/wmktyxtx.html