OAuth2.0 技术
- Spring Security OAuth2
- keycloak
- Nimbus OAuth
- Apache Oltu
- vertx-auth-oauth2
https://www.felord.cn/categories/spring-security/
https://mp.weixin.qq.com/s/dywak06zI4d_LU0s9haVLg
https://mp.weixin.qq.com/s/2i7K9hq7LCytlzOjCl1bXA
https://github.com/linlinjava/litemall
https://github.com/shenzhuan/zscat-me
https://github.com/Mynameisfwk/vivo-shop
https://github.com/paascloud
https://github.com/paascloud/paascloud-master
https://gitee.com/owenwangwen/open-capacity-platform
https://gitee.com/zlt2000/microservices-platform
https://github.com/Ewall1106/mall
https://github.com/newbee-ltd/newbee-mall
https://github.com/macrozheng/mall-admin-web
https://github.com/macrozheng/mall-swarm
https://github.com/macrozheng/mall-learning
https://github.com/lenve/vhr
https://github.com/YunaiV/onemall
https://niocoder.com/
https://niocoder.com/2017/02/17/Spring-atomikos/
https://niocoder.com/categories/#Security
kubernetes
https://github.com/gottaBoy/sealos
https://sealyun.com/github/
Apache
Kylin
https://mp.weixin.qq.com/s/1tlytTG63xDyZyrlyM_zpA
https://mp.weixin.qq.com/s/EgZSEpc7SHl5kg6WLSQdPQ
前言
Java web领域经常提及的两大开源框架主要有两种选择Spring Security和Apache Shiro
Spring Security 和 Apache Shiro
相对于Apache Shiro,Spring Security提供了更多的诸如LDAP、OAuth2.0、ACL、Kerberos、SAML、SSO、OpenID等诸多的安全认证、鉴权协议,可以按需引用。对认证/鉴权更加灵活,粒度更细。可以结合你自己的业务场景进行更加合理的定制化开发。在最新的Spring Security 5.x中更是提供了响应式应用(reactive application)提供了安全控制支持。从语言上来讲,支持使用kotlin、groovy进行开发。
Spring Security因为是利用了Spring IOC 和AOP的特性而无法脱离Spring独立存在。而Apache Shiro可以独立存在。但是Java Web领域Spring可以说是事实上的J2EE规范。使用Java技术栈很少能脱离Spring。也因为功能强大Spring Security被认为非常重,这是不对的。认真学习之后会发现其实也就是那么回事。两种框架都是非常优秀的安全框架,根据实际需要做技术选型。如果你要学习这两种安全框架就必须熟悉一下一些相对专业的概念。
认证/鉴权
这两个概念英文分别为authentication/authorization 。是不是特别容易混淆。无论你选择Apache Shiro 或者 Spring Security 都需要熟悉这两个概念。其实简单来说认证(authentication)就是为了证明你是谁,比如你输入账号密码证明你是用户名为iching的用户。而授权(authorization)是通过认证后的用户所绑定的角色等凭证来证明你可以做什么 。打一个现实中的例子。十一长假大家远行都要乘坐交通工具,现在坐车实名制,也就是说你坐车需要两件东西:身份证和车票 。身份证是为了证明你确实是你,这就是 authentication;而车票是为了证明你张三确实买了票可以上车,这就是 authorization。这个例子从另一方面也证明了。如果只有认证没有授权,认证就没有意义。如果没有认证,授权就无法赋予真正的可信任的用户。两者是同时存在的。
过滤器链
对于servlet web应用来说,想要通用的安全控制最好莫过于使用Servlet Filter 。 过滤器责任链来组成一系列的过滤策略,不同的条件的请求进入不同的过滤器进行各自的处理逻辑。我们可以对这些Filter 进行排列组合以满足我们的实际业务需要。
RBAC模型
RBAC 是基于角色的访问控制(Role-Based Access Control )的简称。在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。当你拥有某个角色以后,你自然继承了该角色的所有功能。对你的一些操作限制不需要直接与你进行沟通,只需要操作你拥有的角色。比如你在公司既是一个java程序员又是一个前端程序员,那么你不但要当sqlboy还要当页面仔。如果有一天经理说了前端负责测试工作,好了你又承担了测试任务。
其他一些概念
比如其它一些常见的安全策略、攻击方式。比如 反向代理、网关、壁垒机这种偏运维的知识;CSRF(Cross-site request forgery)跨站请求伪造 、XSS(跨站脚本攻击)也需要了解一些。对于一些上面提到的什么OAuth2.0之类的协议也最好研究一下。当然这些不是必须的。
总结
本文粗略的简述了Spring Security 和Apache Shiro的一些异同。以及学习它们的一些前置条件。如果你不满足这些条件学习起来可能比较吃力。所以本文的作用是为你学习预热,做一些准备工作,避免新入门的同学陷入迷途。也希望大家多多支持,多多关注。
用户信息UserDetails相关入门
1. 前言
前一篇介绍了 Spring Security 入门的基础准备。从今天开始我们来一步步窥探它是如何工作的。我们又该如何驾驭它。请多多关注公众号: iching 。本篇将通过 Spring Boot 2.x 来讲解 Spring Security 中的用户主体UserDetails。以及从中找点乐子。
2. Spring Boot 集成 Spring Security
这个简直老生常谈了。不过为了照顾大多数还是说一下。集成 Spring Security 只需要引入其对应的 Starter 组件。Spring Security 不仅仅能保护Servlet Web 应用,也可以保护Reactive Web应用,本文我们讲前者。我们只需要在 Spring Security 项目引入以下依赖即可:
1 | <dependencies> |
3. UserDetailsServiceAutoConfiguration
启动项目,访问Actuator端点http://localhost:8080/actuator会跳转到一个登录页面http://localhost:8080/login如下:
#TODO
要求你输入用户名 Username (默认值为user)和密码 Password 。密码在springboot控制台会打印出类似 Using generated security password: e1f163be-ad18-4be1-977c-88a6bcee0d37 的字样,后面的长串就是密码,当然这不是生产可用的。如果你足够细心会从控制台打印日志发现该随机密码是由UserDetailsServiceAutoConfiguration 配置类生成的,我们就从它开始顺藤摸瓜来一探究竟。
3.1 UserDetailsService
UserDetailsService接口。该接口只提供了一个方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
该方法很容易理解:通过用户名来加载用户 。这个方法主要用于从系统数据中查询并加载具体的用户到Spring Security中。
3.2 UserDetails
从上面UserDetailsService 可以知道最终交给Spring Security的是UserDetails 。该接口是提供用户信息的核心接口。该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象Authentication中去。UserDetails 默认提供了:
用户的权限集, 默认需要添加ROLE_ 前缀
用户的加密后的密码, 不加密会使用{noop}前缀
应用内唯一的用户名
账户是否过期
账户是否锁定
凭证是否过期
用户是否可用
如果以上的信息满足不了你使用,你可以自行实现扩展以存储更多的用户信息。比如用户的邮箱、手机号等等。通常我们使用其实现类:
org.springframework.security.core.userdetails.User
该类内置一个建造器UserBuilder 会很方便地帮助我们构建UserDetails 对象,后面我们会用到它。
3.3 UserDetailsServiceAutoConfiguration
UserDetailsServiceAutoConfiguration 全限定名为:
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
源码如下:
1 | @Configuration |
我们来简单解读一下该类,从@Conditional系列注解我们知道该类在类路径下存在AuthenticationManager、在Spring 容器中存在Bean ObjectPostProcessor并且不存在Bean AuthenticationManager, AuthenticationProvider, UserDetailsService的情况下生效。千万不要纠结这些类干嘛用的! 该类只初始化了一个UserDetailsManager 类型的Bean。UserDetailsManager 类型负责对安全用户实体抽象UserDetails的增删查改操作。同时还继承了UserDetailsService接口。
明白了上面这些让我们把目光再回到UserDetailsServiceAutoConfiguration 上来。该类初始化了一个名为InMemoryUserDetailsManager 的内存用户管理器。该管理器通过配置注入了一个默认的UserDetails存在内存中,就是我们上面用的那个user ,每次启动user都是动态生成的。那么问题来了如果我们定义自己的UserDetailsManager Bean是不是就可以实现我们需要的用户管理逻辑呢?
3.4 自定义UserDetailsManager
我们来自定义一个UserDetailsManager 来看看能不能达到自定义用户管理的效果。首先我们针对UserDetailsManager 的所有方法进行一个代理的实现,我们依然将用户存在内存中,区别就是这是我们自定义的:
1 | public class UserDetailsRepository { |
该类负责具体对UserDetails 的增删改查操作。我们将其注入Spring 容器:
1 | @Bean |
为了方便测试 我们也内置一个名称为iching 密码为12345的UserDetails用户,密码采用明文 当你在密码123456上使用了前缀{noop} 意味着你的密码不使用加密,这里我们并没有指定密码加密方式你可以使用PasswordEncoder 来指定一种加密方式。通常推荐使用Bcrypt作为加密方式。默认Spring Security使用的也是此方式。authorities 一定不能为null 这代表用户的角色权限集合。接下来我们实现一个UserDetailsManager 并注入Spring 容器:
1 | @Bean |
这样实际执行委托给了UserDetailsRepository 来做。我们重复 章节3. 的动作进入登陆页面分别输入iching和123456 成功进入。
3.5 数据库管理用户
经过以上的配置,相信聪明的你已经知道如何使用数据库来管理用户了 。只需要将 UserDetailsRepository 中的 users 属性替代为抽象的Dao接口就行了,无论你使用Jpa还是Mybatis来实现。
4. 总结
今天我们对Spring Security 中的用户信息 UserDetails 相关进行的一些解读。并自定义了用户信息处理服务。相信你已经对在Spring Security中如何加载用户信息,如何扩展用户信息有所掌握了。后面我们会由浅入深慢慢解读Spring Security。
Spring Boot 中的 Spring Security 自动配置初探
前言
我们在前几篇对 Spring Security 的用户信息管理机制,密码机制进行了探讨。我们发现 Spring Security Starter相关的 Servlet 自动配置都在spring-boot-autoconfigure-2.1.9.RELEASE(当前 Spring Boot 版本为2.1.9.RELEASE) 模块的路径org.springframework.boot.autoconfigure.security.servlet 之下。其实官方提供的Starter组件的自动配置你都能在spring-boot-autoconfigure-2.1.9.RELEASE下找到。今天我们进一步来解密 Spring Security 在 Spring Boot 的配置和使用。Spring Boot 下 Spring Security 的自动配置
我们可以通过 org.springframework.boot.autoconfigure.security.servlet 路径下找到 Spring Security 关于Servlet的自动配置类。我们来大致了解一下。
2.1 SecurityAutoConfiguration
1 | package org.springframework.boot.autoconfigure.security.servlet; |
SecurityAutoConfiguration 顾名思义安全配置类。该类引入(@import)了 SpringBootWebSecurityConfiguration、WebSecurityEnablerConfiguration 和 SecurityDataConfiguration 三个配置类。 让这三个模块的类生效。是一个复合配置,是 Spring Security 自动配置最重要的一个类之一。 Spring Boot 自动配置经常使用这种方式以达到灵活配置的目的,这也是我们研究 Spring Security 自动配置的一个重要入口 同时 SecurityAutoConfiguration 还将 DefaultAuthenticationEventPublisher 作为默认的 AuthenticationEventPublisher 注入 Spring IoC 容器。如果你熟悉 Spring 中的事件机制你就会知道该类是一个 Spring 事件发布器。该类内置了一个HashMap<String, Constructor<? extends AbstractAuthenticationEvent>>维护了认证异常处理和对应异常事件处理逻辑的映射关系,比如账户过期异常 AccountExpiredException 对应认证过期事件AuthenticationFailureExpiredEvent ,也就是说发生不同认证的异常使用不同处理策略。
2.2 SpringBootWebSecurityConfiguration
1 | @Configuration |
这个类是Spring Security 对 Spring Boot Servlet Web 应用的默认配置。核心在于WebSecurityConfigurerAdapter 适配器。从 @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) 我们就能看出 WebSecurityConfigurerAdapter 是安全配置的核心。 默认情况下 DefaultConfigurerAdapter 将以SecurityProperties.BASIC_AUTH_ORDER(-5) 的顺序注入 Spring IoC 容器,这是个空实现。如果我们需要个性化可以通过继承 WebSecurityConfigurerAdapter 来实现。我们会在以后的博文重点介绍该类。
2.3 WebSecurityEnablerConfiguration
1 | @Configuration |
该配置类会在SpringBootWebSecurityConfiguration 注入 Spring IoC 容器后启用 @EnableWebSecurity 注解。也就是说 WebSecurityEnablerConfiguration 目的仅仅就是在某些条件下激活 @EnableWebSecurity 注解。那么这个注解都有什么呢?
- @EnableWebSecurity 注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}
@Enable* 这类注解都是带配置导入的注解。通过导入一些配置来启用一些特定功能。 @EnableWebSecurity 导入了 WebSecurityConfiguration 、SpringWebMvcImportSelector 、OAuth2ImportSelector 以及启用了 @EnableGlobalAuthentication注解。
3.1 WebSecurityConfiguration
该配置类WebSecurityConfiguration使用一个WebSecurity对象基于用户指定的或者默认的安全配置,你可以通过继承 WebSecurityConfigurerAdapter 或者实现 WebSecurityConfigurer 来定制 WebSecurity 创建一个FilterChainProxy Bean来对用户请求进行安全过滤。这个FilterChainProxy的名称就是 WebSecurityEnablerConfiguration上的 BeanIds.SPRING_SECURITY_FILTER_CHAIN 也就是 springSecurityFilterChain,它是一个Filter,最终会被作为Servlet过滤器链中的一个Filter应用到Servlet容器中。安全处理的策略主要是过滤器的调用顺序。WebSecurityConfiguration 最终会通过 @EnableWebSecurity 应用到系统。
源码分析:
1 | package org.springframework.security.config.annotation.web.configuration; |
3.2 SpringWebMvcImportSelector
该类是为了对 Spring Mvc 进行支持的。一旦发现应用使用 Spring Mvc 的核心前置控制器 DispatcherServlet 就会引入 WebMvcSecurityConfiguration 。主要是为了适配 Spring Mvc 。
3.3 OAuth2ImportSelector
该类是为了对 OAuth2.0 开放授权协议进行支持。ClientRegistration 如果被引用,具体点也就是 spring-security-oauth2 模块被启用(引入依赖jar)时。会启用 OAuth2 客户端配置 OAuth2ClientConfiguration 。
3.4 @EnableGlobalAuthentication
这个类主要引入了 AuthenticationConfiguration 目的主要为了构造 认证管理器 AuthenticationManager 。AuthenticationManager 十分重要后面我们会进行专门的分析。
- SecurityFilterAutoConfiguration
我们在 org.springframework.boot.autoconfigure.security.servlet 路径下还发现了一个配置类 SecurityFilterAutoConfiguration 。该类用于向Servlet容器注册一个名称为securityFilterChainRegistration的bean, 实现类是DelegatingFilterProxyRegistrationBean。该 bean 的目的是注册另外一个 Servlet Filter Bean 到 Servlet 容器,实现类为 DelegatingFilterProxy 。DelegatingFilterProxy 其实是一个代理过滤器,它被 Servlet 容器用于处理请求时,会将任务委托给指定给自己另外一个Filter bean。对于 SecurityFilterAutoConfiguration,来讲,这个被代理的Filter bean的名字为 springSecurityFilterChain , 也就是我们上面提到过的 Spring Security Web提供的用于请求安全处理的Filter bean,其实现类是 FilterChainProxy。
相关的源码分析:
1 | package org.springframework.boot.autoconfigure.security.servlet; |
路径Uri中的 Ant 风格
前言
我们经常在读到一些文章会遇到uri 支持 Ant 风格 ,而且这个东西在 Spring MVC 和 Spring Security 中经常被提及。这到底是什么呢?今天我们来学习了解一下。这对我们学习 Spring MVC 和 Spring Security 十分必要。Ant 风格
说白了 Ant 风格就是一种路径匹配表达式。主要用来对uri的匹配。其实跟正则表达式作用是一样的,只不过正则表达式适用面更加宽泛,Ant仅仅用于路径匹配。Ant 通配符
Ant 中的通配符有三种:
? 匹配任何单字符
- 匹配0或者任意数量的 字符
** 匹配0或者更多的 目录
这里注意了单个* 是在一个目录内进行匹配。 而** 是可以匹配多个目录,一定不要迷糊。
3.1 Ant 通配符示例
通配符 示例 说明
? /ant/p?ttern 匹配项目根路径下 /ant/pattern 和 /ant/pXttern,但是不包括/ant/pttern
- /ant/*.html 匹配项目根路径下所有在ant路径下的.html文件
- /ant/*/path /ant/path、/ant/a/path、/ant/bxx/path 都匹配,不匹配 /ant/axx/bxx/path
- /ant/**/path /ant/path、/ant/a/path、/ant/bxx/path 、/ant/axx/bxx/path都匹配
3.2 最长匹配原则
从 3.1 可以看出 * 和 * 是有冲突的情况存在的。为了解决这种冲突就规定了最长匹配原则(has more characters)。 一旦一个uri 同时符合两个Ant匹配那么走匹配规则字符最多的。为什么走最长?因为字符越长信息越多就越具体。比如 /ant/a/path 同时满足 /**/path 和 /ant//path 那么走/ant/*/path
- Spring MVC 和 Spring Security 中的 Ant 风格
接下来我们来看看 Spring MVC 和 Spring Security 下的 Ant风格。
4.1 Spring MVC 中的 Ant 风格
这里也提一下在 Spring MVC 中 我们在控制器中写如下接口:
1 | /** |
你使用任意合法uri字符替代? 发现都可以匹配,比如/bant 。 还有Spring MVC 的一些 过滤器注册、格式化器注册都用到了 Ant 风格。
4.2 Spring Security 中的 Ant 风格
在 Spring Security 中 WebSecurityConfigurerAdapter 中的你可以通过如下配置进行路由权限访问控制:
1 | public class SecurityConfig extends WebSecurityConfigurerAdapter { |
上面 Spring Security 的配置中在 antMatchers 方法中通过 Ant 通配符来控制了资源的访问权限
自定义配置类入口WebSecurityConfigurerAdapter
前言
今天我们要进一步的的学习如何自定义配置 Spring Security 我们已经多次提到了 WebSecurityConfigurerAdapter ,而且我们知道 Spring Boot 中的自动配置实际上是通过自动配置包下的 SecurityAutoConfiguration 总配置类上导入的 Spring Boot Web 安全配置类 SpringBootWebSecurityConfiguration 来配置的。所以我们就拿它开刀。如果还是一头雾水建议通过 https://felord.cn 查看 Spring Security 实战 。自定义 Spring Boot Web 安全配置类
我们使用我们最擅长的 Ctrl + C 、Ctrl + V 抄源码中的 SpringBootWebSecurityConfiguration ,命名为我们自定义的 CustomSpringBootWebSecurityConfiguration :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
}相信已经有人注意到了上面 DefaultConfigurerAdapter 中我覆写(@Override)了三个方法,我们一般会通过自定义配置这三个方法来自定义我们的安全访问策略。
2.1 认证管理器配置方法
void configure(AuthenticationManagerBuilder auth) 用来配置认证管理器AuthenticationManager。说白了就是所有 UserDetails 相关的它都管,包含 PasswordEncoder 密码机。如果你不清楚可以通过 Spring Security 中的 UserDetail 进行了解。本文对 AuthenticationManager 不做具体分析讲解,后面会有专门的文章来讲这个东西 。 可通过 Spring Security 实战系列 进行学习。
2.2 核心过滤器配置方法
void configure(WebSecurity web) 用来配置 WebSecurity 。而 WebSecurity 是基于 Servlet Filter 用来配置 springSecurityFilterChain 。而 springSecurityFilterChain 又被委托给了 Spring Security 核心过滤器 Bean DelegatingFilterProxy 。 相关逻辑你可以在 WebSecurityConfiguration 中找到。我们一般不会过多来自定义 WebSecurity , 使用较多的使其ignoring() 方法用来忽略 Spring Security 对静态资源的控制。
2.3 安全过滤器链配置方法
void configure(HttpSecurity http) 这个是我们使用最多的,用来配置 HttpSecurity 。 HttpSecurity 用于构建一个安全过滤器链 SecurityFilterChain 。SecurityFilterChain 最终被注入核心过滤器 。 HttpSecurity 有许多我们需要的配置。我们可以通过它来进行自定义安全访问策略。所以我们单独开一章来讲解这个东西。
- HttpSecurity 配置
HttpSecurity 是后面几篇文章的重点,我们将实际操作它来实现一些实用功能。所以本文要着重介绍它。
3.1 默认配置
1 | protected void configure(HttpSecurity http) throws Exception { |
上面是 Spring Security 在 Spring Boot 中的默认配置。通过以上的配置,你的应用具备了一下的功能:
所有的请求访问都需要被授权。
使用 form 表单进行登陆(默认路径为/login),也就是前几篇我们见到的登录页。
防止 CSRF 攻击、 XSS 攻击。
启用 HTTP Basic 认证
3.2 常用方法解读
HttpSecurity 使用了builder 的构建方式来灵活制定访问策略。最早基于 XML 标签对 HttpSecurity 进行配置。现在大部分使用 javaConfig方式。常用的方法解读如下:
方法 说明
openidLogin() 用于基于 OpenId 的验证
headers() 将安全标头添加到响应,比如说简单的 XSS 保护
cors() 配置跨域资源共享( CORS )
sessionManagement() 允许配置会话管理
portMapper() 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee() 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509() 配置基于x509的认证
rememberMe 允许配置“记住我”的验证
authorizeRequests() 允许基于使用HttpServletRequest限制访问
requestCache() 允许配置请求缓存
exceptionHandling() 允许配置错误处理
securityContext() 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi() 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous() 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic() 配置 Http Basic 验证
addFilterBefore() 在指定的Filter类之前添加过滤器
addFilterAt() 在指定的Filter类的位置添加过滤器
addFilterAfter() 在指定的Filter类的之后添加过滤器
and() 连接以上策略的连接器,用来组合安全策略。实际上就是”而且”的意思
玩转自定义登录
前言
前面的关于 Spring Security 相关的文章只是一个预热。为了接下来更好的实战,如果你错过了请从 Spring Security 实战系列 开始。安全访问的第一步就是认证(Authentication),认证的第一步就是登录。今天我们要通过对 Spring Security 的自定义,来设计一个可扩展,可伸缩的 form 登录功能。form 登录的流程
下面是 form 登录的基本流程:
客户端发起请求,服务器端返回成功信息或返回失败信息
只要是 form 登录基本都能转化为上面的流程。接下来我们看看 Spring Security 是如何处理的。Spring Security 中的登录
昨天 Spring Security 实战干货:自定义配置类入口WebSecurityConfigurerAdapter 中已经讲到了我们通常的自定义访问控制主要是通过 HttpSecurity 来构建的。默认它提供了三种登录方式:
formLogin() 普通表单登录
oauth2Login() 基于 OAuth2.0 认证/授权协议
openidLogin() 基于 OpenID 身份认证规范
以上三种方式统统是 AbstractAuthenticationFilterConfigurer 实现的,
- HttpSecurity 中的 form 表单登录
启用表单登录通过两种方式一种是通过 HttpSecurity 的 apply(C configurer) 方法自己构造一个 AbstractAuthenticationFilterConfigurer 的实现,这种是比较高级的玩法。 另一种是我们常见的使用 HttpSecurity 的 formLogin() 方法来自定义 FormLoginConfigurer 。我们先搞一下比较常规的第二种。
4.1 FormLoginConfigurer
该类是 form 表单登录的配置类。它提供了一些我们常用的配置方法:
loginPage(String loginPage) : 登录 页面而并不是接口,对于前后分离模式需要我们进行改造 默认为 /login。
loginProcessingUrl(String loginProcessingUrl) 实际表单向后台提交用户信息的 Action,再由过滤器UsernamePasswordAuthenticationFilter 拦截处理,该 Action 其实不会处理任何逻辑。
usernameParameter(String usernameParameter) 用来自定义用户参数名,默认 username 。
passwordParameter(String passwordParameter) 用来自定义用户密码名,默认 password
failureUrl(String authenticationFailureUrl) 登录失败后会重定向到此路径, 一般前后分离不会使用它。
failureForwardUrl(String forwardUrl) 登录失败会转发到此, 一般前后分离用到它。 可定义一个 Controller (控制器)来处理返回值,但是要注意 RequestMethod。
defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) 默认登陆成功后跳转到此 ,如果 alwaysUse 为 true 只要进行认证流程而且成功,会一直跳转到此。一般推荐默认值 false
successForwardUrl(String forwardUrl) 效果等同于上面 defaultSuccessUrl 的 alwaysUse 为 true 但是要注意 RequestMethod。
successHandler(AuthenticationSuccessHandler successHandler) 自定义认证成功处理器,可替代上面所有的 success 方式
failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 自定义失败处理器,可替代上面所有的 failure 方式
permitAll(boolean permitAll) form 表单登录是否放开
知道了这些我们就能来搞个定制化的登录了。
- Spring Security 聚合登录 实战
接下来是我们最激动人心的实战登录操作。 有疑问的可认真阅读 Spring 实战 的一系列预热文章。
5.1 简单需求
我们的接口访问都要通过认证,登陆错误后返回错误信息(json),成功后前台可以获取到对应数据库用户信息(json)(实战中记得脱敏)。
我们定义处理成功失败的控制器:
1 | @RestController |
然后 我们自定义配置覆写 void configure(HttpSecurity http) 方法进行如下配置(这里需要禁用crsf):
1 | @Configuration |
使用 Postman 或者其它工具进行 Post 方式的表单提交 http://localhost:8080/process?username=iching&password=123456 会返回用户信息:
1 | { |
把密码修改为其它值再次请求认证失败后 :
1 | { |
- 多种登录方式并存的实现
就这么完了了么?现在登录的花样繁多。常规的就有短信、邮箱、扫码 ,第三方是以后我要讲的不在今天范围之内。 如何应对想法多的产品经理? 我们来搞一个可扩展各种姿势的登录方式。我们在上面 2. form 登录的流程 中的 用户 和 判定 之间增加一个适配器来适配即可。 我们知道这个所谓的 判定就是 UsernamePasswordAuthenticationFilter 。
我们只需要保证 uri 为上面配置的/process 并且能够通过 getParameter(String name) 获取用户名和密码即可 。
我突然觉得可以模仿 DelegatingPasswordEncoder 的搞法, 维护一个注册表执行不同的处理策略。当然我们要实现一个 GenericFilterBean 在 UsernamePasswordAuthenticationFilter 之前执行。同时制定登录的策略。
6.1 登录方式定义
定义登录方式枚举
1 | public enum LoginTypeEnum { |
6.2 定义前置处理器接口
定义前置处理器接口用来处理接收的各种特色的登录参数 并处理具体的逻辑。这个借口其实有点随意 ,重要的是你要学会思路。我实现了一个 默认的 form’ 表单登录 和 通过RequestBody放入json` 的两种方式,篇幅限制这里就不展示了
1 | public interface LoginPostProcessor { |
6.3 实现登录前置处理过滤器
该过滤器维护了 LoginPostProcessor 映射表。 通过前端来判定登录方式进行策略上的预处理,最终还是会交给 UsernamePasswordAuthenticationFilter 。通过 HttpSecurity 的 addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)方法进行前置。
1 | /** |
6.4 验证
通过 POST 表单提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0 可以请求成功
更多的方式 只需要实现接口 LoginPostProcessor 注入 PreLoginFilter
内置 Filter 全解析
- 前言
上一文我们使用 Spring Security 实现了各种登录聚合的场面。其中我们是通过在 UsernamePasswordAuthenticationFilter 之前一个自定义的过滤器实现的。我怎么知道自定义过滤器要加在 UsernamePasswordAuthenticationFilter 之前。我在这个系列开篇说了 Spring Security 权限控制的一个核心关键就是 过滤器链 ,这些过滤器如下图进行过滤传递,甚至比这个更复杂!这只是一个最小单元。
request handlerchain handler result
Spring Security 内置了一些过滤器,他们各有各的本事。如果你掌握了这些过滤器,很多实际开发中的需求和问题都很容易解决。今天我们来见识一下这些内置的过滤器。
- 内置过滤器初始化
在 Spring Security 初始化核心过滤器时 HttpSecurity 会通过将 Spring Security 内置的一些过滤器以 FilterComparator 提供的规则进行比较按照比较结果进行排序注册。
2.1 排序规则
FilterComparator 维护了一个顺序的注册表 filterToOrder
1 | FilterComparator() { |
这些就是所有内置的过滤器。 他们是通过下面的方法获取自己的序号:
1 | private Integer getOrder(Class<?> clazz) { |
通过过滤器的类全限定名从注册表 filterToOrder 中获取自己的序号,如果没有直接获取到序号通过递归获取父类在注册表中的序号作为自己的序号,序号越小优先级越高。上面的过滤器并非全部会被初始化。有的需要额外引入一些功能包,有的看 HttpSecurity 的配置情况。 在上一篇文章中。我们禁用了 CSRF 功能,就意味着 CsrfFilter 不会被注册。
- 内置过滤器讲解
接下来我们就对这些内置过滤器进行一个系统的认识。我们将按照默认顺序进行讲解。
3.1 ChannelProcessingFilter
ChannelProcessingFilter 通常是用来过滤哪些请求必须用 https 协议, 哪些请求必须用 http 协议, 哪些请求随便用哪个协议都行。它主要有两个属性:
ChannelDecisionManager 用来判断请求是否符合既定的协议规则。它维护了一个 ChannelProcessor 列表 这些ChannelProcessor 是具体用来执行 ANY_CHANNEL 策略 (任何通道都可以), REQUIRES_SECURE_CHANNEL 策略 (只能通过https 通道), REQUIRES_INSECURE_CHANNEL 策略 (只能通过 http 通道)。
FilterInvocationSecurityMetadataSource 用来存储 url 与 对应的ANY_CHANNEL、REQUIRES_SECURE_CHANNEL、REQUIRES_INSECURE_CHANNEL 的映射关系。
ChannelProcessingFilter 通过 HttpScurity#requiresChannel() 等相关方法引入其配置对象 ChannelSecurityConfigurer 来进行配置。
3.2 ConcurrentSessionFilter
ConcurrentSessionFilter 主要用来判断session是否过期以及更新最新的访问时间。其流程为:
session 检测,如果不存在直接放行去执行下一个过滤器。存在则进行下一步。
根据sessionid从SessionRegistry中获取SessionInformation,从SessionInformation中获取session是否过期;没有过期则更新SessionInformation中的访问日期;
如果过期,则执行doLogout()方法,这个方法会将session无效,并将 SecurityContext 中的Authentication中的权限置空,同时在SecurityContenxtHoloder中清除SecurityContext然后查看是否有跳转的 expiredUrl,如果有就跳转,没有就输出提示信息。
ConcurrentSessionFilter 通过SessionManagementConfigurer 来进行配置。
3.3 WebAsyncManagerIntegrationFilter
WebAsyncManagerIntegrationFilter用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager。用来处理异步请求的安全上下文。具体逻辑为:
从请求属性上获取所绑定的WebAsyncManager,如果尚未绑定,先做绑定。
从asyncManager 中获取 key 为 CALLABLE_INTERCEPTOR_KEY 的安全上下文多线程处理器 SecurityContextCallableProcessingInterceptor, 如果获取到的为 null,
新建一个 SecurityContextCallableProcessingInterceptor 并绑定 CALLABLE_INTERCEPTOR_KEY 注册到 asyncManager 中。
这里简单说一下 SecurityContextCallableProcessingInterceptor 。它实现了接口 CallableProcessingInterceptor,
当它被应用于一次异步执行时,beforeConcurrentHandling() 方法会在调用者线程执行,该方法会相应地从当前线程获取SecurityContext,然后被调用者线程中执行逻辑时,会使用这个 SecurityContext,从而实现安全上下文从调用者线程到被调用者线程的传输。
WebAsyncManagerIntegrationFilter 通过 WebSecurityConfigurerAdapter#getHttp()方法添加到 HttpSecurity 中成为 DefaultSecurityFilterChain 的一个链节。
3.4 SecurityContextPersistenceFilter
SecurityContextPersistenceFilter 主要控制 SecurityContext 的在一次请求中的生命周期 。 请求来临时,创建SecurityContext 安全上下文信息,请求结束时清空 SecurityContextHolder。
SecurityContextPersistenceFilter 通过 HttpScurity#securityContext() 及相关方法引入其配置对象 SecurityContextConfigurer 来进行配置。
3.5 HeaderWriterFilter
HeaderWriterFilter 用来给 http 响应添加一些 Header,比如 X-Frame-Options, X-XSS-Protection ,X-Content-Type-Options。
你可以通过 HttpScurity#headers() 来定制请求Header 。
3.6 CorsFilter
跨域相关的过滤器。这是Spring MVC Java配置和XML 命名空间 CORS 配置的替代方法, 仅对依赖于spring-web的应用程序有用(不适用于spring-webmvc)或 要求在javax.servlet.Filter 级别进行CORS检查的安全约束链接。这个是目前官方的一些解读,但是我还是不太清楚实际机制。
你可以通过 HttpSecurity#cors() 来定制。
3.7 CsrfFilter
CsrfFilter 用于防止csrf攻击,前后端使用json交互需要注意的一个问题。
你可以通过 HttpSecurity.csrf() 来开启或者关闭它。在你使用 jwt 等 token 技术时,是不需要这个的。
3.8 LogoutFilter
LogoutFilter 很明显这是处理注销的过滤器。
你可以通过 HttpSecurity.logout() 来定制注销逻辑,非常有用。
3.9 OAuth2AuthorizationRequestRedirectFilter
和上面的有所不同,这个需要依赖 spring-scurity-oauth2 相关的模块。该过滤器是处理 OAuth2 请求首选重定向相关逻辑的。以后会我会带你们认识它,请多多关注公众号:Felordcn 。
3.10 Saml2WebSsoAuthenticationRequestFilter
这个需要用到 Spring Security SAML 模块,这是一个基于 SMAL 的 SSO 单点登录请求认证过滤器。
关于SAML
SAML 即安全断言标记语言,英文全称是 Security Assertion Markup Language。它是一个基于 XML 的标准,用于在不同的安全域(security domain)之间交换认证和授权数据。在 SAML 标准定义了身份提供者 (identity provider) 和服务提供者 (service provider),这两者构成了前面所说的不同的安全域。 SAML 是 OASIS 组织安全服务技术委员会(Security Services Technical Committee) 的产品。
SAML(Security Assertion Markup Language)是一个 XML 框架,也就是一组协议,可以用来传输安全声明。比如,两台远程机器之间要通讯,为了保证安全,我们可以采用加密等措施,也可以采用 SAML 来传输,传输的数据以 XML 形式,符合 SAML 规范,这样我们就可以不要求两台机器采用什么样的系统,只要求能理解 SAML 规范即可,显然比传统的方式更好。SAML 规范是一组 Schema 定义。
可以这么说,在Web Service 领域,schema 就是规范,在 Java 领域,API 就是规范
3.11 X509AuthenticationFilter
X509 认证过滤器。你可以通过 HttpSecurity#X509() 来启用和配置相关功能。
3.12 AbstractPreAuthenticatedProcessingFilter
AbstractPreAuthenticatedProcessingFilter 处理处理经过预先认证的身份验证请求的过滤器的基类,其中认证主体已经由外部系统进行了身份验证。 目的只是从传入请求中提取主体上的必要信息,而不是对它们进行身份验证。
你可以继承该类进行具体实现并通过 HttpSecurity#addFilter 方法来添加个性化的AbstractPreAuthenticatedProcessingFilter 。
3.13 CasAuthenticationFilter
CAS 单点登录认证过滤器 。依赖 Spring Security CAS 模块
3.14 OAuth2LoginAuthenticationFilter
这个需要依赖 spring-scurity-oauth2 相关的模块。OAuth2 登录认证过滤器。处理通过 OAuth2 进行认证登录的逻辑。
3.15 Saml2WebSsoAuthenticationFilter
这个需要用到 Spring Security SAML 模块,这是一个基于 SMAL 的 SSO 单点登录认证过滤器。关于SAML
3.16 UsernamePasswordAuthenticationFilter
这个看过我相关文章的应该不陌生了。处理用户以及密码认证的核心过滤器。认证请求提交的username和 password,被封装成token进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。
你可以通过 HttpSecurity#formLogin() 及相关方法引入其配置对象 FormLoginConfigurer 来进行配置。 我们在 Spring Security 实战干货: 玩转自定义登录 已经对其进行过个性化的配置和魔改。
3.17 ConcurrentSessionFilter
参见 3.2 ConcurrentSessionFilter。 该过滤器可能会被多次执行。
3.18 OpenIDAuthenticationFilter
基于OpenID 认证协议的认证过滤器。 你需要在依赖中依赖额外的相关模块才能启用它。
3.19 DefaultLoginPageGeneratingFilter
生成默认的登录页。默认 /login 。
3.20 DefaultLogoutPageGeneratingFilter
生成默认的退出页。 默认 /logout 。
3.21 ConcurrentSessionFilter
参见 3.2 ConcurrentSessionFilter 。 该过滤器可能会被多次执行。
3.23 DigestAuthenticationFilter
Digest身份验证是 Web 应用程序中流行的可选的身份验证机制 。DigestAuthenticationFilter 能够处理 HTTP 头中显示的摘要式身份验证凭据。你可以通过 HttpSecurity#addFilter() 来启用和配置相关功能。
3.24 BasicAuthenticationFilter
和Digest身份验证一样都是Web 应用程序中流行的可选的身份验证机制 。 BasicAuthenticationFilter 负责处理 HTTP 头中显示的基本身份验证凭据。这个 Spring Security 的 Spring Boot 自动配置默认是启用的 。
BasicAuthenticationFilter 通过 HttpSecurity#httpBasic() 及相关方法引入其配置对象 HttpBasicConfigurer 来进行配置。
3.25 RequestCacheAwareFilter
用于用户认证成功后,重新恢复因为登录被打断的请求。当匿名访问一个需要授权的资源时。会跳转到认证处理逻辑,此时请求被缓存。在认证逻辑处理完毕后,从缓存中获取最开始的资源请求进行再次请求。
RequestCacheAwareFilter 通过 HttpScurity#requestCache() 及相关方法引入其配置对象 RequestCacheConfigurer 来进行配置。
3.26 SecurityContextHolderAwareRequestFilter
用来 实现j2ee中 Servlet Api 一些接口方法, 比如 getRemoteUser 方法、isUserInRole 方法,在使用 Spring Security 时其实就是通过这个过滤器来实现的。
SecurityContextHolderAwareRequestFilter 通过 HttpSecurity.servletApi() 及相关方法引入其配置对象 ServletApiConfigurer 来进行配置。
3.27 JaasApiIntegrationFilter
适用于JAAS (Java 认证授权服务)。 如果 SecurityContextHolder 中拥有的 Authentication 是一个 JaasAuthenticationToken,那么该 JaasApiIntegrationFilter 将使用包含在 JaasAuthenticationToken 中的 Subject 继续执行 FilterChain。
3.28 RememberMeAuthenticationFilter
处理 记住我 功能的过滤器。
RememberMeAuthenticationFilter 通过 HttpSecurity.rememberMe() 及相关方法引入其配置对象 RememberMeConfigurer 来进行配置。
3.29 AnonymousAuthenticationFilter
匿名认证过滤器。对于 Spring Security 来说,所有对资源的访问都是有 Authentication 的。对于无需登录(UsernamePasswordAuthenticationFilter )直接可以访问的资源,会授予其匿名用户身份。
AnonymousAuthenticationFilter 通过 HttpSecurity.anonymous() 及相关方法引入其配置对象 AnonymousConfigurer 来进行配置。
3.30 SessionManagementFilter
Session 管理器过滤器,内部维护了一个 SessionAuthenticationStrategy 用于管理 Session 。
SessionManagementFilter 通过 HttpScurity#sessionManagement() 及相关方法引入其配置对象 SessionManagementConfigurer 来进行配置。
3.31 ExceptionTranslationFilter
主要来传输异常事件,还记得之前我们见过的 DefaultAuthenticationEventPublisher 吗?
3.32 FilterSecurityInterceptor
这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。如果你要实现动态权限控制就必须研究该类 。
3.33 SwitchUserFilter
SwitchUserFilter 是用来做账户切换的。默认的切换账号的url为/login/impersonate,默认注销切换账号的url为/logout/impersonate,默认的账号参数为username 。
你可以通过此类实现自定义的账户切换。
- 总结
所有内置的 31个过滤器作用都讲解完了,有一些默认已经启用。有一些需要引入特定的包并且对 HttpSecurity 进行配置才会生效,而且它们的顺序是既定的。 只有你了解这些过滤器你才能基于业务深度定制 Spring Security 。
实现自定义退出登录
前言
上一篇对 Spring Security 所有内置的 Filter 进行了介绍。今天我们来实战如何安全退出应用程序。我们使用 Spring Security 登录后都做了什么
这个问题我们必须搞清楚!一般登录后,服务端会给用户发一个凭证。常见有以下的两种:
基于 Session 客户端会存 cookie 来保存一个 sessionId ,服务端存一个 Session 。
基于 token 客户端存一个 token 串,服务端会在缓存中存一个用来校验此 token 的信息。
- 退出登录需要我们做什么
当前的用户登录状态失效。这就需要我们清除服务端的用户状态。
退出登录接口并不是 permitAll, 只有携带对应用户的凭证才退出。
将退出结果返回给请求方。
退出登录后用户可以通过重新登录来认证该用户。 - Spring Security 中的退出登录
接下来我们来分析并实战 如何定制退出登录逻辑。首先我们要了解 LogoutFilter 。
3.1 LogoutFilter
通过 Spring Security 实战干货:内置 Filter 全解析 我们知道退出登录逻辑是由过滤器 LogoutFilter 来执行的。 它持有三个接口类型的属性:
RequestMatcher logoutRequestMatcher 这个用来拦截退出请求的 URL
LogoutHandler handler 用来处理退出的具体逻辑
LogoutSuccessHandler logoutSuccessHandler 退出成功后执行的逻辑
我们通过对以上三个接口的实现就能实现我们自定义的退出逻辑。
3.2 LogoutConfigurer
我们一般不会直接操作 LogoutFilter ,而是通过 LogoutConfigurer 来配置 LogoutFilter。 你可以通过 HttpSecurity#logout() 方法来初始化一个 LogoutConfigurer 。 接下来我们来实战操作一下。
3.2.1 实现自定义退出登录请求URL
LogoutConfigurer 提供了 logoutRequestMatcher(RequestMatcher logoutRequestMatcher)、logoutUrl(Sring logoutUrl) 两种方式来定义退出登录请求的 URL 。它们作用是相同的,你选择其中一种方式即可。
3.2.2 处理具体的逻辑
默认情况下 Spring Security 是基于 Session 的。LogoutConfigurer 提供了一些直接配置来满足你的需要。如下:
clearAuthentication(boolean clearAuthentication) 是否在退出时清除当前用户的认证信息
deleteCookies(String… cookieNamesToClear) 删除指定的 cookies
invalidateHttpSession(boolean invalidateHttpSession) 是否移除 HttpSession
如果上面满足不了你的需要就需要你来定制 LogoutHandler 了。
3.2.3 退出成功逻辑
logoutSuccessUrl(String logoutSuccessUrl) 退出成功后会被重定向到此 URL ,你可以写一个Controller 来完成最终返回,但是需要支持 GET 请求和 匿名访问 。 通过 setDefaultTargetUrl 方法注入到 LogoutSuccessHandler
defaultLogoutSuccessHandlerFor(LogoutSuccessHandler handler, RequestMatcher preferredMatcher) 用来构造默认的 LogoutSuccessHandler 我们可以通过添加多个来实现从不同 URL 退出执行不同的逻辑。
LogoutSuccessHandler logoutSuccessHandler 退出成功后执行的逻辑的抽象根本接口。
3.3 Spring Security 退出登录实战
现在前后端分离比较多,退出后返回json。 而且只有用户在线才能退出登录。否则不能进行退出操作。我们采用实现 LogoutHandler 和 LogoutSuccessHandler 接口这种编程的方式来配置 。退出请求的 url 依然通过 LogoutConfigurer#logoutUrl(String logoutUrl)来定义。
3.3.1 自定义 LogoutHandler
默认情况下清除认证信息 (invalidateHttpSession),和Session 失效(invalidateHttpSession) 已经由内置的SecurityContextLogoutHandler 来完成。我们自定义的 LogoutHandler 会在SecurityContextLogoutHandler 来执行。
1 | @Slf4j |
以上是我们实现的 LogoutHandler 。 我们可以从 logout 方法的 authentication 变量中 获取当前用户信息。你可以通过这个来实现你具体想要的业务。比如记录用户下线退出时间、IP 等等。
3.3.2 自定义 LogoutSuccessHandler
如果我们实现了自定义的 LogoutSuccessHandler 就不必要设置 LogoutConfigurer#logoutSuccessUrl(String logoutSuccessUrl) 了。该处理器处理后会响应给前端。你可以转发到其它控制器。重定向到登录页面,也可以自行实现其它 MediaType ,可以是 json 或者页面
1 | @Slf4j |
3.3.4 自定义退出的 Spring Security 配置
为了方便调试我 注释掉了我们 实现的自定义登录,你可以通过 http:localhost:8080/login 来登录,然后通过 http:localhost:8080/logout 测试退出。
1 | @Override |
手把手教你实现JWT Token
前言
Json Web Token (JWT) 近几年是前后端分离常用的 Token 技术,是目前最流行的跨域身份验证解决方案。你可以通过文章 一文了解web无状态会话token技术JWT 来了解 JWT。今天我们来手写一个通用的 JWT 服务。DEMO 获取方式在文末,实现在 jwt 相关包下spring-security-jwt
spring-security-jwt 是 Spring Security Crypto 提供的 JWT 工具包 。1
2
3
4
5<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring-security-jwt.version}</version>
</dependency>核心类只有一个: org.springframework.security.jwt.JwtHelper 。它提供了两个非常有用的静态方法。
JWT 编码
JwtHelper 提供的第一个静态方法就是 encode(CharSequence content, Signer signer) 这个是用来生成jwt的方法 需要指定 payload 跟 signer 签名算法。payload 存放了一些可用的不敏感信息:
iss jwt签发者
sub jwt所面向的用户
aud 接收jwt的一方
iat jwt的签发时间
exp jwt的过期时间,这个过期时间必须要大于签发时间 iat
jti jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
除了以上提供的基本信息外,我们可以定义一些我们需要传递的信息,比如目标用户的权限集 等等。切记不要传递密码等敏感信息 ,因为 JWT 的前两段都是用了 BASE64 编码,几乎算是明文了。
3.1 构建 JWT 中的 payload
我们先来构建 payload :
1 | public class JwtPayloadBuilder { |
通过建造类 JwtClaimsBuilder 我们可以很方便来构建 JWT 所需要的 payload json 字符串传递给 encode(CharSequence content, Signer signer) 中的 content 。
3.2 生成 RSA 密钥并进行签名
为了生成 JWT Token 我们还需要使用 RSA 算法来进行签名。 这里我们使用 JDK 提供的证书管理工具 Keytool 来生成 RSA 证书 ,格式为 jks 格式。
生成证书命令参考:
1 | keytool -genkey -alias iching -keypass iching -keyalg RSA -storetype PKCS12 -keysize 1024 -validity 365 -keystore /Users/minyi/web/ideaSpace/spring-boot-starter/iching-auth-server/src/main/resources/iching.jks -storepass 123456 -dname "CN=(iching), OU=(iching), O=(iching), L=(zz), ST=(hn), C=(cn)" |
其中 -alias iching -storepass 123456 我们要作为配置使用要记下来。我们要使用下面定义的这个类来读取证书
查看jks文件中所有证书内容
keytool -list -v -keystore cert_file_name
修改证书文文件的加密密码
keytool -storepasswd -keystore cert_file_name
输入命令,会首先提示你输入证书原始密码;之后会继续要求你输入两次新密码。
1 | class KeyPairFactory { |
获取了 KeyPair 就能获取公私钥 生成 Jwt 的两个要素就完成了。我们可以和之前定义的 JwtPayloadBuilder 一起封装出生成 Jwt Token 的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14private String jwtToken(String aud, int exp, Set<String> roles, Map<String, String> additional) {
String payload = jwtPayloadBuilder
.iss(jwtProperties.getIss())
.sub(jwtProperties.getSub())
.aud(aud)
.additional(additional)
.roles(roles)
.expDays(exp)
.builder();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RsaSigner signer = new RsaSigner(privateKey);
return JwtHelper.encode(payload, signer).getEncoded();
}
通常情况下 Jwt Token 都是成对出现的,一个为平常请求携带的 accessToken, 另一个只作为刷新 accessToken 之用的 refreshToken 。而且 refreshToken 的过期时间要相对长一些。当 accessToken 失效而refreshToken 有效时,我们可以通过 refreshToken 来获取新的 Jwt Token对 ;当两个都失效就用户就必须重新登录了。
生成 Jwt Token对 的方法如下:
1 | public JwtTokenPair jwtTokenPair(String aud, Set<String> roles, Map<String, String> additional) { |
通常 Jwt Token对 会在返回给前台的同时放入缓存中。过期策略你可以选择分开处理,也可以选择以refreshToken 的过期时间为准。
JWT 解码以及验证
JwtHelper 提供的第二个静态方法是Jwt decodeAndVerify(String token, SignatureVerifier verifier) 用来 验证和解码 Jwt Token 。我们获取到请求中的token后会解析出用户的一些信息。通过这些信息去缓存中对应的token ,然后比对并验证是否有效(包括是否过期)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* 解码 并校验签名 过期不予解析
*
* @param jwtToken the jwt token
* @return the jwt claims
*/
public JSONObject decodeAndVerify(String jwtToken) {
Assert.hasText(jwtToken, "jwt token must not be bank");
RSAPublicKey rsaPublicKey = (RSAPublicKey) this.keyPair.getPublic();
SignatureVerifier rsaVerifier = new RsaVerifier(rsaPublicKey);
Jwt jwt = JwtHelper.decodeAndVerify(jwtToken, rsaVerifier);
String claims = jwt.getClaims();
JSONObject jsonObject = JSONUtil.parseObj(claims);
String exp = jsonObject.getStr(JWT_EXP_KEY);
// 是否过期
if (isExpired(exp)) {
throw new IllegalStateException("jwt token is expired");
}
return jsonObject;
}上面我们将有效的 Jwt Token 中的 payload 解析为 JSON对象 ,方便后续的操作。
配置
我们将 JWT 的可配置项抽出来放入 JwtProperties 如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40/**
* Jwt 在 springboot application.yml 中的配置文件
*/
@Data
@ConfigurationProperties(prefix=JWT_PREFIX)
public class JwtProperties {
static final String JWT_PREFIX= "jwt.config";
/**
* 是否可用
*/
private boolean enabled;
/**
* jks 路径
*/
private String keyLocation;
/**
* key alias
*/
private String keyAlias;
/**
* key store pass
*/
private String keyPass;
/**
* jwt签发者
**/
private String iss;
/**
* jwt所面向的用户
**/
private String sub;
/**
* access jwt token 有效天数
*/
private int accessExpDays;
/**
* refresh jwt token 有效天数
*/
private int refreshExpDays;
}然后我们就可以配置 JWT 的 javaConfig 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27@EnableConfigurationProperties(JwtProperties.class)
@ConditionalOnProperty(prefix = "jwt.config",name = "enabled")
@Configuration
public class JwtConfiguration {
/**
* Jwt token storage .
*
* @return the jwt token storage
*/
@Bean
public JwtTokenStorage jwtTokenStorage() {
return new JwtTokenCacheStorage();
}
/**
* Jwt token generator.
*
* @param jwtTokenStorage the jwt token storage
* @param jwtProperties the jwt properties
* @return the jwt token generator
*/
@Bean
public JwtTokenGenerator jwtTokenGenerator(JwtTokenStorage jwtTokenStorage, JwtProperties jwtProperties) {
return new JwtTokenGenerator(jwtTokenStorage, jwtProperties);
}
}然后你就可以通过 JwtTokenGenerator 编码/解码验证 Jwt Token 对 ,通过 JwtTokenStorage 来处理 Jwt Token 缓存。缓存这里我用了Spring Cache Ehcache 来实现,你也可以切换到 Redis 。
登录后返回 JWT Token
前言
欢迎阅读 Spring Security 实战干货 系列文章,上一文 我们实现了 JWT 工具。本篇我们将一起探讨如何将 JWT 与 Spring Security 结合起来,在认证成功后不再跳转到指定页面而是直接返回 JWT Token 。 本文的DEMO 可通过文末的方式获取流程
JWT 适用于前后端分离。我们在登录成功后不在跳转到首页,将会直接返回 JWT Token 对(DEMO中为JwtTokenPair),登录失败后返回认证失败相关的信息。实现登录成功/失败返回逻辑
如果你看过 Spring Security 实战干货: 玩转自定义登录 将非常容易理解下面的做法。
3.1 AuthenticationSuccessHandler 返回 JWT Token
AuthenticationSuccessHandler 用于处理登录成功后的逻辑,我们编写实现并注入 Spring IoC 容器:
1 | /** |
3.2 AuthenticationFailureHandler 返回认证失败信息
AuthenticationFailureHandler 处理认证失败后的逻辑,前端根据此返回进行跳转处理逻辑,我们也实现它并注入 Spring IoC 容器:
1 | /** |
- 配置
把上面写好的两个 Handler Bean 写入 登录配置,相关片断如下,详情参见文末 DEMO:1
httpSecurity.formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
- 验证
我们依然通过 Spring Security 实战干货: 玩转自定义登录 一文中章节 6.4 测试 来运行。结果如下:
5.1 登录成功结果
1 | { |
我们取 access_token 使用官网jwt.io 提供的解码功能进行解码如下:
5.2 登录失败结果
1 | { |
- 总结
今天我们将 JWT 和 Spring Security 联系了起来,实现了 登录成功后返回 JWT Token 。 这仅仅是一个开始,在下一篇我们将介绍 客户端如何使用 JWT Token 、服务端如何验证 JWT Token
文档
https://www.felord.cn/_doc/_springboot/2.1.5.RELEASE/_book/index.html
https://docs.spring.io/spring-boot/docs/current/reference/
https://docs.spring.io/spring-boot/docs/
http://www.what21.com/u/10004/4895501825056762414.htm
自定义异常处理
前言
最近实在比较忙,很难抽出时间来继续更 Spring Security 实战干货系列。今天正好项目中 Spring Security 需要对认证授权异常的处理,就分享出来吧 。Spring Security 中的异常
Spring Security 中的异常主要分为两大类:一类是认证异常,另一类是授权相关的异常。
2.1 AuthenticationException
AuthenticationException 是在用户认证的时候出现错误时抛出的异常。主要的子类如图:
根据该图的信息,系统用户不存在,被锁定,凭证失效,密码错误等认证过程中出现的异常都由 AuthenticationException 处理。
2.2 AccessDeniedException
AccessDeniedException 主要是在用户在访问受保护资源时被拒绝而抛出的异常。同 AuthenticationException 一样它也提供了一些具体的子类。如下图:
AccessDeniedException 的子类比较少,主要是 CSRF 相关的异常和授权服务异常。
- Http 状态对认证授权的规定
Http 协议对认证授权的响应结果也有规定。
3.1 401 未授权状态
HTTP 401 错误 - 未授权(Unauthorized) 一般来说该错误消息表明您首先需要登录(输入有效的用户名和密码)。 如果你刚刚输入这些信息,立刻就看到一个 401 错误,就意味着,无论出于何种原因您的用户名和密码其中之一或两者都无效(输入有误,用户名暂时停用,账户被锁定,凭证失效等) 。总之就是认证失败了。其实正好对应我们上面的 AuthenticationException 。
3.2 403 被拒绝状态
HTTP 403 错误 - 被禁止(Forbidden) 出现该错误表明您在访问受限资源时没有得到许可。服务器理解了本次请求但是拒绝执行该任务,该请求不该重发给服务器。并且服务器想让客户端知道为什么没有权限访问特定的资源,服务器应该在返回的信息中描述拒绝的理由。一般实践中我们会比较模糊的表明原因。 该错误对应了我们上面的 AccessDeniedException 。
- Spring Security 中的异常处理
我们在 Spring Security 实战干货系列文章中的 自定义配置类入口 WebSecurityConfigurerAdapter 一文中提到 HttpSecurity 提供的 exceptionHandling() 方法用来提供异常处理。该方法构造出 ExceptionHandlingConfigurer 异常处理配置类。该配置类提供了两个实用接口:
AuthenticationEntryPoint 该类用来统一处理 AuthenticationException 异常
AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常
我们只要实现并配置这两个异常处理类即可实现对 Spring Security 认证授权相关的异常进行统一的自定义处理。
4.1 实现 AuthenticationEntryPoint
以 json 信息响应。
1 | public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint { |
4.2 实现 AccessDeniedHandler
同样以 json 信息响应。
1 | public class SimpleAccessDeniedHandler implements AccessDeniedHandler { |
4.3 个人实践建议
其实我个人建议 Http 状态码 都返回 200 而将 401 状态在 元信息 Map 中返回。因为异常状态码在浏览器端会以 error 显示。我们只要能捕捉到 401 和 403 就能认定是认证问题还是授权问题。
4.4 配置
实现了上述两个接口后,我们只需要在 WebSecurityConfigurerAdapter 的 configure(HttpSecurity http) 方法中配置即可。相关的配置片段如下:
1 | http.exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint()) |
使用 JWT 认证访问接口
前言
欢迎阅读Spring Security 实战干货系列。之前我讲解了如何编写一个自己的 Jwt 生成器以及如何在用户认证通过后返回 Json Web Token 。今天我们来看看如何在请求中使用 Jwt 访问鉴权。DEMO 获取方法在文末。常用的 Http 认证方式
我们要在 Http 请求中使用 Jwt 我们就必须了解 常见的 Http 认证方式。
2.1 HTTP Basic Authentication
HTTP Basic Authentication 又叫基础认证,它简单地使用 Base64 算法对用户名、密码进行加密,并将加密后的信息放在请求头 Header 中,本质上还是明文传输用户名、密码,并不安全,所以最好在 Https 环境下使用。其认证流程如下:
客户端发起 GET 请求 服务端响应返回 401 Unauthorized, www-Authenticate 指定认证算法,realm 指定安全域。然后客户端一般会弹窗提示输入用户名称和密码,输入用户名密码后放入 Header 再次请求,服务端认证成功后以 200 状态码响应客户端。
2.2 HTTP Digest Authentication
为弥补 BASIC 认证存在的弱点就有了 HTTP Digest Authentication 。它又叫摘要认证。它使用随机数加上 MD5 算法来对用户名、密码进行摘要编码,流程类似 Http Basic Authentication ,但是更加复杂一些:
步骤1:跟基础认证一样,只不过返回带 WWW-Authenticate 首部字段的响应。该字段内包含质问响应方式认证所需要的临时咨询码(随机数,nonce)。 首部字段WWW-Authenticate 内必须包含 realm 和 nonce 这两个字段的信息。客户端就是依靠向服务器回送这两个值进行认证的。nonce 是一种每次随返回的 401 响应生成的任意随机字符串。该字符串通常推荐由 Base64 编码的十六进制数的组成形式,但实际内容依赖服务器的具体实现
步骤2:接收到 401 状态码的客户端,返回的响应中包含 DIGEST 认证必须的首部字段 Authorization 信息。首部字段 Authorization 内必须包含username、realm、nonce、uri 和 response 的字段信息,其中,realm 和 nonce 就是之前从服务器接收到的响应中的字段。
步骤3:接收到包含首部字段 Authorization 请求的服务器,会确认认证信息的正确性。认证通过后则会返回包含 Request-URI 资源的响应。
并且这时会在首部字段 Authorization-Info 写入一些认证成功的相关信息。
2.3 SSL 客户端认证
SSL 客户端认证就是通常我们说的 HTTPS 。安全级别较高,但需要承担 CA 证书费用。SSL 认证过程中涉及到一些重要的概念,数字证书机构的公钥、证书的私钥和公钥、非对称算法(配合证书的私钥和公钥使用)、对称密钥、对称算法(配合对称密钥使用)。相对复杂一些这里不过多讲述。
2.4 Form 表单认证
Form 表单的认证方式并不是HTTP规范。所以实现方式也呈现多样化,其实我们平常的扫码登录,手机验证码登录都属于表单登录的范畴。表单认证一般都会配合 Cookie,Session 的使用,现在很多 Web 站点都使用此认证方式。用户在登录页中填写用户名和密码,服务端认证通过后会将 sessionId 返回给浏览器端,浏览器会保存 sessionId 到浏览器的 Cookie 中。因为 HTTP 是无状态的,所以浏览器使用 Cookie 来保存 sessionId。下次客户端会在发送的请求中会携带 sessionId 值,服务端发现 sessionId 存在并以此为索引获取用户存在服务端的认证信息进行认证操作。认证过则会提供资源访问。
我们在Spring Security 实战干货:登录后返回 JWT Token 一文其实也是通过 Form 提交来获取 Jwt 其实 Jwt 跟 sessionId 同样的作用,只不过 Jwt 天然携带了用户的一些信息,而 sessionId 需要去进一步获取用户信息。
2.5 Json Web Token 的认证方式 Bearer Authentication
我们通过表单认证获取 Json Web Token ,那么如何使用它呢? 通常我们会把 Jwt 作为令牌使用 Bearer Authentication 方式使用。Bearer Authentication 是一种基于令牌的 HTTP 身份验证方案,用户向服务器请求访问受限资源时,会携带一个 Token 作为凭证,检验通过则可以访问特定的资源。最初是在 RFC 6750 中作为 OAuth 2.0 的一部分,但有时也可以单独使用。
我们在使用 Bear Token 的方法是在请求头的 Authorization 字段中放入 Bearer
- Spring Security 中实现接口 Jwt 认证
接下来我们是我们该系列的重头戏 ———— 接口的 Jwt 认证。
3.1 定义 Json Web Token 过滤器
无论上面提到的哪种认证方式,我们都可以使用 Spring Security 中的 Filter 来处理。 Spring Security 默认的基础配置没有提供对 Bearer Authentication 处理的过滤器, 但是提供了处理 Basic Authentication 的过滤器:
org.springframework.security.web.authentication.www.BasicAuthenticationFilter
BasicAuthenticationFilter 继承了 OncePerRequestFilter 。所以我们也模仿 BasicAuthenticationFilter 来实现自己的 JwtAuthenticationFilter 。 完整代码如下:
1 | /** |
具体看代码注释部分,逻辑有些地方根据你业务进行调整。匿名访问必然是不能带 Token 的!
3.2 配置 JwtAuthenticationFilter
首先将过滤器 JwtAuthenticationFilter 注入 Spring IoC 容器 ,然后一定要将 JwtAuthenticationFilter 顺序置于 UsernamePasswordAuthenticationFilter 之前:
1 | @Override |
- 使用 Jwt 进行请求验证
编写一个受限接口 ,我们这里是 http://localhost:8080/foo/test 。直接请求会被 401 。 我们通过下图方式获取 Token : - 刷新 Jwt Token
我们在 Spring Security 实战干货:手把手教你实现JWT Token 中已经实现了 Json Web Token 都是成对出现的逻辑。accessToken 用来接口请求, refreshToken 用来刷新 accessToken 。我们可以同样定义一个 Filter 可参照 上面的 JwtAuthenticationFilter 。只不过 这次请求携带的是 refreshToken,我们在过滤器中拦截 URI跟我们定义的刷新端点进行匹配。同样验证 Token ,通过后像登录成功一样返回 Token 对即可
链接
https://blog.csdn.net/nvluco/article/details/86406324
https://blog.csdn.net/a294634473/article/details/100930212
https://blog.csdn.net/wtdemeil/article/details/98486449
https://blog.csdn.net/killdrsa/article/details/89147431
https://stackoverflow.com/questions/44171633/spring-boot-oauth2-access-is-denied-user-is-anonymous-redirecting-to-authen?rq=1
https://blog.csdn.net/Amor_Leo/article/details/101751690
https://github.com/SophiaLeo/sophia_scaffolding
RBAC权限控制概念的理解
前言
欢迎阅读 Spring Security 实战干货系列文章 。截止到上一篇我们已经能够简单做到用户主体认证到接口的访问控制了,但是依然满足不了实际生产的需要。 如果我们需要一个完整的权限管理系统就必须了解一下 RBAC (Role-Based Access Control基于角色的访问控制) 的权限控制模型。为什么需要 RBAC?
在正式讨论 RBAC 模型之前,我们要思考一个问题,为什么我们要做角色权限系统? 答案很明显,一个系统肯定具有不同访问权限的用户。比如付费用户和非付费用户的权限,如果你是 QQ音乐的会员那么你能听高音质的歌曲,如果不是就不能享受某些便利的、优质的服务。那么这是一成不变的吗?又时候为了流量增长或者拉新的需要,我们又可能把一些原来充钱才能享受的服务下放给免费用户。如果你有了会员等级那就更加复杂了,VIP1 跟 VIP2 具有的功能肯定又有所差别了。主流的权限管理系统都是 RBAC 模型的变形和运用,只是根据不同的业务和设计方案,呈现不同的显示效果。
下图展示了用户和角色以及资源的简单关系:
rbacflow.png
那为什么不直接给用户分配权限,还多此一举的增加角色这一环节呢?当然直接给用户具体的资源访问控制权限也不是不可以。只是这样做的话就少了一层关系,扩展性弱了许多。如果你的系统足够简单就不要折腾 RBAC 了,怎么简单就怎么玩。如果你的系统需要考虑扩展性和权限控制的多样性就必须考虑 RBAC 。
如果你有多个具有相同权限的用户,再分配权限的时候你就需要重复为用户去 Query (查询) 和 Add (赋予) 权限,如果你要修改,比如上面的 VIP1 增加一个很 Cool 的功能,你就要遍历 VIP1 用户进行修改。有了角色后,我们只需要为该角色制定好权限后,将相同权限的用户都指定为同一个角色即可,便于权限管理。
对于批量的用户权限调整,只需调整该用户关联的角色权限,无需遍历,既大幅提升权限调整的效率,又降低了漏调权限的概率。这样用户和资源权限解除了耦合性,这就是 RBAC 模型的优势所在。
- RBAC 模型的分类
RBAC 模型可以分为:RBAC0、RBAC1、RBAC2、RBAC3 四种。其中 RBAC0 是基础,其它三种都是在 RBAC0 基础上的变种。大部分情况下,使用 RBAC0 模型就可以满足常规的权限管理系统设计了。不过一定不要拘泥于模型,要以业务需要为先导。接下来简单对四种模型进行简单的介绍一下。
3.1 RBAC0
RBAC0 是基础,定义了能构成 RBAC 权限控制系统的最小的集合,RBAC0 由四部分构成:
用户(User) 权限的使用主体
角色(Role) 包含许可的集合
会话(Session)绑定用户和角色关系映射的中间通道。而且用户必须通过会话才能给用户设置角色。
许可(Pemission) 对特定资源的特定的访问许可。
rbac0.png
3.2 RBAC1
RBAC1 在 RBAC0 的基础之上引入了角色继承的概念,有了继承那么角色就有了上下级或者等级关系。父角色拥有其子角色所有的许可。通俗讲就是来说: 你能干的,你的领导一定能干,反过来就不一定能行。
rbac1.png
3.3 RBAC2
在体育比赛中,你不可能既是运动员又是裁判员!
这是很有名的一句话。反应了我们经常出现的一种职务(其实也就是角色)冲突。有些角色产生的历史原因就是为了制约另一个角色,裁判员就是为了制约运动员从而让运动员按照规范去比赛。如果一个人兼任这两个角色,比赛必然容易出现不公正的情况从而违背竞技公平性准则。还有就是我们每个人在不同的场景都会充当不同的角色,在公司你就是特定岗位的员工,在家庭中你就是一名家庭成员。随着场景的切换,我们的角色也在随之变化。
所以 RBAC2 在 RBAC0 的基础上引入了静态职责分离(Static Separation of Duty,简称SSD)和动态职责分离(Dynamic Separation of Duty,简称DSD)两个约束概念。他们两个作用的生命周期是不同的;
SSD 作用于约束用户和角色绑定时。 1.互斥角色:就像上面的例子你不能既是A又是B,互斥的角色只能二选一 ; 2. 数量约束:用户的角色数量是有限的不能多于某个基数; 3. 条件约束:只能达到某个条件才能拥有某个角色。经常用于用户等级体系,只有你充钱成为VIP才能一刀999。
DSD 作用于会话和角色交互时。当用户持有多个角色,在用户通过会话激活角色时加以条件约束,根据不同的条件执行不同的策略。
图就不画了就是在 RBAC0 加了上述两个约束。
3.4 RBAC3
我全都要!
RBAC1 和 RBAC2 各有神通。当你拿着这两个方案给产品经理看时,他给了你一个坚定的眼神:我全都要! 于是 RBAC3 就出现了。也就是说 RBAC3 = RBAC1 + RBAC2 。
- RBAC 中一些概念的理解
四个模型说完,我们来简单对其中的一些概念进行进一步的了解。
4.1 用户(User)
对用户的理解不应该被局限于单个用户,也可以是用户组(类似 linux 的 User Group), 或许还有其它的名字比如部门或者公司;也可以是虚拟的账户,客户,甚至说第三方应用也可以算用户 。所以对用户的理解要宽泛一些,只要是有访问资源需求的主体都可以纳入用户范畴。
4.2 角色(Role)
角色是特定许可的集合以及载体。一个角色可以包含多个用户,一个用户同样的也可以属于多个角色;同样的一个角色可以包含多个用户组,一个用户组也可以具有多个角色,所以角色和用户是多对多的关系。角色是可以细分的,也就是可以继承、可以分组的。
4.3 许可(Permission)
许可一般称它为权限。通常我们将访问的目标统称为资源,不管是数据还是静态资源都是资源。我们访问资源基本上又通过 api 接口来访问。所以一般权限都体现在对接口的控制上。再细分的话我将其划分为菜单控制,具体数据增删改查功能控制(前台体现为按钮)。另外许可具有原子性,不可再分。我们将许可授予角色时就是粒度最小的单元。
- 总结
基于角色的访问控制(RBAC)已成为高级访问控制的主要方法之一。通过RBAC,您可以控制最终用户在广义和精细级别上可以做什么。您可以指定用户是管理员,专家用户还是最终用户,并使角色和访问权限与组织中员工的职位保持一致。仅根据需要为员工完成工作的足够访问权限来分配权限。通过上面的介绍相信一定会让你有所收获。对我接下来的 Spring Security 实战干货 集成 RBAC 也是提前预一下热。其实不管你使用什么安全框架, RBAC 都是必须掌握的
基于配置的接口角色访问控制
前言
欢迎阅读 Spring Security 实战干货 系列文章 。对于受限的访问资源,并不是对所有认证通过的用户开放的。比如 A 用户的角色是会计,那么他就可以访问财务相关的资源。B 用户是人事,那么他只能访问人事相关的资源。我们在 一文中也对基于角色的访问控制的相关概念进行了探讨。在实际开发中我们如何对资源进行角色粒度的管控呢?今天我来告诉你 Spring Security 是如何来解决这个问题的。将角色写入 UserDetails
我们使用 UserDetailsService 加载 UserDetails 时也会把用户的 GrantedAuthority 权限集写入其中。你可以将角色持久化并在这个点进行注入然后配置访问策略,后续的问题交给 Spring Security 。在 HttpSecurity 中进行配置角色访问控制
我们可以通过配置 WebSecurityConfigurerAdapter 中的 HttpSecurity 来控制接口的角色访问。
3.1 通过判断用户是否持有角色来进行访问控制
httpSecurity.authorizeRequests().antMatchers(“/foo/test”).hasRole(“ADMIN”)
表示 持有 ROLE_ADMIN 角色的用户才能访问 /foo/test 接口。注意:hasRole(String role) 方法入参不能携带前缀 ROLE_ 。我们来查看 SecurityExpressionRoot 中相关源码:
1 | public final boolean hasRole(String role) { |
很明显 hasRole 方法源于 hasAnyRole (持有任何其中角色之一,就能满足访问条件,用于一个接口开放给多个角色访问时) :
1 | public final boolean hasAnyRole(String... roles) { |
如果一个接口开放给多个角色,比如 /foo/test 开放给了 ROLE_APP 和 ROLE_ADMIN 可以这么写:
httpSecurity.authorizeRequests().antMatchers(“/foo/test”).hasAnyRole(“APP”,”ADMIN”)
hasAnyRole 方法最终的实现为 hasAnyAuthorityName(String prefix, String… roles):
1 | private boolean hasAnyAuthorityName(String prefix, String... roles) { |
上面才是根本的实现, 需要一个 prefix 和每一个 role 进行拼接,然后用户的角色集合 roleSet 中包含了就返回true 放行,否则就 false 拒绝。默认的 prefix 为 defaultRolePrefix= ROLE_ 。
3.2 通过判断用户的 GrantedAuthority 来进行访问控制
我们也可以通过 hasAuthority 和 hasAnyAuthority 来判定。 其实底层实现和 hasAnyRole 方法一样,只不过 prefix 为 null 。也就是你写入的 GrantedAuthority 是什么样子的,这里传入参数的就是什么样子的,不再受 ROLE_ 前缀的制约。
2.1 章节的写法等同如下的写法:
1 | httpSecurity.authorizeRequests().antMatchers(“/foo/test”).hasAuthority(“ROLE_ADMIN”) |
- 匿名访问
匿名身份验证的用户和未经身份验证的用户之间没有真正的概念差异。Spring Security 的匿名身份验证只是为您提供了一种更方便的方式来配置访问控制属性。所有的匿名用户都持有角色 ROLE_ANONYMOUS 。所以你可以使用 2.1 和 2.2 章节的方法来配置匿名访问:你也可以通过以下方式进行配置:1
httpSecurity.authorizeRequests().antMatchers(“/foo/test”).hasAuthority(“ROLE_ANONYMOUS”)
1
httpSecurity.authorizeRequests().antMatchers(“/foo/test”).anonymous()
- 开放请求
开放请求可以这么配置:1
httpSecurity.authorizeRequests().antMatchers(“/foo/test”).permitAll()
- permitAll 与 anonymous 的一些探讨
开放请求 其实通常情况下跟 匿名请求 有交叉。它们的主要区别在于: 当前的 Authentication 为 null 时 permitAll 是放行的,而 anonymous 需要 Authentication 为 AnonymousAuthenticationToken 。这里是比较难以理解的,下面是来自 Spring 文档中的一些信息:
通常,采用“默认拒绝”的做法被认为是一种良好的安全做法,在该方法中,您明确指定允许的内容,并禁止其他所有内容。定义未经身份验证的用户可以访问的内容的情况与此类似,尤其是对于Web应用程序。许多站点要求用户必须通过身份验证才能使用少数几个URL(例如,主页和登录页面)。在这种情况下,最简单的是为这些特定的URL定义访问配置属性,而不是为每个受保护的资源定义访问配置属性。换句话说,有时很高兴地说默认情况下需要ROLE_SOMETHING,并且只允许该规则的某些例外,例如应用程序的登录,注销和主页。您还可以从过滤器链中完全忽略这些页面,从而绕过访问控制检查,
这就是我们所说的匿名身份验证。
使用 permitAll() 将配置授权,以便在该特定路径上允许所有请求(来自匿名用户和已登录用户),anonymous() 主要是指用户的状态(是否登录)。基本上,直到用户被“认证”为止,它就是“匿名用户”。就像每个人都有“默认角色”一样。
基于注解的接口角色访问控制
前言
欢迎阅读 Spring Security 实战干货 系列文章 。在上一篇 基于配置的接口角色访问控制 我们讲解了如何通过 javaConfig 的方式配置接口的角色访问控制。其实还有一种更加灵活的配置方式 基于注解 。今天我们就来探讨一下。DEMO 获取方式在文末。Spring Security 方法安全
Spring Security 基于注解的安全认证是通过在相关的方法上进行安全注解标记来实现的。
2.1 开启全局方法安全
我们可以在任何 @Configuration实例上使用 @EnableGlobalMethodSecurity 注解来启用全局方法安全注解功能。该注解提供了三种不同的机制来实现同一种功能,所以我们单独开一章进行探讨。
@EnableGlobalMethodSecurity 注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
/**
* 基于表达式进行方法访问控制
*/
boolean prePostEnabled() default false;
/**
* 基于 @Secured 注解
*/
boolean securedEnabled() default false;
/**
* 基于 JSR-250 注解
*/
boolean jsr250Enabled() default false;
boolean proxyTargetClass() default false;
int order() default Ordered.LOWEST_PRECEDENCE;
}@EnableGlobalMethodSecurity 源码中提供了 prePostEnabled 、securedEnabled 和 jsr250Enabled 三种方式。当你开启全局基于注解的方法安全功能时,也就是使用 @EnableGlobalMethodSecurity 注解时我们需要选择使用这三种的一种或者其中几种。我们接下来将分别介绍它们。
使用 prePostEnabled
如果你在 @EnableGlobalMethodSecurity 设置 prePostEnabled 为 true ,则开启了基于表达式的方法安全控制。通过表达式运算结果的布尔值来决定是否可以访问(true 开放, false 拒绝 )。
有时您可能需要执行开启 prePostEnabled 复杂的操作。对于这些实例,您可以扩展 GlobalMethodSecurityConfiguration,确保子类上存在@EnableGlobalMethodSecurity(prePostEnabled = true) 。例如,如果要提供自定义 MethodSecurityExpressionHandler :1
2
3
4
5
6
7
8@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
// ... create and return custom MethodSecurityExpressionHandler ...
return expressionHandler;
}
}上面示例属于高级操作,一般没有必要。无论是否继承GlobalMethodSecurityConfiguration 都将会开启四个注解。 @PreAuthorize 和 @PostAuthorize 侧重于方法调用的控制;而 @PreFilter 和 @PostFilter 侧重于数据的控制。
4.1 @PreAuthorize
在标记的方法调用之前,通过表达式来计算是否可以授权访问。接下来我来总结以下常用的表达式。
基于 SecurityExpressionOperations 接口的表达式,也就是我们在上一文的 javaConfig 配置。示例: @PreAuthorize(“hasRole(‘ADMIN’)”) 必须拥有 ROLE_ADMIN 角色。
基于 UserDetails 的表达式,此表达式用以对当前用户的一些额外的限定操作。示例:@PreAuthorize(“principal.username.startsWith(‘Felordcn’)”) 用户名开头为 Felordcn 的用户才能访问。
基于对入参的 SpEL表达式处理。关于 SpEL 表达式可参考官方文档。或者通过关注公众号:Felordcn 来获取相关资料。 示例: @PreAuthorize(“#id.equals(principal.username)”) 入参 id 必须同当前的用户名相同。
4.2 @PostAuthorize
在标记的方法调用之后,通过表达式来计算是否可以授权访问。该注解是针对 @PreAuthorize 。区别在于先执行方法。而后进行表达式判断。如果方法没有返回值实际上等于开放权限控制;如果有返回值实际的结果是用户操作成功但是得不到响应。
4.3 @PreFilter
基于方法入参相关的表达式,对入参进行过滤。分页慎用!该过程发生在接口接收参数之前。 入参必须为 java.util.Collection 且支持 remove(Object) 的参数。如果有多个集合需要通过 filterTarget=<参数名> 来指定过滤的集合。内置保留名称 filterObject 作为集合元素的操作名来进行评估过滤。
样例:
// 入参为Collection
// 过滤掉 felord jetty 为 Felordcn
@PreFilter(value = “filterObject.startsWith(‘F’)”,filterTarget = “ids”)
// 如果 当前用户持有 ROLE_AD 角色 参数都符合 否则 过滤掉不是 f 开头的
// DEMO 用户不持有 ROLE_AD 角色 故而 集合只剩下 felord
@PreFilter(“hasRole(‘AD’) or filterObject.startsWith(‘f’)”)
4.4 @PostFilter
和@PreFilter 不同的是, 基于返回值相关的表达式,对返回值进行过滤。分页慎用!该过程发生接口进行数据返回之前。 相关测试与 @PreFilter 相似,参见文末提供的 DEMO。
- 使用 securedEnabled
如果你在 @EnableGlobalMethodSecurity 设置 securedEnabled 为 true ,就开启了角色注解 @Secured ,该注解功能要简单的多,默认情况下只能基于角色(默认需要带前缀 ROLE_)集合来进行访问控制决策。
该注解的机制是只要其声明的角色集合(value)中包含当前用户持有的任一角色就可以访问。也就是 用户的角色集合和 @Secured 注解的角色集合要存在非空的交集。 不支持使用 SpEL 表达式进行决策。
- 使用 jsr250Enabled
启用 JSR-250 安全控制注解,这属于 JavaEE 的安全规范(现为 jakarta 项目)。一共有五个安全注解。如果你在 @EnableGlobalMethodSecurity 设置 jsr250Enabled 为 true ,就开启了 JavaEE 安全注解中的以下三个:
@DenyAll 拒绝所有的访问
@PermitAll 同意所有的访问
@RolesAllowed 用法和 5. 中的 @Secured 一样。
7. 总结
今天讲解了 Spring Security 另一种基于注解的静态配置。相比较基于 javaConfig 的方式要灵活一些、粒度更细、基于 SpEL 表达式可以实现更加强大的功能。但是这两种的方式还是基于编程的静态方式,具有一定的局限性。更加灵活的方式应该是动态来处理用户的角色和资源的映射关系
SecurityContext相关的知识
前言
欢迎阅读 Spring Security 实战干货 系列文章 。在前两篇我们讲解了 基于配置 和 基于注解 来配置访问控制。今天我们来讲一下如何在接口访问中检索当前认证用户信息。
我们先讲一下具体的场景。通常我们在认证后访问需要认证的资源时需要获取当前认证用户的信息。比如 “查询我的个人信息”。如果你直接在接口访问时显式的传入你的 UserID 肯定是不合适的。因为你认证通过后访问资源,系统是知道你是谁的。而且显式的暴露用户的检索接口也不安全。所以我们需要一个业务中可以检索当前认证用户的工具。 接下来我们来看看 Spring Security 是如何解决这个痛点的。安全上下文 SecurityContext
不知道你有没有留意Spring Security 实战干货:使用 JWT 认证访问接口 中是如何实现 JWT 认证拦截器 JwtAuthenticationFilter 。当服务端对 JWT Token 认证通过后,会将认证用户的信息封装到 UsernamePasswordAuthenticationToken 中 并使用工具类放入安全上下文 SecurityContext 中,当服务端响应用户后又使用同一个工具类将 UsernamePasswordAuthenticationToken 从 SecurityContext 中 clear 掉。
我们来简单了解 SecurityContext 具体是个什么东西。1
2
3
4
5
6
7
8
9
10package org.springframework.security.core.context;
import java.io.Serializable;
import org.springframework.security.core.Authentication;
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication var1);
}从源码上来看很简单就是一个 存储 Authentication 的容器。而 Authentication 是一个用户凭证接口用来作为用户认证的凭证使用,通常常用的实现有 认证用户 UsernamePasswordAuthenticationToken 和 匿名用户AnonymousAuthenticationToken。其中 UsernamePasswordAuthenticationToken 包含了 UserDetails , AnonymousAuthenticationToken 只包含了一个字符串 anonymousUser 作为匿名用户的标识。我们通过 SecurityContext 获取上下文时需要来进行类型判断。接下来我们来聊聊操作 SecurityContext 的工具类。
SecurityContextHolder
这个工具类就是 SecurityContextHolder 。 它提供了两个有用的方法:
setContext 设置当前的 SecurityContext
getContext 获取当前的 SecurityContext , 进而你可以获取到当前认证用户。
clearContext 清除当前的 SecurityContext
平常我们通过这三个方法来操作安全上下文 SecurityContext 。你可以直接在代码中使用工具类 SecurityContextHolder 获取用户信息,像下面一样:
1 | public String getCurrentUser() { |
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
1 | 初始化了一个 FilterInvocation 然后被 invoke 方法处理: |
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don’t re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
1 | 每一次请求被 Filter 过滤都会被打上标记 FILTER_APPLIED,没有被打上标记的 走了父类的 beforeInvocation 方法然后再进入过滤器链,看上去是走了一个前置的处理。那么前置处理了什么呢? |
@Bean
public RequestMatcherCreator requestMatcherCreator() {
return metaResources -> metaResources.stream()
.map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
.collect(Collectors.toSet());
}
1
2
3
4HttpRequest 匹配到对应的资源配置后就能根据资源配置去取对应的角色集合。这些角色将交给访问决策管理器 AccessDecisionManager 进行投票表决以决定是否放行。
4. AccessDecisionManager
决策管理器,用来投票决定是否放行请求。
public interface AccessDecisionManager {
// 决策 主要通过其持有的 AccessDecisionVoter 来进行投票决策
void decide(Authentication authentication, Object object,
Collection
InsufficientAuthenticationException;
// 以确定AccessDecisionManager是否可以处理传递的ConfigAttribute
boolean supports(ConfigAttribute attribute);
//以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全 object 类型。
boolean supports(Class<?> clazz);
}
1 | AccessDecisionManager 有三个默认实现: |
/**
动态权限组件配置
@author Felordcn
/
@Configuration
public class DynamicAccessControlConfiguration {
/**RequestMatcher 生成器
@return RequestMatcher
/
@Bean
public RequestMatcherCreator requestMatcherCreator() {
return metaResources -> metaResources.stream().map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod())) .collect(Collectors.toSet());
}
/**
元数据加载器
@return dynamicFilterInvocationSecurityMetadataSource
/
@Bean
public FilterInvocationSecurityMetadataSource dynamicFilterInvocationSecurityMetadataSource() {
return new DynamicFilterInvocationSecurityMetadataSource();
}/**
角色投票器
@return roleVoter
/
@Bean
public RoleVoter roleVoter() {
return new RoleVoter();
}/**
基于肯定的访问决策器
@param decisionVoters AccessDecisionVoter类型的 Bean 会自动注入到 decisionVoters
@return affirmativeBased
/
@Bean
public AccessDecisionManager affirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
return new AffirmativeBased(decisionVoters);
}
}
1
2Spring Security 的 Java Configuration 不会公开它配置的每个 object 的每个 property。这简化了大多数用户的配置。
虽然有充分的理由不直接公开每个 property,但用户可能仍需要像本文一样的取实现个性化需求。为了解决这个问题,Spring Security 引入了 ObjectPostProcessor 的概念,它可用于修改或替换 Java Configuration 创建的许多 Object 实例。 FilterSecurityInterceptor 的替换配置正是通过这种方式来进行:@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
private static final String LOGIN_PROCESSING_URL = “/process”;/**
Json login post processor json login post processor.
@return the json login post processor
/
@Bean
public JsonLoginPostProcessor jsonLoginPostProcessor() {
return new JsonLoginPostProcessor();
}/**
Pre login filter pre login filter.
@param loginPostProcessors the login post processors
@return the pre login filter
/
@Bean
public PreLoginFilter preLoginFilter(CollectionloginPostProcessors) {
return new PreLoginFilter(LOGIN_PROCESSING_URL, loginPostProcessors);
}/**
Jwt 认证过滤器.
@param jwtTokenGenerator jwt 工具类 负责 生成 验证 解析
@param jwtTokenStorage jwt 缓存存储接口
@return the jwt authentication filter
/
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {
return new JwtAuthenticationFilter(jwtTokenGenerator, jwtTokenStorage);
}/**
The type Default configurer adapter.
/
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private PreLoginFilter preLoginFilter;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
@Autowired
private AccessDecisionManager accessDecisionManager;@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {super.configure(auth);
}
@Override
public void configure(WebSecurity web) {super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {http.csrf().disable() .cors() .and() // session 生成策略用无状态策略 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint()) .and() // 动态权限配置 .authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(filterSecurityInterceptorObjectPostProcessor()) .and() .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class) // jwt 必须配置于 UsernamePasswordAuthenticationFilter 之前 .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 登录 成功后返回jwt token 失败后返回 错误信息 .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler) .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());
}
/**
- 自定义 FilterSecurityInterceptor ObjectPostProcessor 以替换默认配置达到动态权限的目的
- @return ObjectPostProcessor
- /
private ObjectPostProcessorfilterSecurityInterceptorObjectPostProcessor() {
return new ObjectPostProcessor() {
};@Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setAccessDecisionManager(accessDecisionManager); object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource); return object; }
}
}
}然后你编写一个 Controller 方法就将其在数据库注册为一个资源进行动态的访问控制了。无须注解或者更详细的 Java Config 配置。
- 总结
从最开始到现在一共10个 DEMO 。我们循序渐进地从如何学习 Spring Security 到目前实现了基于 RBAC、动态的权限资源访问控制。如果你能坚持到现在那么已经能满足了一些基本开发定制的需要。当然 Spring Security 还有很多局部的一些概念,我也会在以后抽时间进行讲解
纠正对 JSON Web Token 认识的误区
1.前言
JSON Web Token (JWT) 其实目前已经广为软件开发者所熟知了,但是 JOSE (Javascript Object Signing and Encryption) 却鲜有人知道,我第一次知道它是在 Spring Security 的官方文档中,它改变了我对 JWT 的一些认识。目前国内能找到相关中文资料不是太多。所以我觉得有必要归纳一下。
- JOSE 概述
JOSE 是一种旨在提供在各方之间安全传递声明(claims)的方法的规范集。我们常用的 JWT 就包含了允许客户端访问特定应用下特定资源的声明。JOSE 制定了一系列的规范来达到此目的。目前该规范还在不断的发展,我们常用的包含以下几个 RFC :
JWS(RFC 7515) -JSON Web签名,描述生成和处理签名消息
JWE(RFC 7516) -JSON Web加密,描述了保护和处理加密 消息
JWK(RFC 7517) -JSON Web密钥,描述 Javascript 对象签名和加密中加密密钥的 格式和处理
JWA(RFC 7518) -JSON Web算法,描述了 Javascript 对象签名和加密中使用的 加密 算法
JWT(RFC 7519) -JSON Web令牌,描述以 JSON 编码并由 JWS 或 JWE 保护的声明的表示形式
3. 我们都看错了 JWT
看了对 JWT 的描述中提到 “令牌以 JWS 或者 JWE 声明表示”。莫非我之前的认知是错误的吗? 找了一些官方的资料研究了一番后,确实我之前的认知是不够全面的。
官方定义:
JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties
直译过来:JSON Web令牌(JWT)是一种紧凑的URL安全方法,用于表示要在两方之间转移的声明。
也就是说我们通常说的 JWT 实际上是一个对声明进行 JOSE 处理方式的统称。我们之前用的应该叫 JWS(JSON Web Signature),是 JWT 的一种实现,除了 JWS , JWT 还有另一种实现 JWE(JSON Web Encryption) 。它们之间的关系应该是这样的:
- 什么是 JWE
JWS 我们就不说了,就是通常我们所说的 JWT。包括之前我在 Spring Security 实战干货 中所涉及到的 JWT 都是 JWS。我们来说一下 JWE 。JWS 仅仅是对声明(claims)作了签名,保证了其不被篡改,但是其 payload(中段负载) 信息是暴露的。也就是 JWS 仅仅能保证数据的完整性而不能保证数据不被泄露。所以我以前也说过它不适合传递敏感数据。
JWE 的出现就是为了解决这个问题的。具体的可以看下图:
从上面可以看出 JWE 的生成非常繁琐,作为 Token 可能比较消耗资源和耗时。用作安全的数据传输途径应该不错。
- Spring Security jose 相关
这里需要简单提一下 Spring Security 提供了 JOSE 有关的类库 spring-security-oauth2-jose ,你可以使用该类库来使用 JOSE 。如果 Java 开发者要在 Spring Security 安全框架中使用 OAuth2.0 ,这个类库也是需要研究一下的。
简单的认识 OAuth2.0 协议
1.前言
欢迎阅读 Spring Security 实战干货 系列文章 。OAuth2.0 是近几年比较流行的授权机制,对于普通用户来说可能每天你都在用它,我们经常使用的第三方登录大都基于 OAuth2.0。随着应用的互联互通,个性化服务之间的相互调用,开放性的认证授权成为 客观的需要。
- OAuth2.0 的简单认识
OAuth定义了如下角色,并明确区分了它们各自的关注点,以确保快速构建一致性的授权服务:
Resource Owner 资源拥有者,通常指的是终端用户,其作用是同意或者拒绝、甚至是选择性的给第三方应用程序的授权请求。
User Agent 用户代理 指的的资源拥有者授权的一些渠道。一般指的是浏览器、APP
Client 请求授权和请求访问受限资源的客户端程序。
Authorization Server 对用户授权进行鉴别并根据鉴别结果进行同意或拒绝的授权响应的服务器。
Resource Server 能够接受和响应受保护资源请求的服务器。
单纯的文字性描述是不是有些难以理解。所以我这里讲一个亲身经历的事例来情景化以上的四个概念。马上又到程序员集中面试的季节了,有一年我去面试,到了地方才发现如此的“高大上”,访客需要通过验证码才能通过闸门,于是我联系了面试公司的HR ,后面的流程大概是这样的:
我向面试公司(HR)发送了一个进门的要求。
HR 给了我一个可以获取进门许可请求的链接。
我通过链接进行进门许可请求。
请求得到响应,返给我一个验证码。
我在闸门程序中输入验证码。
验证通过后放行。
在我学习了 OAuth2.0 协议之后我发现这次经历可以体现出 OAuth2.0 的一些设计理念。访客必须通过授权才能访问大楼。这种方式避免了闲杂人等出入办公场所,而且对访客可控(从访问时间和次数上),甚至可以实现对楼层的访问可控(当然上面的例子中没有)。
再结合 OAuth2.0 可以知道 访客就是 Client,公司(业主)就是 Resource Owner,物业就是 Authorization Server ,那个闸机就是 Resource Server,闸机有可能也受到物业的管控。 这是那张著名的流程图:
这个例子只是为了快速的来认识 OAuth2.0 ,它是一种有效的、可靠的委托授权框架。它提供了多种授权模式在不同的场景下供你选择。
- 授权模式
为了获得访问许可,客户端需要向授权服务器出示有效的授权凭证,也就是说客户端必须得到用户授权(authorization grant)OAuth2.0 提供了多种授权模式供开发者在不同的场景中使用,以下是授权模式的一些总结:
授权方式 客户端类型/用例
Authorization code 旨在用于具有后端的传统Web应用程序以及本机(移动或桌面)应用程序,以利用通过系统浏览器的单点登录功能。
Implicit 适用于不带后端的基于浏览器的(JavaScript)应用程序。
Password 对于应用程序和授权服务器属于同一提供程序的受信任本机客户端。
Client credentials 客户端以自己的名义来获取许可,而不是以终端用户名义,或者可以说该用户端也是资源拥有者
Refresh token 令牌失效后使客户端可以刷新其访问令牌,而不必再次执行代码或 密码授予的步骤。
SAML 2.0 bearer 使拥有SAML 2.0断言(登录令牌)的客户端将其交换为OAuth 2.0访问令牌。
JWT bearer 拥有一个安全域中的JSON Web令牌断言的客户端将其交换为另一域中的OAuth 2.0访问令牌。
Device code 设备的唯一编码,一般该编码不可更改,多用于一些智能设备
Token exchange 使用代理模拟的方式获取令牌
其中前五种为我们所熟知。我们后续会详细介绍它们。
- OAuth 2.0 的一些要点
摘自《OAuth 2 实战》:
由于 OAuth2.0 被定义为一个框架,对于 OAuth2.0 是什么和不是什么,一直未明确。我们所说的 OAuth2.0 是指 OAuth2.0 核心规范中定义的协议,RFC 6749 核心规范详述了一系列获取访问令牌的方法;还包括其伴随规范中定义的 bearer 令牌,RFC 6750该规范规定了这种令牌的用法。获取令牌和使用令牌这两个环节是 OAuth2.0 的基本要素。正如我们将在本节中看到的,在更广泛的 OAuth2.0 生态系统中存在很多其他技术,它们配合 OAuth2.0 核心,提供更多 OAuth2.0 本身不能提供的功能。我们认为,这样的生态系统是协议健康发展的体现,但是不应与协议本身混为一谈。
基于以上的原则 OAuth2.0 有以下一些要点需要明确被认识到:
OAuth2.0 并非身份认证协议,虽然在授权的过程中涉及到身份认证,但是 OAuth2.0 协议本身并不处理用户的信息。客户端访问受保护的资源时并不关心资源的拥有者。
OAuth2.0 不提供一些消息签名,为了保证安全性所以不应脱离 Https 。在使用其它协议或者系统时也应该明确一个安全机制来承担 Https 所承担的任务。
OAuth2.0 并没有定义加密方式,虽然目前使用的较多的是 JOSE 规范
OAuth2.0 虽然令牌被客户端持有并使用,但是客户端并不能解析以及处理令牌。
5. 总结
OAuth2.0 其实还有个特点,它并不是单体协议。它被分成了多个定义和流程,每个定义和流程都有各自的应用场景
OAuth2.0 技术选型参考
前言
在使用 OAuth2.0 中 Authorization Server (授权服务器)是一个回避不了的设施,在大多数情况下我们调用的是一些知名的、可靠的、可信任的第三方平台,比如 QQ、微信、微博、github 等。我们的应用只作为 Client 进行注册接入即可。也就是说我们只需要实现 OAuth2.0 客户端的逻辑就可以了,无须关心授权服务器的实现。然而有时候我们依然希望构建自己的 Authorization Server。我们应该如何实现?今天不会讨论具体的技术细节,来谈谈 OAuth2.0 的技术选型。Spring Security OAuth2 现状
在做 Spring Security 相关教程 的时候首先会考虑 Spring 提供的 OAuth2.0 功能。当我去 Spring 官网了解相关的类库时发现居然 Spring 的 OAuth2.0 类库即将过期的通知,有图有真相:
总结以下就是 Spring Security OAuth 的模块即将过期,后续的功能已经迁移到 Spring Security 5.2.x 中,但是不会再提供 Authorization Server 的功能。 在官方声明中还提到, 当前 Spring Security OAuth 分支是 2.3.x 和 2.4.x。2.3.x版本将于2020年3月达到停产期。我们将在达到功能均等后至少一年支持2.4.x版本。因此鼓励用户开始将其旧版 OAuth 2.0 客户端和资源服务器应用程序迁移到Spring Security 5.2 中的新支持。详细参见 官方博客.
- 对 OAuth2.0 的技术选型
从上面的信息看来, Spring Security 未来依然提供 OAuth2 的 客户端支持 和 资源服务器支持。授权服务器 将逐渐退出 Spring Security 的生态环境。所以如果没有授权服务器需求的情况下选择 Spring Security 依然是没有问题的,一旦有这个需求我们该如何选择?我这里调研了几个开源免费的项目。
3.1 keycloak
keycloak是 RedHat 公司出品。是一个致力于解决应用和服务身份验证与访问管理的开源工具。可以通过简单的配置达到保护应用和服务的目的。它提供了身份和访问管理的有用功能:
单点登录(SSO),身份代理和第三方登录。
支持 OpenID Connect,OAuth 2.0 和 SAML 2.0 等标准协议。
用户集中管理。
客户端适配器,轻松保护应用程序和服务。
可视化管理控制台和帐户管理控制台。
可扩展性、高性能、快速实现落地。
文档比较完毕,而且是一个成熟的、免费的商业级产品。
3.2 Nimbus SDK
全称是 Nimbus OAuth 2.0 / OpenID Connect SDK,这是一个类库。Spring 官方在博客中提到可以使用该类库构建 Authorization Server,它同时支持 OAuth2.0 和 OpenID Connect,比较完整地实现了这两个协议,而且针对补充协议也在积极的跟进。缺点在于中文教程不多而且是一个类库性质的。不过官方提供了 DEMO ,有能力的同学入门也不算难事。
3.3 Apache Oltu
Apache Oltu 是 Apache 基金会旗下的一个毕业项目。提供了 OAuth2.0 的常用实现,根据文档提供的信息来看上手还是比较简单的,模块化的提供了对 Authorization Server、Resource Server、Client、JOSE、 的支持。中文教程网上还是有不少的,缺点在于项目维护比较滞后,最新的版本是 2016 年发布的。
3.4 Vertx-auth-oauth2
vertx-auth-oauth2 属于 Vert.x 生态,提供了比较完整的 OAuth2.0 实现,而且项目维护比较活跃,唯一的缺点在于有技术栈的局限性。