安全管理之Shiro
本篇来学习OAuth2和Shiro相关知识以如何在SpringBoot框架中使用它们。
OAuth2
OAuth2简介
OAuth是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等〉,而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。例如,用户想通过QQ登录知乎,这时知乎就是一个第三方应用,知乎要访问用户的一些基本信息就需要得到用户的授权,如果用户把自己的QQ用户名和密码告诉知乎,那么知乎就能访问用户的所有数据,并且只有用户修改密码才能收回授权,这种授权方式安全隐患很大,如果使用OAuth,就能很好地解决这一问题。
采用令牌的方式可以让用户灵活地对第三方应用授权或者收回权限。OAuth2是OAuth协议的下一版本,但不向下兼容OAuth1.0 。OAuth2关注客户端开发者的简易性,同时为Web 应用、桌面应用、移动设备、起居室设备提供专门的认证流程。传统的Web开发登录认证一般都是基于Session的,但是在前后端分离的架构中继续使用Session会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持Cookie(微信小程序),要么使用非常不便,对于这些问题,使用OAuth2认证都能解决。因此OAuth2对于解决信息授权与认证有非常大的帮助。
OAuth2角色
在学习OAuth2之前,了解OAuth2中的4种基本角色对于学习和理解OAuth的工作原理有重要意义。(1)资源所有者:资源所有者即用户,具有头像、照片、视频等资源;(2)客户端:客户端即第三方应用,如上方提到的知乎;(3)授权服务器:授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。(4)资源服务器:资源服务器是提供给用户资源的服务器,如头像、照片、视频等资源。
通常来说,授权服务器和资源服务器可以是同一台服务器。
OAuth2授权流程
在熟悉了OAuth2中的4个基本角色以后,接下来开始学习OAuth2的授权流程,具体的流程如下:(1)客户端(第三方应用)向用户请求授权;(2)用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端;(3)客户端拿着授权许可凭证去授权服务器申请令牌;(4)授权服务器验证信息无误后,发放令牌给客户端;(5)客户端拿着令牌去资源服务器访问资源;(6)资源服务器验证令牌无误后开放资源访问。
上述是一个大致的流程,因为OAuth2有4种不同的授权方式,每种授权模式的授权流程又存在一定的差异,不过大致流程如下图所示:
授权模式
OAuth协议的授权模式一共有4种,接下来简要介绍一下:
- 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本上都是使用这种模式。
- 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌,一般若网站是纯静态页面,则可以采用这种方式。
- 密码模式:密码模式是用户把用户密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌。这需要用户对客户端高度信任,如客户端和服务提供商是同一家公司。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务器提供者申请授权。严格来说,客户端模式并不能算作OAuth协议要解决的问题的一种解决方案,但是对于开发者而言,在一些前端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是较为方便的。
这四种模式各有各自特点,分别适用于不同的开发场景,开发者需要结合使用情况进行选择。
OAuth2实践
本例要介绍的是在前后端分离应用(移动端、微信小程序等)提供的认证服务器中如何搭建OAuth服务,因此主要介绍密码模式。相应的搭建步骤如下:
第一步,创建项目和添加依赖。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为oauth2springboot
,然后添加如下依赖:
1 | <!--添加spring security依赖--> |
由于SpringBoot中的OAuth协议是在Spring Security的基础上完成的,因此首先需要添加Spring Security依赖,要使用OAuth2,因此需要先添加OAuth2相关依赖,令牌可以存储在Redis缓存服务器上,同时Redis具有过期等功能,非常适合令牌的存储,因此也需要添加Redis依赖。
项目创建成功后,接下来在在application.properties
文件中添加Redis的链接信息:
1 | # 基本连接信息配置 |
第二步,配置授权服务器。授权服务器和资源服务器可以是同一台服务器,也可以不是,本例子假设是同一台服务器,通过不同的配置分别开启授权服务器和资源服务器。新建config包,并在其中创建AuthorizationServerConfig
类,注意这个类需要实现AuthorizationServerConfigurerAdapter
类(注意使用这个类时需要指定spring-security-oauth2
的版本,否则会找不到该类),里面的代码为:
1 | /**授权服务器**/ |
解释一下上述代码的含义:
- 首先自定义
AuthorizationServerConfig
类需要继承AuthorizationServerConfigurerAdapter
类,完成对授权服务器的配置,然后通过@EnableAuthorizationServer
注解开启授权服务器。 - 接着注入
AuthenticationManager
对象,用来支持password模式。注入RedisConnectionFactory
对象用来完成Redis缓存,将令牌信息存储到Redis缓存中。注入UserDetailsService
对象,该对象将为刷新token提供技术支持。passwordEncoder
方法用于返回一个加密后的密码对象,这个和前面设置一样。 configure(ClientDetailsServiceConfigurer clients)
方法用于配置password授权模式,authorizedGrantTypes
表示OAuth2中的授权模式为”password”和”refresh_token”两种。在标准的OAuth2协议中,授权模式并不包括”refresh_token”,但是在SpringSecurity的实现中将其归为一种,因此如果要实现”access_token”的刷新,就需要添加这样一种授权模式;accessTokenValiditySeconds
方法设置了”access_token”的过期时间;resourceIds
配置了资源id;secret
方法配置了加密后的密码,明文是1234。configure(AuthorizationServerEndpointsConfigurer endpoints)
方法配置了令牌的存储,AuthenticationManager
和UserDetailsService
主要用于支持password模式以及令牌的刷新。configure(AuthorizationServerSecurityConfigurer security)
方法表示支持client_id
和client_secret
做登录认证。
第三步,配置资源服务器。在config包,内创建ResourceServerConfig
类,注意这个类需要实现ResourceServerConfigurerAdapter
类,里面的代码为:
1 | /**资源服务器**/ |
解释一下上述代码的含义:
- 首先自定义
ResourceServerConfig
类需要继承ResourceServerConfigurerAdapter
类,完成对资源服务器的配置,然后通过@EnableResourceServer
注解开启资源服务器配置。 - 接着使用
resources.resourceId("rid").stateless(true);
来配置资源id,这里的资源id和授权服务器中的资源id需要保持一致,然后设置这些资源仅基于令牌认证。之后就是配置HttpSecurity
,里面定义一些URL规则等,这个和前面的配置几乎一致,因此也就不再赘述了。
第四步,配置Security。新建WebSecurityConfig
类,注意它需要继承WebSecurityConfigurerAdapter
类,里面的代码为:
1 | /**Spring Security相关配置**/ |
可以发现这里SpringSecurity的配置和前面的配置基本上一致,只是多了两个Bean,这两个Bean将注入授权服务器配置类中使用。另外这里的HttpSecurity
配置主要是配置/oauth/**
模式的URL,这一类的请求直接放行。在SpringSecurity配置和资源服务器配置中,一共涉及到两个HttpSecurity
,其中SpringSecurity中的配置优先级高于资源服务器中的配置,即请求地址先经过SpringSecurity中的HttpSecurity
,再经过资源服务器的HttpSecurity
。
第五步,验证测试。新建HelloController类,其中的代码为:
1 | @RestController |
根据前面的配置可知,想成功访问这三个链接需要admin角色、user角色和登录后访问。
当所有的配置都完成以后,先启动Redis服务器,然后再启动SpringBoot项目。首先发送一个POST请求,用于获取token,请求地址如下(注意这是一个POST请求,为了显示方便将参数写在地址栏中):
1 | http://localhost:8080/oauth/token?username=envy&password=1234&grant_type=password&client_id=password&scope=all&client_secret=1234 |
可以看到该请求地址中包含了用户名、密码、授权模式、客户端id、scope以及客户端密码,这些基本上就是授权服务器内配置的数据。当该请求访问成功后,页面返回一段JOSN信息:
而这个返回结果就包含了5项信息:access_token、token_type、refresh_token、expires_in和scope。其中access_token是获取其他资源时需要使用到的令牌,token_type是令牌类型,refresh_token用来刷新令牌,expires_in表示access_token的过期时间,当access_token过期后,使用refresh_token重新获取新的access_token(前提是refresh_token未过期),请求地址如下(注意这里也是POST请求):
1 | http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=1710f670-4843-4886-bc81-5df9500d73b2&client_id=password&client_secret=1234 |
注意到获取新的access_token时需要携带refresh_token,同时授权模式设置为refresh_token,在获取的结果中access_token也会变化,同时access_token的有效期也会变化,如下图所示:
接下来访问所有的资源时,只需要携带access_token参数即可,如/user/hello
接口:
1 | http://localhost:8080/user/hello?access_token=1df2dc6f-51e0-44c1-9c64-2546990617c1 |
访问结果如下图所示:
如果非法访问一个资源,如envy用户访问/admin/hello
接口时,页面就会返回一个无法请求的信息提示:
现在来查看一下redis的数据:
这样一个password模式的OAuth认证体系就搭建完成了。
OAuth中的认证模式有4种,开发者需要结合实际情况来选择其中的一种,本例只是介绍了在前后端分离应用中常见的password模式,其他的授权模式也都有自己的使用场景,这些笔者都会在后续文章中依次进行介绍。
其实通过前一篇和本篇到目前的学习,开发者可能也意识到了Spring Security OAuth2的使用还是较复杂的,配置也比较繁琐,如果开发者的使用场景比较简单,完全可以按照前面介绍的授权流程来搭建属于自己的OAuth2认证体系。如果你之前使用过微信支付,可以发现这个流程其实非常相似,且获取用户信息的方式也很类似这里就不介绍了,有兴趣的可以自行去学习。
Shiro
Shiro简介
Apache Shiro是一个开源的轻量级的Java安全框架,它提供身份验证、授权、密码管理以及会话管理等功能。相对于Spring Security, Shiro框架更加直观、易用,同时也能提供健壮的安全性。在传统的SSM框架中,手动整合Shiro时需要较为繁琐的配置步骤。而针对SpringBoot,Shiro官方提供了shiro-spring-boot-web-starter
来简化Shiro在SpringBoot中的配置。接下来介绍shiro-spring-boot-web-starter
的使用步骤。
SpringBoot整合Shiro框架
第一步,创建SpringBoot Web项目并添加依赖。使用spring Initializr
构建工具构建一个SpringBoot的Web应用,名称为shirospringboot
,然后在pom.xml文件中添加Shiro依赖以及页面模板引擎依赖,代码为:
1 | <!--添加Shiro依赖--> |
特别注意这里不需要添加spring-boot-starter-web
依赖,因为shiro-spring-boot-web-starter
中已经依赖了spring-boot-starter-web
依赖。同时本案例使用了Thymeleaf模板,因此需要添加Thymeleaf依赖,另外为了在Thymeleaf中使用shiro标签,因此需要引入thymeleaf-extras-shiro
依赖。
第二步,Shiro基本配置。首先在application.properties
配置文件中配置Shiro的基本信息,代码如下所示:
1 | # 开启Shiro配置,默认为true |
Shiro基本信息配置完成后,接下来在Java代码中配置Shiro,只需提供两个最基本的Bean即可。新建config包,并在其中创建ShiroConfig类,其中的代码为:
1 | @Configuration |
解释一下上述代码的含义:
- 这里提供了两个关键Bean,一个所示Realm,另一个是
ShiroFilterChainDefinition
。至于ShiroDialect
则是为了支持在Thymeleaf中使用Shiro标签,如果不在Thymeleaf中使用Shiro标签,那么可以不提供ShiroDialect
。 - Realm可以是自定义的Realm,也可以是Shiro提供的Realm,简单起见这里没有配置数据库连接,这里直接配置了两个用户:envy/1234和admin/1234,分别对应角色user和admin,其中user角色只具有read权限,而admin角色拥有read和write权限。
ShiroFilterChainDefinition
方法中配置了基本的过滤规则,/login
和/doLogin
可以匿名访问,/logout
是一个注销登录的请求,其余的请求都需要认证后才能访问。
第三步,新建controller类。接下来就是配置登录接口以及页面访问接口。新建一个controller包,并在其中创建UserController
类,里面的代码为:
1 | @Controller |
简单解释一下上述代码的含义:
- 在
doLogin
方法中,首先构造一个UsernamePasswordToken的实例,然后获取到一个Subject对象,并调用该对象中的login方法执行登录操作,在登录操作执行过程中,当有异常抛出时,说明登录失败,页面需要携带信息并返回给登录视图;当登录成功时,则重定向到/index
接口。 - 接下来暴露两个接口
/admin
和/user
,对于/admin
接口来说需要具有admin角色的用户才能访问;而对于/user
接口而言,具备admin或者user角色中的任意一个即可访问,因此需要使用Logical.OR
来表示这种逻辑或关系。 - 请注意由于这里是使用模板引擎,因此需要使用
@Controller
注解,而不是@RestController
注解,这一点需要注意。
对于其他不需要角色就能访问的接口,直接定义在WebMvc中是最佳选择。新建一个WebMvcConfig
类,注意它需要实现WebMvcConfigurer
接口,并重写其中的addViewControllers
方法,其中的代码为:
1 | @Configuration |
这里就设置了三个URL,login、index和authorized页面及视图名称,访问这些URL是不需要经过controller控制器的。
第四步,新建异常处理类。接着创建全局异常处理器进行全局异常处理,本例子主要是处理授权异常。新建一个ExceptionController
类,其中的代码为:
1 | @ControllerAdvice |
当用户访问位授权的资源时,会自动跳转到unauthorized视图,并携带相应的出错信息。
第五步,新建对应的模板页面。当上述信息均配置完成时,接下来在resources/templates
目录下创建5个HTML页面用于后续测试。
(1)新建index.html
页面,其中的代码为:
1 | <!DOCTYPE html> |
index.html
是登录成功后的首页,首先展示当前登录用户的用户名,然后展示一个“注销登录”链接,若当前登录用户具备“admin”角色,则展示一个“管理员页面”的超链接;若当前登录用户具备“admin”或者“user”角色,则展示一个“普通用户页面”的超链接。注意这里导入的名称空间是xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
和在JSP页面中导入的Shiro名称空间不一致。
(2)新建login.html
页面,其中的代码为:
1 | <!DOCTYPE html> |
login.html
是一个普通的登录页面,在登录失败时通过一个div来显示登录失败的信息。
(3)新建user.html
页面,其中的代码为:
1 | <!DOCTYPE html> |
user.html
是一个普通的用户信息展示页面。
(4)新建admin.html
页面,其中的代码为:
1 | <!DOCTYPE html> |
admin.html
是一个管理员的信息展示页面。
(5)新建unauthorized.html
页面,其中的代码为:
1 | <!DOCTYPE html> |
unauthorized.html
是一个授权失败的信息展示页面,该页面还会展示授权出错的信息。
第六步,测试。当上述信息均配置完成时,启动SpringBoot项目,访问登录页面,分别使用envy/1234和admin/1234进行登录,结果如下图所示:
注意因为envy用户不具备admin角色,因此登录成功后的页面是上没有前往管理员页面的超链接。
登录成功后,无论是envy还是admin用户,单机“注销登录”都会注销成功,然后回到登录页面,envy用户因为不具备admin角色,因此登录成功后的页面是上没有前往管理员页面的超链接,无法进入到管理员页面中。此时若开发者使用envy用户登录,然后手动在浏览器地址栏中输入http://localhost:8080/admin
,则页面会跳转到未授权页面,如下图所示:
以上通过一个简单的例子学习了如何在SpringBoot中整合Shiro以及如何在Thymeleaf中使用Shiro标签,一旦整合成功,接下来Shiro的用法就和原来的一模一样。注意这里仅仅是介绍了SpringBoot整合Shiro,对于Shiro的其他用法这里未介绍,后续会出一些文章来介绍关于Shiro的用法。
安全管理小结
在第十四篇中学习了如何在SpringBoot中整合SpringSecurity,而在第十五篇中学习了SpringBoot中整合Shiro。对于SpringSecurity,有基于传统认证方式的Session认证,也有使用OAuth2的认证。一般来说,在传统的Web架构中,使用Session认证方便快速,但是如果结合微服务、前后端分离等架构,则使用OAuth认证更加方便,具体使用哪一种,需要开发者根据实际情况进行选择。对于Shiro来说,虽然功能不及SpringSecurity强大,但是简单易用,而且也能胜任大部分的中小型项目。当然,在SpringBoot 项目中, Spring Security的整合显然要更加容易,因此可以首选Spring Security。如果开发团队对Spring Security不熟悉,但是熟悉Shiro的使用,当然也可以使用Shiro,这个要结合具体情况来定。但是无论是使用哪种框架,都是为了保证项目的安全,这一点需要引起高度重视。