[置顶] spring-boot实战:shiro
2017-10-02 15:43
477 查看
有很长一段时间都觉得自己添加个filter,基于RBAC模型,就能很轻松的实现权限控制,没必要引入shiro,spring-security这样的框架增加系统的复杂度。事实上也的确这样,如果你的需求仅仅是控制用户能否访问某个url,使用框架和自己实现filter效果基本一致,区别在于使用shiro和spring-security能够提供更多的扩展,集成了很多实用的功能,整体结构更加规范。
shiro和spring-security有哪些更多功能,这里不再展开,感兴趣的同学可以自行百度,我们这里以shiro为例,讲述spring-boot项目如何整合shiro实现权限控制。
shiroFilter是配置的重点,
* anon表示允许匿名访问
* shiroFilterFactoryBean.setFilters(loginFilter)来设置自定义的过滤器,如本处设置了LoginFilter用于添加登录拦截
* filterChainDefinitionMap.put(“/**”, “loginFilter”);用于指定loginFilter的作用范围
doGetAuthenticationInfo用于验证用户账号信息,可根据具体业务来调整认证策略
doGetAuthorizationInfo用于获取用户拥有的角色和权限
4、创建登录拦截器
用户登录状态拦截器,不允许匿名访问的url会经过该filter,如果未登录,则返回未登录提示(未登录处理可根据具体业务进行调整)
以上代码是比较通用的登录、退出功能,如果没有特殊需求,可直接使用上述功能
@RequiresPermissions 和 @RequiresRoles分别用于限制该方法可访问的权限和角色,两者如果同时使用,默认是“&”关系;两者的value参数都可以设置为数组,数组元素间的关系可以通过logical属性来设置,有Logical.AND,Logical.OR两个值可选择
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,已用于多个正式项目,当前可能因为版本问题不是很完善,后续持续优化,希望你能有所收获!
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() { 4000 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.javapublic 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); i 9f0e f(!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
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring-boot实战:shiro
- [置顶] spring boot项目实战:跨域问题解决
- [置顶] spring boot项目实战:swagger2在线文档
- [置顶] spring boot项目实战:redis