spring集成shiro详解

最近项目中要用到shiro作为权限管理,以前都是用自定义的或者spring security,所以就开始看了一些网上的文章,但是感觉都写得很零散。而且大多数都只是给了几行代码,我们得项目相对比较复杂,需要进行一些额外得改造和扩展;所以自己也是结合源码做了一下学习和总结。

shiro说明

这一块网上可以参考的文章很多,我这也基本是看的别人的,可以自己搜一下,需要提前对这些概念有一个了解

Apache Shiro是一个强大且灵活的开源安全框架,易于使用且好理解,撇开了搭建安全框架时的复杂性。 Shiro可以帮助我们做以下几件事:

  • 认证使用者的身份

  • 提供用户的访问控制,比如:

    • 决定一个用户是否被授予某个特定的安全角色

    • 决定用户是否允许做某件事

  • 可以在任何环境中使用Session API,不在局限于web或是EJB容器中

  • 可以在认证,访问控制或是session的生命周期的期间中对特定事件产生反应

  • 可以整合多个数据源的用户安全数据到一个统一的用户视图中

  • 支持单点登录

  • 支持'记住我'功能 等等

Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;

SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;

SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);

SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的


在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。

credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。

最常见的 principals 和 credentials 组合就是用户名 / 密码了。接下来先进行一个基本的身份认证。

另外两个相关的概念是之前提到的 Subject 及 Realm,分别是主体及验证主体的数据源

快速开始

最快速的方式就是集成shiro-spring-boot-web-starter

<!-- 方便做页面渲染 -->
<dependency>
                <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
        </dependency>

大致的项目结构:

shiro.ini

#用户
[users]
#用户admin的密码是admin,此用户具有role1和role2两个角色
admin=admin,role1,role2
test=123456,role2

#权限
[roles]
#角色role1对资源user拥有create、update权限
role1=user:create,user:update
#角色role2对资源user拥有create、delete权限
role2=user:create,user:delete

application.yml

server:
  port: 8081
spring:
  thymeleaf:
    prefix: classpath:/pages/
    suffix: .html
shiro:
  loginUrl: /login

login&index.html

login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>test</title>
</head>
<body>
<form action="/login" method="post">
    <label>用户名:</label><input name="username"/><br/>
    <label>密码:</label><input name="password" type="password"/>
    <input type="submit" value="登录" />
</form>
</body>
</html>

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>test</title>
</head>
<body>
welcome
</body>
</html>
WebConfig

这个类是我不想写登录和成功的跳转的controller

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/index").setViewName("index");
    }
}

这个时候直接访问ip:port/login就能到登录页面  输入用户名密码(admin/admin)点击登录就可以登录成功并跳转到index.html

这个只是一个最简单的实现,但是实用性不高,而且有个坑;就是如果输入的用户名或者密码有错误,他不会提示,而是直接post请求url /login到controller,因为我们没有这个controller 所以会报405,也可以自己写这个controller,然后判断当前用户是否登录。大致代码:

@PostMapping
    public String login(){
        Subject subject = SecurityUtils.getSubject();
        if(!subject.isAuthenticated()){
            //返回错误的ajax信息或者重新跳转登录页面
        }
    }

接下来会对这个例子做一个详细原理讲解,其中有一些扩展点,我们可以基于这些扩展点实现前后端分离、集成其他认证系统、JWT等

详细说明

查看源码,我们发现shiro-spring-boot-web-starter这个帮我们配置好了很多bean,其中在ShiroWebAutoConfiguration 这个类中定义了

protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        chainDefinition.addPathDefinition("/**", "authc");
        return chainDefinition;
    }

拦截全部请求,并且使用我们上面说的FormAuthenticationFilter来进行处理

ShiroWebFilterConfiguration的父类中定义了

protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

        filterFactoryBean.setLoginUrl(loginUrl);
        filterFactoryBean.setSuccessUrl(successUrl);
        filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);

        filterFactoryBean.setSecurityManager(securityManager);
        filterFactoryBean.setGlobalFilters(globalFilters());
        filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
        filterFactoryBean.setFilters(filterMap);

        return filterFactoryBean;
    }

这个filter会注册到容器的filter中来拦截请求,并使用上面的配置的filterChain来处理

先来一张上面这个登录请求的流程

下面是部分重点的 详细说明:

AbstractShiroFilter

这个类就是我们刚刚定义的ShiroFilterFactoryBean所实际生成的ShiroFilter bean的父类,大部分功能都是在这里买呢实现的.在这里主要做件事:

1.生成 Subject,并且存入ThreadLocal中,供之后的SecurityUtils调用获取,如果用户已经登录,获取到的也是登陆过的信息

2.根据我们前面的ShiroFilterChainDefinition配置,调用 PathMatchingFilterChainResolver 类生成匹配的filterChain 我们前面springboot默认配置的所有请求authc,那么就都是FormAuthenticationFilter,同时还有globalFilter,globalFilter意思就是所有请求都会有的过滤器,在这里会有一个默认的INVALID_FILTER,有兴趣可以自己看看

3.执行filterChain的所有filter

在这里我们可以自定义ShiroFilterChainDefinition匹配规则,按照前面的filter类型,举个例子

public ShiroFilterChainDefinition shiroFilterChainDefinition(){
        DefaultShiroFilterChainDefinition chainDefinition=new DefaultShiroFilterChainDefinition();
        // 登出功能
        chainDefinition.addPathDefinition("/logout","logout");
        // 错误页面无需认证
        chainDefinition.addPathDefinition("/error","anon");
        // druid连接池的角色控制,只有拥有admin角色的admin用户可以访问,不理解可以先不管
        chainDefinition.addPathDefinition("/druid/**","authc, roles[admin]");
        //基于权限的控制
        chainDefinition.addPathDefinition("/docs/**", "authc, perms[document:read]");
        // 静态资源无需认证
        chainDefinition.addPathDefinition("/static/**","anon");
        chainDefinition.addPathDefinition("/hello","anon");
        // 其余资源都需要认证
        chainDefinition.addPathDefinition("/**","authc");
        return chainDefinition;
    }

实际上就是在这里根据不同的路径匹配生成不同的filterChain

* 同时,我们可以在这里自定义这个ShiroFilter,添加我们自定义filter类型,或者全局filter,我们很多比较特殊的扩展功能都是基于自定义filter实现的,示例代码:

public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, ObjectMapper objectMapper) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        filterChainDefinitionMap.put("/auth/login", "authj");
        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
        filterChainDefinitionMap.put("/**", "authj");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        Map<String, Filter> filters = new HashMap<>();
        filters.put("authj", new JsonPayloadAuthenticationFilter(objectMapper));
        shiroFilterFactoryBean.setFilters(filters);

        return shiroFilterFactoryBean;

    }

这里我们定义了一个新的JsonPayloadAuthenticationFilter,并指定/auth/login的走这个filter

下面的这些filter都是继承层级关系,前面的一般都是父类,按照继承原理,调用的方法都是子有用子类的,没有就用父类的。

PathMatchingFilter

这个会对url进行再一次匹配验证,

AccessControlFilter

在这个类中就会对具体的权限进行校验,具体流程:

1.调用 子类的 isAccessAllowed 方法判断是否登录,FormAuthenticationFilter没有实现这个方法,所以调用的是AuthenticationFilter的,里面就是简单的判断

return subject.isAuthenticated() && subject.getPrincipal() != null;

如果是已登录的就会成功进入URL(注意:这里暂时没有权限的东西)

2.如果失败就会调用  onAccessDenied 方法,这个方法FormAuthenticationFilter是实现了的,所以进入子类方法

FormAuthenticationFilter

这里类中会做几个事:

1.判断当前url是否是登录url,这里的判断依据就是我们前面application.yml配置的或者通过自定义shiroFilterFactoryBean时,setLoginurl设置的。

2.如果是判断是否是post请求,post请求才能执行后续登录

3.执行  executeLogin 方法,这个方法是他的父类AuthenticatingFilter提供的,

4.父类的executeLogin方法会调用子类的createToken方法,这里也是FormAuthenticationFilter实现的,也就是从request中去取 username和password

protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String username = getUsername(request);
        String password = getPassword(request);
        return createToken(username, password, request, response);
    }

我们可以继承该类重写这个方法,来实现自己的token

5.之后会调用Subject的login方法,而Subject的login方法调用的是securityManager的login方法

6.最终会调用到  ModularRealmAuthenticator 的 doSingleRealmAuthentication方法

特别注意,如果只有一个realm才是调用这个方法,如果配置了多个realm 会调用另外的方法,并根据策略判断是否登录成功,这里不细讲了

Realm

1.最终doSingleRealmAuthentication调用的是Realm的 getAuthenticationInfo方法,这个方法是父类AuthenticatingRealm提供的,并且不允许重写,这个方法里调用了自定义的Realm的doGetAuthenticationInfo 方法,也就是我们很多教程所说的需要自定义实现的类,在我们的快速开始教程中在 ShiroAutoConfiguration 这个自动装配类中,如果我们有shiro.ini这个配置文件,会为我们自动生成一个 Realm,但是实际上,我们的用户数据一般是存在数据库中,所以需要自定义realm。

2.调用自定义realm后,会调用 org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch方法,判断密码是否相等,注意:在这个方法里面,如果你配置了 CredentialsMatcher 他会将你token中的密码按照这个matcher中的加密方式进行加密后再比较

一个基本的请求流程就是这样,里面还有一些旁路方法调用可以自己去看看

了解这个流程之后,我们来看里面的一些其他扩展点

扩展点

上面这个demo只是实现了一个很基础的实现,而且显示很多,比如:form数据的提交只能是application/x-www-form-urlencoded,才能获取到用户名密码 在真正的生产项目中,我们一般还会做很多改造,我们可以根据官方的starter来看看有哪些可以扩展的地方,这些扩展都有什么用,官方的自动配置文件可以在对应starter jar的 META-INF/spring.factory中看到,大部分可以通过覆盖他的bean实现,这里只讲一些重要的

这是shiro-spring-boot-stater的
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
  org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration,\
  org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration,\
  org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration

这是shiro-spring-boot-web-starter的
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,\
  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration

自定义filter

上面提到过,在定义ShiroFilterFactoryBean的时候,可以添加自己的filter,并制定对应的路径映射到自定义的filter上,我们一般会继承已有的这些filter或者上层的抽象filter,比如我的例子里面,需要读取application/json类型的username和password,就重写了一个类:

@RequiredArgsConstructor
public class JsonPayloadAuthenticationFilter extends AuthenticatingFilter {

    private final ObjectMapper objectMapper;

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        String data = IOUtils.toString(request.getReader());
        SystemUser systemUser = objectMapper.readValue(data, SystemUser.class);
        return createToken(systemUser.getUsername(), systemUser.getPassword(), request, response);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response) && isLoginSubmission(request)) {
            return executeLogin(request, response);
        } else {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setCharacterEncoding("UTF-8");
            httpResponse.setContentType("application/json;charset=UTF-8");
            httpResponse.setStatus(400);
            String json = objectMapper.writeValueAsString(AjaxResponse.UN_AUTH);
            httpResponse.getWriter().print(json);
            return false;
        }
    }

    private boolean isLoginSubmission(ServletRequest request) {
        return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
    }


}

这里还有一种方法是直配置登录的url匹配方式为anon,也就是不过滤,然后通过spring获取参数,然后手动构造token,并手动调用subject的login方法,大多数教程里是在这么写的,我这里只是将原理,而且这种方式相对更优雅

自定义REALM

从上面的时序图可以看到,最终的login调用的是subject‘的login,subject的login调用的是securityManager的login,然后调用到Authenticator,然后调用了Realm的获取用户信息并进行对比。我们一般需要自己定影realm,然后从数据库查用户信息。

密码加密

上面的时序图讲到了,对比的时候调用的CredentialsMatcher的对比方法,所以我们的密码如果是加密存在数据库中的,可以定义一个加密对比bean,例子:

@Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列的次数,比如散列两次,相当于 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);
        // storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

此时需要注意,realm返回的用户信息的密码一定是和上述加密结果一致的 也是提前加密好了的,并且可以设置盐值

其他

TODO补充

前后端分离项目集成shiro

一直没搞懂为什么有的人做这种类型的集成,换成前后端分离的项目就不知道怎么弄了,前后端分离和不分离,对于后端来说是没什么区别的,返回给前端的其实都是一组字符,只是不分离的项目返回的是一段html,分离的项目返回的是我们自己定义的内容

而且,前后端分离的项目也不是一定就只能用token来做认证,用session还是一样的。只是如果不用网关吧前后端弄成相同的域名的话会有跨域的问题,跨域的解决看下面

如果登录成功了就返回一个AjaxResult对象就行

spring集成shiro跨域问题

使用cors的方案

public class CorsFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("CorsFilter init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;

        if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
            response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
            response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, If-Modified-Since, x-token, Cookie, access-token");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.addHeader("Access-Control-Allow-Credentials", "true");
            response.setStatus(HttpStatus.OK.value());
            return;
        }

        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, If-Modified-Since, x-token, Cookie, access-token");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.addHeader("Access-Control-Allow-Credentials", "true");

        filterChain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }
}

shiro集成其他认证方式

不管是集成自己的token还是其他第三方的统一认证,如果对上面讲的比较清楚了,其实我们只需要知道在两个扩展点进行扩展就行了

生成认证token

新加一个自己的filter,filter集成抽象类或者某个比较适合的类,重写createToken方法,举个例子,如果是JWT,直接从request header中取到他并用自己的方法解析就行了

验证token

如果是基于oauth2等统一认证中心,重写onAccessDenied方法,拿到上面的token后,调用统一认证中心进行校验就行了(第一次校验后可以吧用户信息和登录结果存到自己的系统里面,基于session或你自己的token)

总结

参考

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
课程简介:历经半个多月的时间,Debug亲自撸的 “企业员工角色权限管理平台” 终于完成了。正如字面意思,本课程讲解的是一个真正意义上的、企业级的项目实战,主要介绍了企业级应用系统中后端应用权限的管理,其中主要涵盖了六大核心业务模块、十几张数据库表。 其中的核心业务模块主要包括用户模块、部门模块、岗位模块、角色模块、菜单模块和系统日志模块;与此同时,Debug还亲自撸了额外的附属模块,包括字典管理模块、商品分类模块以及考勤管理模块等等,主要是为了更好地巩固相应的技术栈以及企业应用系统业务模块的开发流程! 核心技术栈列表: 值得介绍的是,本课程在技术栈层面涵盖了前端和后端的大部分常用技术,包括Spring Boot、Spring MVC、Mybatis、Mybatis-Plus、Shiro(身份认证与资源授权跟会话等等)、Spring AOP、防止XSS攻击、防止SQL注入攻击、过滤器Filter、验证码Kaptcha、热部署插件Devtools、POI、Vue、LayUI、ElementUI、JQuery、HTML、Bootstrap、Freemarker、一键打包部署运行工具Wagon等等,如下图所示: 课程内容与收益: 总的来说,本课程是一门具有很强实践性质的“项目实战”课程,即“企业应用员工角色权限管理平台”,主要介绍了当前企业级应用系统中员工、部门、岗位、角色、权限、菜单以及其他实体模块的管理;其中,还重点讲解了如何基于Shiro的资源授权实现员工-角色-操作权限、员工-角色-数据权限的管理;在课程的最后,还介绍了如何实现一键打包上传部署运行项目等等。如下图所示为本权限管理平台的数据库设计图: 以下为项目整体的运行效果截图: 值得一提的是,在本课程中,Debug也向各位小伙伴介绍了如何在企业级应用系统业务模块的开发中,前端到后端再到数据库,最后再到服务器的上线部署运行等流程,如下图所示:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值