简介
• Apache Shiro 是 Java 的一个安全(权限)框架相对于Spring Security更加轻量
• Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在 JavaEE 环境。
• Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、 缓存等
功能简介
• Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
• Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
• Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普JavaSE 环境,也可以是 Web 环境的;
• Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
• Web Support:Web 支持,可以非常容易的集成到Web 环境;
• Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
• Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
• Testing:提供测试支持;
• Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
• Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
Shiro 架构
Shiro外部来看
从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作
• Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外API 核心就是 Subject。 Subject 代表了当前“用户” , 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;与 Subject 的所有交互都会委托给 SecurityManager;Subject 其实是一个门面,SecurityManager 才是实际的执行者;
• SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中DispatcherServlet 的角色
• Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager 要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource
Shiro内部来看
• Subject:任何可以与应用交互的“用户”;
• SecurityManager :相当于SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证、授权、会话及缓存的管理。
• Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
• Authorizer:授权器、 即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
• Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的 Realm;
• SessionManager:管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web环境,也可以用在如普通的 JavaSE 环境
• CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能
• Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密。
Shiro工作原理
ShiroFilterFactoryBean的静态内部类SpringShiroFilter是Spring全局的过滤器,面注册了属于shiro自己的内置过滤器。Shiro内置了几个过滤器无需手动配置,我们也可以自定义过滤器他们都会在ShiroFilterFactoryBean初始化时加载到filters里。
以下为shior默认过滤器
SpringShiroFilter的工作流程
每个realm都有自己的数据缓存cache,二次调用直接从缓存中取数据
Shiro的基础知识
过滤器访问规则
• [urls] 部分的配置,其格式是: “ url=拦截器[参数],拦截器[参数]”;
• 如果当前请求的 url 匹配 [urls] 部分的某个 url 模式,将会执行其配置的拦截器。
• anon(anonymous) 拦截器表示匿名访问(即不需要登录即可访问)
• authc (authentication)拦截器表示需要身份认证通过后才能访问
URL 模式使用 Ant 风格模式
• Ant 路径通配符支持 ?、 *、 **,注意通配符匹配不包括目录分隔符“ /”:
– ?:匹配一个字符,如 /admin? 将匹配 /admin1,但不匹配 /admin 或 /admin/;
– *:匹配零个或多个字符串,如 /admin 将匹配 /admin、/admin123,但不匹配 /admin/1;
– **:匹配路径中的零个或多个路径,如 /admin/** 将匹配 /admin/a 或 /admin/a/
URL 匹配顺序
• URL 权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的 url 模式对应的拦截器链。
• 如:
– /bb/**=filter1
– /bb/aa=filter2
– /**=filter3
– 如果请求的url是“ /bb/aa”,因为按照声明顺序进行匹配,那么将使用 filter1 进行拦截。
权限注解
• @RequiresAuthentication:表示当前Subject已经通过login进行了身份验证;即 Subject. isAuthenticated() 返回 true
• @RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的。
• @RequiresGuest:表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
• @RequiresRoles(value={“admin”, “user”}, logical=Logical.AND):表示当前 Subject 需要角色 admin 和user
• @RequiresPermissions (value={“ user:a”, “ user:b”},logical= Logical.OR):表示当前 Subject 需要权限 user:a 或user:b
会话
Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如web容器tomcat),不管 JavaSE 还是 JavaEE 环境
都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对Web 的透明支持、 SSO 单点登录的支持等特性。
会话监听器
会话监听器用于监听会话创建、过期及停止事件
项目实战
需引入的包
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.1.0</version> </dependency> <!--集成jwt实现token认证--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </exclusion> </exclusions> <version>2.3.1.RELEASE</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
ShiroFilterFactoryBean的配置
Shiro通过Map集合组成了一个拦截器链 ,自顶向下过滤,一旦匹配,则不再执行下面的过滤。如果下面的定义与上面冲突,那按照了谁先定义谁说了算。PermissionsAuthorizationFilter继承自AuthorizationFilter所以也可以校验用户用户是否登入
@Bean public ShiroFilterFactoryBean factory(SecurityManager securityManager, JdbcTemplate jdbcTemplate) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); Map<String, String> filterRuleMap = new LinkedHashMap<>(); factoryBean.setLoginUrl("/litigation/system/not/logged"); factoryBean.setUnauthorizedUrl("/litigation/system/unauthorized"); // 配置不会被拦截的链接 List<String> urls = getIgnoredUrlsProperties().getUrls(); for (String url : urls) { filterRuleMap.put(url, "anon"); } List<Map<String, Object>> menus = jdbcTemplate.queryForList("select * from menu"); for (Map<String, Object> menu : menus) { Object url = menu.get("url"); Object perms = menu.get("perms"); if (url != null && perms != null) { filterRuleMap.put(url.toString(), "perms[" + perms.toString() + "]"); } } filterRuleMap.put("/litigation/**","authc"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; }
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import java.util.Base64; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/litigation/system") @Slf4j public class SystemController { @RequestMapping(value = "/not/logged") public String notLogged(){ throw new ServerException(ErrorCode.NO_LOGIN); } @RequestMapping(value = "/unauthorized") public String unauthorized(){ throw new ServerException(ErrorCode.NO_PERMITTED); } }
Menu表的数据
配置无需过滤的路径
ignored.urls[0]=/ ignored.urls[1]=/v2/api-docs ignored.urls[2]=/swagger**/** ignored.urls[3]=/swagger**/**/** ignored.urls[4]=/webjars/** ignored.urls[5]=/v2/** ignored.urls[6]=/litigation/system/login ignored.urls[7]=/litigation/system/not/logged ignored.urls[8]=/litigation/system/unauthorized
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.List; /** * @author lzhcode */ @Data @Configuration @ConfigurationProperties(prefix = "ignored") public class IgnoredUrlsProperties { private List<String> urls = new ArrayList<>(); }
Relm的登入和接口权限判断
import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.mgt.SessionsSecurityManager; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.DefaultSessionManager; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.CollectionUtils; import javax.annotation.PostConstruct; import java.util.*; @Slf4j public abstract class AbstractUserRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; //获取用户角色的权限信息 public abstract UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo); /** * 获取登入用户的所有角色和权限 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { User user = (User) principals.getPrimaryPrincipal(); String currentLoginName = user.getMobile(); Set<String> userRoles = new HashSet<>(); Set<String> userPermissions = new HashSet<>(); //从数据库中获取当前登录用户的详细信息 Map param = new HashMap<>(); param.put("mobile",currentLoginName); List<User> users = userMapper.selectListSelective(param); if (!CollectionUtils.isEmpty(users)) { User userInfo = users.get(0); UserRolesAndPermissions roleContainer = doGetRoleAuthorizationInfo(userInfo); userRoles.addAll(roleContainer.getUserRoles()); userPermissions.addAll(roleContainer.getUserPermissions()); } else { throw new AuthorizationException(); } //为当前用户设置角色和权限 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRoles(userRoles); authorizationInfo.addStringPermissions(userPermissions); return authorizationInfo; } /** * 登录认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authenticationToken) throws AuthenticationException { //UsernamePasswordToken对象用来存放提交的登录信息 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; //查出是否有此用户 Map param = new HashMap<>(); param.put("mobile",token.getUsername()); param.put("status",StatusEnum.NORMAL.getCode()); List<User> users = userMapper.selectListSelective(param); if (!CollectionUtils.isEmpty(users)) { //防止重复登入 SessionsSecurityManager securityManager = (SessionsSecurityManager) SecurityUtils.getSecurityManager(); DefaultSessionManager sessionManager = (DefaultSessionManager) securityManager.getSessionManager(); Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();//获取当前已登录的用户session列表 for (Session session : sessions) { //清除该用户以前登录时保存的session //如果和当前session是同一个session,则不剔除 if (SecurityUtils.getSubject().getSession().getId().equals(session.getId())) break; User user = (User) (session.getAttribute("user")); if (user != null) { String mobile = user.getMobile(); if (token.getUsername().equals(mobile)) { log.info(mobile + "已登录,剔除中..."); sessionManager.getSessionDAO().delete(session); } } } User user = users.get(0); // 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验 SimpleAuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); return authcInfo; } return null; } protected class UserRolesAndPermissions { Set<String> userRoles; Set<String> userPermissions; public UserRolesAndPermissions(Set<String> userRoles, Set<String> userPermissions) { this.userRoles = userRoles; this.userPermissions = userPermissions; } public Set<String> getUserRoles() { return userRoles; } public Set<String> getUserPermissions() { return userPermissions; } } @PostConstruct public void initCredentialsMatcher() { //该句作用是重写shiro的密码验 setCredentialsMatcher(new CustomCredentialsMatcher()); } }