Spring Boot 4:Shiro
Apache Shiro
Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。
Apache Shiro的首要目标是易于使用和理解。安全通常很复杂,甚至让人感到很痛苦,但是Shiro却不是这样子的。一个好的安全框架应该屏蔽复杂性,向外暴露简单、直观的API,来简化开发人员实现应用程序安全所花费的时间和精力。
Shiro能做什么呢?
- 验证用户身份
- 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
- 在非 web 或 EJB 容器的环境下可以任意使用Session API
- 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
- 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
- 支持单点登录(SSO)功能
- 支持提供“Remember Me”服务,获取用户关联信息而无需登录
…
等等——都集成到一个有凝聚力的易于使用的API。
Shiro 致力在所有应用环境下实现上述功能,小到命令行应用程序,大到企业应用中,而且不需要借助第三方框架、容器、应用服务器等。当然 Shiro 的目的是尽量的融入到这样的应用环境中去,但也可以在它们之外的任何环境下开箱即用。
Apache Shiro Features 特性
Apache Shiro是一个全面的、蕴含丰富功能的安全框架。下图为描述Shiro功能的框架图:
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:
- Authentication(认证):用户身份识别,通常被称为用户“登录”
- Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
- Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
- Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:
- Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
- 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
- 并发:Apache Shiro 支持多线程应用程序的并发特性。
- 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
- “Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)。
- “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。
注意: Shiro不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro
High-Level Overview 高级概述
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。
- Subject:认证实体,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
Subject 包含 Principals 和 Credentials 两个信息。我们看下两者的具体含义。
Principals:代表身份。可以是用户名、邮件、手机号码等等,用来标识一个登录主体的身份;
Credentials:代表凭证。常见的有密码,数字证书等等。 - SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
- Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
Maven Dependency
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
Shiro Config
必要:自定义 Realm、安全管理器 SecurityManager 和 Shiro 过滤器
package sun.flower.diver.base.config; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import sun.flower.diver.base.realm.MyRealm; import java.util.LinkedHashMap; import java.util.Map; /** * Shiro Configuration * * @Author YangXuyue * @Date 2018-4-15 12:57 */ @Configuration public class ShiroConfig { /** * Shiro Filter * * @param securityManager * @return */ @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); //如果不设置默认会自动寻找Web工程根目录下的"/login.html"页面 shiroFilterFactoryBean.setLoginUrl("/user/login"); //登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); /* 默认登录的 URL:身份认证失败会访问该 URL; 认证成功之后要跳转的 URL; 权限认证失败后要跳转的 URL; 需要拦截或者放行的 URL:这些都放在一个 Map 中。 */ // 拦截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(32); // 配置不会被拦截的链接 顺序判断 filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/fonts/**", "anon"); filterChainDefinitionMap.put("/images/**", "anon"); filterChainDefinitionMap.put("/plugins/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/libs/**", "anon"); filterChainDefinitionMap.put("/favicon.ico", "anon"); filterChainDefinitionMap.put("/**/*.css", "anon"); filterChainDefinitionMap.put("/**/*.js", "anon"); filterChainDefinitionMap.put("/**/*.html", "anon"); // 以“/user/admin” 开头的用户需要身份认证,authc 表示要进行身份认证 // filterChainMap.put("/user/admin*", "authc"); // “/user/student” 开头的用户需要角色认证,是“admin”才允许 // filterChainMap.put("/user/student*/**", "roles[admin]"); // “/user/teacher” 开头的用户需要权限认证,是“user:create”才允许 // filterChainMap.put("/user/teacher*/**", "perms[\"user:create\"]"); // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/user/login", "anon"); // 所有url都必须有user权限才可以访问 一般讲/**放在最后面 filterChainDefinitionMap.put("/**", "user"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 生命周期 * * @return */ @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } /** * 身份认证realm * * @return */ @Bean(name = "myRealm") public MyRealm systemAuthorizingRealm() { return new MyRealm(); } //配置核心安全事务管理器 @Bean(name = "securityManager") public SecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置realm. securityManager.setRealm(myRealm); // 设置"记住我"管理器 securityManager.setRememberMeManager(rememberMeManager()); return securityManager; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( @Qualifier("securityManager") SecurityManager manager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(manager); return advisor; } /** * thymleaf里使用shiro * * @return */ //@Bean(name = "shiroDialect") //public ShiroDialect shiroDialect() { // return new ShiroDialect(); //} /** * 记住我Cookie * * @return */ @Bean public SimpleCookie rememberMeCookie() { //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); //<!-- 记住我cookie生效时间7天 ,单位秒;--> simpleCookie.setMaxAge(604800); return simpleCookie; } /** * cookie管理对象 * * @return */ @Bean public CookieRememberMeManager rememberMeManager() { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); return cookieRememberMeManager; } }
Shiro Realm
package sun.flower.diver.base.realm; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import sun.flower.diver.modules.system.consts.UserStatus; import sun.flower.diver.base.kit.ShiroKit; import sun.flower.diver.modules.base.consts.SystemConsts; import sun.flower.diver.modules.base.domain.ShiroInfo; import sun.flower.diver.modules.system.domain.Resource; import sun.flower.diver.modules.system.domain.Role; import sun.flower.diver.modules.system.domain.User; import sun.flower.diver.modules.system.service.RoleService; import sun.flower.diver.modules.system.service.UserService; import java.util.*; /** * 自定义Realm实现 * * @Author YangXuyue * @Date 2018/04/11 16:32 */ @Component("myRealm") public class MyRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; /** * 授权:在调用controller方法时,如果方法上面有shiro注解,会触发下面的方法 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //返回验证信息AuthenticationInfo,包括用户角色和权限 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //取得用户信息 ShiroInfo shiroInfo = (ShiroInfo) this.getAuthenticationCacheKey(principalCollection); authorizationInfo.setRoles(shiroInfo.getRoles()); authorizationInfo.setStringPermissions(shiroInfo.getPermissions()); return authorizationInfo; } /** * 身份认证 * 调用subject.login(token);的时候,会触发下面的方法 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; User user = userService.findByUsernameAndPassword(token.getUsername(), new String(token.getPassword())); // 用户不存在 Optional.ofNullable(user).orElseThrow(UnknownAccountException::new); //if (null == user) { // throw new UnknownAccountException(); //} // 用户锁定状态 if (UserStatus.LOCKED.equals(user.getLocked())) { throw new LockedAccountException(); } ShiroInfo shiroInfo = new ShiroInfo(); shiroInfo.setUserId(user.getId()); shiroInfo.setUsername(user.getUsername()); shiroInfo.setPassword(user.getPassword()); // 用户id查角色,角色id查权限 Set<String> rolesSet = new HashSet<>(); Set<String> stringPermissionsSet = new HashSet<>(); // 封装role和permission Map<String, Object> userInfo = new HashMap<>(); userInfo.put("userId", user.getId()); List<Role> roles = userService.listRoles(userInfo); if (null != roles) { for (Role role : roles) { rolesSet.add(role.getName()); Map<String, Object> roleInfo = new HashMap<>(); roleInfo.put("roleId", role.getId()); List<Resource> resources = roleService.listResources(roleInfo); if (null != resources) { for (Resource resource : resources) { stringPermissionsSet.add(resource.getPermission()); } } } } shiroInfo.setRoles(rolesSet); shiroInfo.setPermissions(stringPermissionsSet); // 将当前登录用户放入session中,不使用查询出来的dbUser是为了防止部分隐私信息泄露 User userTemp = new User(); userTemp.setId(user.getId()); userTemp.setName(user.getName()); userTemp.setUsername(user.getUsername()); userTemp.setNickname(user.getNickname()); userTemp.setEmail(user.getEmail()); userTemp.setPhone(user.getPhone()); ShiroKit.setSessionAttribute(SystemConsts.ACTIVE_USER, userTemp); return new SimpleAuthenticationInfo(shiroInfo, user.getPassword(), getName()); } }
ShiroKit
package sun.flower.diver.base.kit; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sun.flower.basic.kit.EncryptKit; import sun.flower.basic.kit.StringKit; import sun.flower.diver.modules.base.consts.SystemConsts; import sun.flower.basic.base.BaseResult; import sun.flower.diver.modules.base.domain.ShiroInfo; import sun.flower.diver.modules.system.domain.User; /** * Shiro Kit * * @Author YangXuyue * @Date 2018/08/26 20:06 */ public final class ShiroKit { private ShiroKit() { } private static final Logger LOGGER = LoggerFactory.getLogger(ShiroKit.class); /** * 获取Subject * * @return */ public static Subject getSubject() { return SecurityUtils.getSubject(); } /** * 获取Session * * @return */ public static Session getSession() { return getSubject().getSession(); } /** * 获取当前用户信息 * * @return */ public static User getActiveUser() { return (User) getSessionAttribute(SystemConsts.ACTIVE_USER); } /** * 获取当前用户的权限信息 * * @return */ public static ShiroInfo getShiroInfo() { return (ShiroInfo) getSubject().getPrincipal(); } /** * 设置Session信息 * * @param key * @param value */ public static void setSessionAttribute(Object key, Object value) { getSession().setAttribute(key, value); } /** * 获取Session值 * * @param key * @return */ public static Object getSessionAttribute(Object key) { return getSession().getAttribute(key); } /** * 判断用户是否登录 * * @return */ public static boolean isLogin() { return getSubject().getPrincipal() != null; } /** * 退出登录 */ public static void logout() { getSubject().logout(); } /** * 登录 */ public static BaseResult login(String loginName, String password, boolean rememberMe) { BaseResult result = null; Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken( StringKit.trim(loginName), EncryptKit.md5(password), rememberMe); try { subject.login(token); if (subject.isAuthenticated()) { result = BaseResult.ok(); } } catch (IncorrectCredentialsException e) { String msg = "密码错误!"; result = BaseResult.error(msg); LOGGER.error(msg); } catch (ExcessiveAttemptsException e) { String msg = "登录失败次数过多!"; result = BaseResult.error(msg); LOGGER.error(msg); } catch (LockedAccountException e) { String msg = "帐号已被锁定!"; result = BaseResult.error(msg); LOGGER.error(msg); } catch (DisabledAccountException e) { String msg = "帐号已被禁用!"; result = BaseResult.error(msg); LOGGER.error(msg); } catch (ExpiredCredentialsException e) { String msg = "帐号已过期!"; result = BaseResult.error(msg); LOGGER.error(msg); } catch (UnknownAccountException e) { String msg = "帐号不存在!"; result = BaseResult.error(msg); LOGGER.error(msg); } catch (UnauthorizedException e) { String msg = "没有得到授权!"; result = BaseResult.error(msg); LOGGER.error(msg); } return result; } }
ShiroInfo.java
public class ShiroInfo implements Serializable { private static final long serialVersionUID = 1L; /** * 用户id */ private Long userId; /** * 登录名 */ private String username; /** * 密码 */ private String password; /** * 用户所属的角色名称集合 */ private Set<String> roles; /** * 用户拥有的权限集合 */ private Set<String> permissions; @Override public String toString() { return ToStringBuilder.reflectionToString(this); } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Set<String> getRoles() { return roles; } public void setRoles(Set<String> roles) { this.roles = roles; } public Set<String> getPermissions() { return permissions; } public void setPermissions(Set<String> permissions) { this.permissions = permissions; } }