前言
开心一刻
我和儿子有个共同的心愿, 出国旅游. 昨天儿子考试得了全班第一, 我跟媳妇合计着带他出国见见世面, 吃晚饭的时候, 一家人开始了讨论这个. 我:"儿子, 你的心愿是什么?", 儿子:"吃汉堡包", 我:"往大了说", 儿子:"变形金刚", 我:"今天你爹说了算, 想想咱俩共同的心愿", 儿子怯生生的瞅了媳妇一眼说:"换个妈?", 我心里咯噔一下:"这虎犊子, 坑自己也就算了, 怎么还坑爹呢".
牛: 小子, 你的杀气太重, 我早就看穿一切, 吃我一脚!
路漫漫其修远兮, 吾将上下而求索!
GitHub:https://github.com/youzhibing
码云(gitee):https://gitee.com/youzhibing
前情回顾与补充
回顾
在上篇博文中, 我们讲到了 SpringShiroFilter 是如何注册到 servlet 容器的: SpringShiroFilter 首先注册到 spring 容器, 然后被包装成 FilterRegistrationBean, 最后通过 FilterRegistrationBean 注册到 servlet 容器, 至此 shiro 的 Filter 加入到了 servlet 容器的 FilterChain 中. 另外还讲到了 shiro 的代理 FilterChain:ProxiedFilterChain, 请求来到 shiro 的 Filter 后, 会先经过 shiro 的 Filter 链, 再接着走 servlet 容器的 Filter 链, 如下图所示
如果请求经 PathMatchingFilterChainResolver 匹配成功, 那么请求会先经过 shiro Filter 链(ProxiedFilterChain), 之后再走剩下的 servlet Filter 链, 如果匹配不成功, 则直接走剩下的 servlet Filter 链. 每一次请求都会经过 shiro Filter,shiro Filter 来控制 filter 链的走向(有点类似 springmvc 的 Dispatcher), 先生成 ProxiedFilterChain, 请求先走 ProxiedFilterChain, 然后再走接着走 servlet filter 链.
上图中, 在单独的 shiro 工程中, shiro Filter 是 ShiroFilter, 而在与 spring 的集成工程中则是 SpringShiroFilter.
补充
shiro 的 Filter 关系图
shiro filter 关系图
此关系图中涉及到了 shiro 的入口: ShiroFilter 或 SpringShiroFilter, 认证拦截器: FormAuthenticationFilter, 没有涉及授权 Filter(PermissionsAuthorizationFilter,RolesAuthorizationFilter), 因为 shiro 的授权我们一般用的是注解的方式, 而不是 Filter 方式.
ShiroFilterFactoryBean 中的 createFilterChainManager()
- protected FilterChainManager createFilterChainManager() {
- DefaultFilterChainManager manager = new DefaultFilterChainManager();
- Map<String, Filter> defaultFilters = manager.getFilters();
- //apply global settings if necessary: 应用全局设置
- for (Filter filter : defaultFilters.values()) {
- applyGlobalPropertiesIfNecessary(filter);
- }
- //Apply the acquired and/or configured filters: 应用和配置 filter, 一般没有
- Map<String, Filter> filters = getFilters();
- if (!CollectionUtils.isEmpty(filters)) {
- for (Map.Entry<String, Filter> entry : filters.entrySet()) {
- String name = entry.getKey();
- Filter filter = entry.getValue();
- applyGlobalPropertiesIfNecessary(filter);
- if (filter instanceof Nameable) {
- ((Nameable) filter).setName(name);
- }
- //'init' argument is false, since Spring-configured filters should be initialized
- //in Spring (i.e. 'init-method=blah') or implement InitializingBean:
- manager.addFilter(name, filter, false);
- }
- }
- //build up the chains: 构建 shiro filter 链
- Map<String, String> chains = getFilterChainDefinitionMap();
- if (!CollectionUtils.isEmpty(chains)) {
- for (Map.Entry<String, String> entry : chains.entrySet()) {
- String url = entry.getKey();
- String chainDefinition = entry.getValue();
- manager.createChain(url, chainDefinition);
- }
- }
- return manager;
- }
- View Code
1, 给 shiro 默认的 filter 应用全局配置
- //apply global settings if necessary:
- for (Filter filter : defaultFilters.values()) {
- applyGlobalPropertiesIfNecessary(filter);
- }
- private void applyGlobalPropertiesIfNecessary(Filter filter) {
- applyLoginUrlIfNecessary(filter); // 设置 filter 的 loginUrl
- applySuccessUrlIfNecessary(filter); // 设置 filter 的 successUrl
- applyUnauthorizedUrlIfNecessary(filter); // 这个我们一般没有配置
- }
- private void applyLoginUrlIfNecessary(Filter filter) {
- String loginUrl = getLoginUrl(); // shiroFilterFactoryBean.setLoginUrl("/login"); 设置的 loginUrl
- if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
- AccessControlFilter acFilter = (AccessControlFilter) filter;
- //only apply the login url if they haven't explicitly configured one already:
- String existingLoginUrl = acFilter.getLoginUrl();
- if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
- acFilter.setLoginUrl(loginUrl);
- }
- }
- }
- private void applySuccessUrlIfNecessary(Filter filter) {
- String successUrl = getSuccessUrl(); // shiroFilterFactoryBean.setSuccessUrl("/index"); 设置的 successUrl
- if (StringUtils.hasText(successUrl) && (filter instanceof AuthenticationFilter)) {
- AuthenticationFilter authcFilter = (AuthenticationFilter) filter;
- //only apply the successUrl if they haven't explicitly configured one already:
- String existingSuccessUrl = authcFilter.getSuccessUrl();
- if (AuthenticationFilter.DEFAULT_SUCCESS_URL.equals(existingSuccessUrl)) {
- authcFilter.setSuccessUrl(successUrl);
- }
- }
- }
- private void applyUnauthorizedUrlIfNecessary(Filter filter) {
- String unauthorizedUrl = getUnauthorizedUrl();
- if (StringUtils.hasText(unauthorizedUrl) && (filter instanceof AuthorizationFilter)) {
- AuthorizationFilter authzFilter = (AuthorizationFilter) filter;
- //only apply the unauthorizedUrl if they haven't explicitly configured one already:
- String existingUnauthorizedUrl = authzFilter.getUnauthorizedUrl();
- if (existingUnauthorizedUrl == null) {
- authzFilter.setUnauthorizedUrl(unauthorizedUrl);
- }
- }
- }
- View Code
shiro 默认 11 个 filter
标红的的 filter 的 loginUrl 和 successUrl 会被设置成我们在 ShiroFilterFactoryBean 配置的, loginUrl 会被设置成 "/login",successUrl 被设置成 "index"; 这里我们需要关注下 AnonymousFilter,LogoutFilter 和 FormAuthenticationFilter, 我们目前只用到了这三个 filter.
2, 应用和配置我们在 ShiroFilterFactoryBean 设置的 Filters
ShiroFilterFactoryBean 类有个 setFilters(Map<String,Filter> filters > 方法, 可以通过此方法向 shiro 注册 filter, 不过我们一般没有用到.
3, 构建 filter 链
会将 ShiroFilterFactoryBean 中 private Map
我们配置的 filterChainDefinitionMap 中涉及到 3 个 Filter,LogoutFilter 负责 / logout,AnonymousFilter 负责(/login,/favicon.ico,/JS/**,/CSS/**,/img/**,/fonts/**),FormAuthenticationFilter 负责 /**. 至此, filter 链准备工作完成.
认证
身份认证, 即在应用中谁能证明他就是他本人. 认证方式有很多, 用的最多的就是用户名 / 密码来证明. shiro 中, 用户需要提供 pricipals(身份)和 credentials(证明)给 shiro, 从而应用能够验证用户身份. 一个主体 (Subject) 可以有多个 principals, 但只有一个 Primary principals, 一般是用户名 / 手机号, credentials 是一个只有主体知道的安全值, 一般是用户名 / 数字证书. 最常见的 principals 和 credentials 组合就是用户名 / 密码了.
接下来我们来看看一次完整的请求 : 未登录 - 登录 - 登录成功 . 还记得是哪个 filter 注册到了 servlet filter 链吗?, 就是 SpringShiroFilter, 每次请求都会经过 SpringShiroFilter; 从 shiro filter 关系图中可知, 请求肯定会经过 OncePerRequestFilter 的 doFilter 方法, 我们就从此方法开始
未登录
url 请求: http://localhost:8881/
那么此时的 url 与我们配置的哪个 filterChainDefinition 匹配呢? 很显然是 filterChainDefinitionMap.put("/**", "authc").authc 是 shiro 中默认 11 个 filter 中 FormAuthenticationFilter 的名字, 那么也就是说生成的 ProxiedFilterChain 如下所示
也就是请求会先经过 FormAuthenticationFilter, 之后再回到 servlet filter 链: orig. 那我们接着看请求到 FormAuthenticationFilter 中后做了些什么处理(注意看 shiro filter 关系图)
executeChain(request, response, chain)继续执行 filter 链之前有个 preHandle(request, response)处理, 来判断时候需要继续执行 filter 链. 跟进去会来到 onPreHandle 方法
- public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
- }
- protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
- return super.isAccessAllowed(request, response, mappedValue) ||
- (!isLoginRequest(request, response) && isPermissive(mappedValue));
- }
- // super.isAccessAllowed 判断时候已经认证过, 有个标志字段: authenticated
- // isLoginRequest 判断是否是登录请求, 很显然不是, 登录请求是 / login, 目前是 /
- // isPermissive 没搞明白, 可能应对一些特殊的 filter
- protected boolean onAccessDenied(ServletRequest request,
- ServletResponse response, Object mappedValue) throws Exception {
- return onAccessDenied(request, response);
- }
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
- if (isLoginRequest(request, response)) { // 是否是登录请求
- if (isLoginSubmission(request, response)) { // 是否是 post 请求
- 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; // get 方式的登录请求则继续执行 filter 链, 最终会来到我们的 controller 的登录 get 请求
- }
- } else {
- if (log.isTraceEnabled()) {
- log.trace("Attempting to access a path which requires authentication. Forwarding to the" +
- "Authentication url [" + getLoginUrl() + "]");
- }
- saveRequestAndRedirectToLogin(request, response); // 重定向到 / login
- return false; // 返回 false, 表示 filter 链不继续执行了
- }
- }
- View Code
最终会重定向到 / login, 这又是一次新的 get 请求, 会重新将上面的流程走一遍, 只是 url 变成了: http://localhost:8881/login. 此时的 ProxiedFilterChain 如下所示
请求来到 AnonymousFilter 之后, onPreHandler 直接返回 true, 接着走剩下的 servlet Filter 链, 最终来到我们的 controller
- @GetMapping("/login")
- public String loginPage() {
- return "login";
- }
将登录页返回回去
登录
url 请求: http://localhost:8881/login, 请求方式是 post
流程与上面未登录差不多, 此时的 ProxiedFilterChain 如下所示
AnonymousFilter 的 onPreHandler 方法直接返回的 true, 请求会接着走剩下的 servlet Filter 链, 最终来到我们的 controller
- @PostMapping("/login")
- @ResponseBody
- public OwnResult dologin(String username, String password) {
- username = username.trim();
- // 判断当前用户是否可用
- User user = userService.findUserByUsername(username);
- if(user == null) {
- return OwnResult.build(RespCode.ERROR_USER_NOT_EXIST.getCode(), username + "用户不存在");
- }
- if (user.getStatus() == Constants.USER_DISABLED) {
- return OwnResult.build(RespCode.ERROR_USER_DISABLED.getCode(), "账号已被禁用, 请联系管理员");
- }
- UsernamePasswordToken token = new UsernamePasswordToken(username, password);
- Subject subject = SecurityUtils.getSubject();
- try {
- subject.login(token); // 登录认证交给 shiro
- return OwnResult.ok();
- } catch (AuthenticationException e) {
- return OwnResult.build(RespCode.ERROR_USERNAME_PASSWORD.getCode(), "用户名或密码错误");
- }
- }
- View Code
登录认证过程委托给了 shiro, 我们来看看具体的认证过程
如果开启了认证缓存 (authenticationCachingEnabled=true), 则会先从缓存中获取 authenticationInfo, 若没有则调用我们自定义 Realm 的 doGetAuthenticationInfo 方法获取数据库中用户的信息, 并缓存起来; 然后将 authenticationInfo 与登录页面输入的用户信息(封装成 UsernamePasswordToken) 进行匹配验证. 登录认证失败会抛出 AuthenticationException; 登录成功则会将 authenticated 设置成 true, 表示已经认证过了.
注意: 登录认证没有完全交给 shiro, 而是在我们的 controller 中委托给 shiro 了, 这与完全交由 shiro 还是有区别的(具体可以看下 FormAuthenticationFilter 的 onAccessDenied 方法).
登录成功
登录成功后, 我们往往会请求主页, url 请求: http://localhost:8881/index
流程与上面两个差不多, 此时的 ProxiedFilterChain 如下所示
此时 authenticated 已经为 true, 会接着走余下的 servlet Filler 链, 最终请求会来到我们的 controller
- @RequestMapping({"/","/index"})
- public String index(Model model){
- List<Menu> menus = menuService.listMenu();
- model.addAttribute("menus", menus);
- model.addAttribute("username", getUsername());
- return "index_v1";
- }
- View Code
将 index_v1.html 返回回去
授权
授权, 也叫访问控制, 即在应用中控制谁能访问哪些资源(如访问页面 / 编辑数据 / 页面操作等). 授权中有几个需要了解的关键对象: 主体(Subject), 资源(Resource), 权限(Permission), 角色(Role). 主体: 即访问应用的用户, shiro 中使用 Subject 代表该用户; 资源: 应用中用户可以访问的任何东西, 比如访问 JSP 页面, 查看 / 编辑某些数据, 访问某个业务方法等; 权限: 表示在应用中用户有没有操作某个资源的权力, 能不能访问某个资源; 角色: 可以理解成权限的集合, 一般情况下我们会赋予用户角色而不是权限, 这样用户可以拥有一组权限, 赋予权限时比较方便.
shiro 支持三种方式的授权
1, 编程式, 通过写 if/else 授权代码块
- Subject subject = SecurityUtils.getSubject();
- if(subject.hasRole("admin")) {
- // 有权限, 执行相关业务
- } else {
- // 无权限, 给相关提示
- }
2, 注解式, 通过在执行的 Java 方法上放置相应的注解完成
- @RequiresPermissions("sys:user:user")
- public List<User> listUser() {
- // 有权限, 获取数据
- }
3,JSP/GSP 标签, 在 JSP/GSP 页面通过相应的标签完成
- <shiro:hasRole name="admin">
- <!-- 有权限 -->
- </shiro:hashRole>
一般而言, 编程式基本不用, 注解方式比较普遍, 标签方式用的不多; 那么我们就来看看注解方式, 它是如何实现权限控制的. 一看到注解, 我们就要想到 aop(动态代理), 在目标对象的前后可以织入增强处理, 具体我们往下看.
注解权限控制
authorizationInfo 获取
执行目标方法前 (也就是 @RequiresPermissions("xxx") 修饰的方法), 会先调用 assertAuthorized(methodInvocation)进行权限的验证, 分两步: 先获取 authorizationInfo, 再进行权限的检查. 上图展示了 authorizationInfo, 权限的检查请往下看.
先从缓存中获取 authorizationInfo, 若没有则调用我们自定义 Realm 的 doGetAuthorizationInfo 方法来获取 authorizationInfo(设置了 roles 与 stringPermissions), 并将其放入缓存中, 然后返回 authorizationInfo; 若从缓存中获取到了 authorizationInfo, 则直接返回, 而不需要通过 Realm 从数据库中获取了. 一般情况下, 权限缓存是开启的: myShiroRealm.setAuthorizationCachingEnabled(true);
权限检查
当 authorizationInfo 获取到之后, 进行来就是需要检查 authorizationInfo 中是否含有 @RequiresPermissions("xxx")中的 xxx 了, 我们往下看
可以看到, 检查过程过程就是将 authorizationInfo 中的 Permission 集合组个与 xxx 进行匹对, 一旦匹对成功, 则权限检查通过, 流程往下走即执行目标方法(也就是我们的业务方法), 如果一个都没匹对成功, 则会抛出 UnauthorizedException 异常
上述讲了 Permission 的方式进行权限的控制, 通过 Role 控制的方式大同小异, 有兴趣的朋友可以自己去跟一跟. 当然还有其他的方式, 但用的最多的是 Permission 和 Role.
总结
1,anon: 匿名访问, 不需要认证, 一般就是针对游客可以访问的资源; authc: 登录认证;
2, 我们所有的请求一般由 shiro 中 3 个 Filter:LogoutFilter,AnonymousFilter,FormAuthenticationFilter 分摊了, LogoutFilter 负责 / logout,AnonymousFilter 负责 / login 和静态资源, FormAuthenticationFilter 则负责剩下的(/**);
3, 未登录的请求会由 FormAuthenticationFilter 重定向 / login, 登录成功后会将 authenticated 设置成 true, 那么之后的请求会正常走剩下的 servlet filter 链, 最终来到我们的 controller; 登录认证过程会先从缓存获取 authenticationInfo, 没有则通过 realm 从数据库获取并放入缓存, 然后将页面输入的用户信息 UsernamePasswordToken 与 authenticationInfo 进行匹配验证. 个人不建议开启认证缓存, 当修改用户信息后刷新缓存中的认证信息, 不好处理, 另外认证频率本来就不高, 缓存的意义不大;
4, 授权一般采用注解方式, 注解往往配合 aop 来实现目标方法前后的增强织入, shiro 的权限注解就是在目标方法前的增强处理. 校验过程与认证过程类似, 先从缓存中获取 authorizationInfo, 没有则通过 realm 从数据库获取, 然后放入缓存, 看 authorizationInfo 中是否有 @RequiresPermissions("xxx")中的 xxx 来完成权限的验证. 个人建议开启权限缓存, 权限的验证还是挺多的, 如果不开启缓存, 那么会给数据库造成一定的压力;
留个疑问, 有兴趣的朋友可以去查看下源码: 假如 session 过期后, 我们再请求, shiro 是如何处理并跳转到登录页的?
来源: https://www.cnblogs.com/youzhibing/p/10122606.html