spring boot项目实战程序员Spring Boot

spring boot实战之shiro

2017-10-02  本文已影响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是配置的重点,

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;
    }

}

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;
    }
}

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);
}

小结

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

上一篇 下一篇

猜你喜欢

热点阅读