spring-boot实战:shiro

来源:互联网 发布:怎样学好linux 编辑:程序博客网 时间:2024/06/05 06:45

有很长一段时间都觉得自己添加个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

@Configurationpublic 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={"用户登录"})@RestControllerpublic 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,已用于多个正式项目,当前可能因为版本问题不是很完善,后续持续优化,希望你能有所收获!