补习系列- springboot 整合 shiro 一指禅

目标 了解ApacheShiro是什么,能做什么; 通过QuickStart 代码领会 Shiro的关键概念; 能基于SpringBoot 整合Shiro 实现URL安全访问; 掌握基于注解的方法,以实现灵活定制。 一、Apache Shiro是什么 Apache Shiro 是一个强大且易用的Java安全框架,用于实现身份认证、鉴权、会话管理及加密功能。 框架提供了非常简单且易于上手的API,可以支持快速为web应用程序实现安全控制能力。 官网地址 github 地址 Shiro 能做什么 Apache Shiro 的设计初衷是让安全管理变得易于上手和容易理解,它可以实现: 鉴别用户身份,是否本系统注册的A用户; 管理用户权限,是否有某个角色,或某些权限; 即使没有web或EJB容器,也可以使用Session API 可以聚合一个或多个用户权限数据源并且以用户视图的形式统一表现出来 实现单点登录功能(SSO) 无需登录便可实现记住我这一功能 有什么特性 官网-Features 主要概念 包括了 Authentication(身份鉴别)、Authorization(权限管理)、Session Management(会话管理)、Cryptography(加密) 这号称软件安全的四大基石.. 关于几个概念,用下面的表格说明: 名称 解释 Authentication(身份鉴别) 指鉴别登录用户的身份 Authorization(权限认证) 决定用户是否有权访问某物 Session Management(会话管理) 支持独立的会话管理 Cryptography(加密) 利用加密算法保证数据安全 其他特性非核心,但是非常有用 web应用支持 如JavaEE、Spring的整合支持 缓存 用于提升安全管理的效率 并发 可支持多线程应用 测试 可以通过单元测试和集成测试验证程序的安全性 Run As 允许用户将某一身份赋予另一用户(在一些行政管理软件中常用) Remember Mes 在Session(会话)期间记住用户身份,当只有强制要求登录是才需要用户登录 架构说明 看看下面的图: 图中涉及了若干个模块,关于每个模块的大致作用如下: Subject 交互实体,对应于当前用户。 SecurityManager 安全管理器,Shiro最核心的模块,管理各安全模块的工作; Authenticator 身份鉴别组件,执行和反馈用户的认证(登录), 该组件从Realm中获取用户信息。 Authentication Strategy 如果配置了多个Realm,该怎么协调?这就用到策略 Authorizer 权限认证,顾名思义,就是用于负责用户访问控制的模块。 SessionManager 会话管理器,在Web环境中Shiro一般会沿用Servlet容器的会话。 但脱离了Web环境就会使用独立的会话管理。 SessionDAO 执行会话持久化的工具 CacheManager 一个缓存管理器,可为 Shiro 的其他组件提供缓存能力。 Cryptography 加密组件,提供了大量简单易用的安全加密API 到这里,不需要为这么多的模块而苦恼,在使用Shiro时,只需要牢牢记住下面的实体关系,便不会产生理解上的困难。 简而言之 应用程序依赖于 Subject 实体来标识当前的用户,而SecurityManager 则通过Realm接口读取数据,进而实现 Subject 的关联管理。 二、快速入门 为了帮助读者更快速理解Shiro,下面上一段QuickStart的代码 // 加载 shiro.ini并构造 SecurityManager Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); // 设置当前的 SecurityManager对象 SecurityUtils.setSecurityManager(securityManager); // 获取当前用户 Subject currentUser = SecurityUtils.getSubject(); // 操作会话 Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } // 执行登录 if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } catch (AuthenticationException ae) { // unexpected condition? error? } } // 输出用户信息 log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); // 检查角色 if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } // 检查权限 if (currentUser.isPermitted("lightsaber:weild")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } // 结束,执行注销 currentUser.logout(); System.exit(0); 上面这段代码来自 shiro-sample/QuickStart.java, 关于代码的解释.. 老司机认为看下注释是一定能懂的了。 三、SpringBoot 整合 Shiro 我们尝试将 Shiro 整合到 SpringBoot 项目,翻了下官网并没有太多介绍, 猜想这可能与 SpringBoot 框架还比较新有关系,Shiro是个老框架(2010年出的第一个版本).. 但最终老司机还是成功找到了 胶合组件:shiro-spring-boot-starter 接下来,为项目引入依赖: org.apache.shiro shiro-spring-boot-starter 1.4.0 接下来,我们将完成一个 URL访问安全控制 的示例,通过这个案例 读者可以了解到如何根据业务定制必要的功能模块。 系统设计 图示中,名为lilei 的用户拥有 normal (普通用户)的角色,而相应的具备customer.profile的读写权限。 以上是基于RBAC(基于角色的权限控制) 的设计,RBAC 目前的应用非常广泛 在 web应用访问中,某些页面是允许任何人访问的,某些需要登录用户,比如个人中心 而某些页面需要具备一些特权,比如vip资料.. 如下图所示: 用户模块 通常,在设计用户权限时都会考虑用户信息、角色信息以及对应的权限 用户实体 public static class UserInfo { private String username; private String passwordHash; private String salt; 需要注意到 salt是用于密码存储的加盐值(用于防止暴力破解) passwordHash 是原始密码经过加盐哈希计算后的值(16进制形式) 角色实体 public static class RoleInfo { private String roleName; private List perms; 为了简化,我们直接将权限用字符串形式表示,一个角色RoleInfo包含了一组权限perm。 用户管理器 在我们的样例中,需要实现一个UserManager类,用于做用户信息、权限信息的管理。 public class ShiroUserManager { // 用户表 private final Map users = new HashMap(); // 角色权限表 private final Map> userRoles = new HashMap>(); private static final Logger logger = LoggerFactory.getLogger(ShiroUserManager.class); // 密钥匹配类 private ShiroHashMatcher matcher; public ShiroUserManager(ShiroHashMatcher matcher) { this.matcher = matcher; } public ShiroHashMatcher getMatcher() { return this.matcher; } @PostConstruct private void init() { // 预置信息 register("lilei", "111111", "123"); grant("normal", new RoleInfo("customer", "customer.profile.read")); grant("normal", new RoleInfo("customer", "customer.profile.write")); } /** * 获取用户信息 * * @param username * @return */ public UserInfo getUser(String username) { if (StringUtils.isEmpty(username)) { return null; } return users.get(username); } /** * 获取权限信息 * * @param username * @return */ public List getRoles(String username) { if (StringUtils.isEmpty(username)) { return Collections.emptyList(); } return userRoles.get(username); } /** * 添加用户 * * @param username * @param password * @param salt * @return */ public UserInfo register(String username, String password, String salt) { if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password) || StringUtils.isEmpty(salt)) { return null; } // 生成加盐密码Hash值 String passwordHash = matcher.getCredentialHash(password, salt); logger.info("user {} register with passHash :{}", username, passwordHash); UserInfo user = new UserInfo(username, passwordHash, salt); users.put(username, user); return user; } /** * 授权操作 * * @param username * @param role */ public void grant(String username, RoleInfo role) { if (userRoles.containsKey(username)) { userRoles.get(username).add(role); } else { List roleList = new ArrayList(); roleList.add(role); userRoles.put(username, roleList); } } 在上面的实现中,我们仅仅将用户、角色信息放在内存中管理,并内置了名为lilei的用户角色。 在真实应用中,用户权限需要通过持久层(DB)实现 密钥算法 我们基于Shiro的基础类HashedCredentialsMatcher进行了扩展。 选用SHA-256哈希算法,设置迭代次数为1024。 public class ShiroHashMatcher extends HashedCredentialsMatcher { public ShiroHashMatcher() { setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME); setHashIterations(1024); setStoredCredentialsHexEncoded(true); } public String getCredentialHash(Object credentials, Object salt) { return new SimpleHash(this.getHashAlgorithmName(), credentials, salt, this.getHashIterations()).toHex(); } Realm实现 在Shiro 框架中, Realm 是用作用户权限信息查询的接口,我们的实现如下: public class ShiroRealm extends AuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(ShiroRealm.class); private ShiroUserManager userManager; public ShiroRealm(ShiroUserManager userManager) { this.setCredentialsMatcher(userManager.getMatcher()); this.userManager = userManager; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { logger.info("check authorization info"); SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(); // 获取当前用户 UserInfo userInfo = (UserInfo) principals.getPrimaryPrincipal(); // 查询角色信息 List roleInfos = userManager.getRoles(userInfo.getUsername()); if (roleInfos != null) { for (RoleInfo roleInfo : roleInfos) { authInfo.addRole(roleInfo.getRoleName()); if (roleInfo.getPerms() != null) { for (String perm : roleInfo.getPerms()) { authInfo.addStringPermission(perm); } } } } return authInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { logger.info("check authentication info"); String username = (String) token.getPrincipal(); // 获取用户信息 UserInfo user = userManager.getUser(username); if (user == null) { return null; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPasswordHash(), ByteSource.Util.bytes(user.getSalt()), getName()); return authenticationInfo; } Bean 注册 将实现好的 ShiroRealm 注册为Bean,并初始化 WebSecurityManager @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm()); return securityManager; } @Bean public ShiroRealm realm() { ShiroRealm realm = new ShiroRealm(userManager()); return realm; } @Bean public ShiroUserManager userManager() { return new ShiroUserManager(matcher()); } @Bean public ShiroHashMatcher matcher() { return new ShiroHashMatcher(); } 定义拦截链 拦截器链通过 ShiroFilterFactoryBean实现定制,实现如下: @Bean public ShiroFilterFactoryBean filter(org.apache.shiro.mgt.SecurityManager securityManager) { logger.info("config shiro filter"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 定义URL拦截链 Map filterChainDefinitionMap = new LinkedHashMap(); // 允许匿名用户访问首页 filterChainDefinitionMap.put("/shiro/index", "anon"); // 定义注销路径 filterChainDefinitionMap.put("/shiro/logout", "logout"); // 所有用户界面都需要身份验证,否则会跳转到loginurl,由FormAuthenticationFilter处理 filterChainDefinitionMap.put("/shiro/user/**", "authc"); // 为login路径定义拦截,由FormAuthenticationFilter处理 filterChainDefinitionMap.put("/shiro/login", "authc"); // 所有vip路径要求具备vip角色权限 filterChainDefinitionMap.put("/shiro/vip/**", "roles[vip]"); // 指定loginurl 路径 shiroFilterFactoryBean.setLoginUrl("/shiro/login"); // 登录成功后跳转路径 shiroFilterFactoryBean.setSuccessUrl("/shiro/user/"); // for un authenticated shiroFilterFactoryBean.setUnauthorizedUrl("/shiro/unauth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 自定义filters,可覆盖默认的Filter列表,参考 DefaultFilter Map filters = new LinkedHashMap(); // 定制logout 过滤,指定注销后跳转到登录页(默认为根路径) LogoutFilter logoutFilter = new LogoutFilter(); logoutFilter.setRedirectUrl("/shiro/login"); filters.put("logout", logoutFilter); // 定制authc 过滤,指定登录表单参数 FormAuthenticationFilter authFilter = new FormAuthenticationFilter(); authFilter.setUsernameParam("username"); authFilter.setPasswordParam("password"); filters.put("authc", authFilter); shiroFilterFactoryBean.setFilters(filters); return shiroFilterFactoryBean; } 跟着老司机的注释,上面代码应该不难理解(尽管有点冗长),filterChainDefinitionMap的定义中, key对应于url路径,而value则对应了过滤器的缩写,Shiro内置的过滤器可参考DefaultFilter枚举 配置 过滤器 功能 anon AnonymousFilter 可匿名访问 authc FormAuthenticationFilter form表单登录拦截 authcBasic BasicHttpAuthenticationFilter basic登录拦截 logout LogoutFilter 注销处理 noSessionCreation NoSessionCreationFilter 禁止创建会话 perms PermissionsAuthorizationFilter 指定权限 port PortFilter 指定端口 rest HttpMethodPermissionFilter HttpMethod转换 roles RolesAuthorizationFilter 指定角色 ssl SslFilter 需要https user UserFilter 已登录或Rememberme 深入一点 FormAuthenticationFilter 实现了表单登录的拦截逻辑: 如果当前没有登录,则跳转到 loginUrl; 如果是登录请求,则执行登录操作,成功后跳转到 loginSuccessUrl 如果登录失败,将当前的异常信息写入请求上下文,由业务处理。 扒一扒源码,可以看到相应的逻辑实现: protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); } return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } //allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Au
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信