SpringSecurity 以及 Auth2.0 学习圣经: 从入门到精通 SpringSecurity& Auth2.0
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
SpringSecurity 以及 Auth2.0 学习圣经: 从入门到精通 SpringSecurity& Auth2.0
尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,并且拿了很多大厂offer。
其中 SpringCloud 工业级底座 ,是大家的面试核心,面试重点:
说说:OAuth2.0 的四种授权方式,说说是哪四种?
说说:Spring Security的原理?
说说:Spring Security是如何实现 OAuth2.0 ?
最近有小伙伴在面试蚂蚁,问到了相关的面试题,可以说是逢面必问。
小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,联合社群小伙伴,来一个 SpringSecurity& Auth2.0 学习圣经: 从入门到精通 SpringSecurity& Auth2.0 。
特别说明的是, 本学习圣经属于 尼恩团队 从0到1 大实战:穿透 SpringCloud 工业级 底座工程(一共包括 15大圣经的 ) 其中之一。
15大圣经 ,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
尼恩团队 从0到1 大实战 SpringCloud 工业级底座 的 知识体系的轮廓如下,详情请点击:15大圣经的介绍:
工业级脚手架实现的包括的 15大学习圣经,目录如下:
其中,专题1 权限设计以及 安全认证相关的两个圣经,具体如下:
- SpringSecurity& Auth2.0 学习圣经: 从入门到精通 SpringSecurity& Auth2.0
- Sa-Token学习圣经: 从入门到精通Sa-Token
本文,就是 SpringSecurity& Auth2.0 学习圣经的 v1.0版本。 这个版本,稍后会录制视频, 录完之后,正式版本会有更新, 最新版本找尼恩获取。
1 安全认证的基本概念
两个基本概念
- 认证(Authentication)
- 授权(Authorization)
1.1 认证
认证就是根据用户名密码登录的过程,就是所谓的登录认证
对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:
- 如果校验通过,则:正常返回数据。
- 如果校验未通过,则:抛出异常,告知其需要先进行登录。
那么,判断会话是否登录的依据是什么?
我们先来简单分析一下登录访问流程:
- 用户提交
name
+password
参数,调用登录接口。 - 登录成功,返回这个用户的 Token 会话凭证。
- 用户后续的每次请求,都携带上这个 Token。
- 服务器根据 Token 判断此会话是否登录成功。
所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。
1.2 授权(鉴权)
所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限:
- 有,就让你通过。
- 没有?那么禁止访问!
深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。
例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"]
,这时候我来校验权限 "user-update"
,则其结果就是:验证失败,禁止访问。
2 Spring Security 核心组件
Spring Security 核心组件有 Authentication(认证/身份验证) 、 AuthenticationProvider(认证提供者) 、 AuthenticationManager(认证管理者)
比较重要的两个感念
- Authentication:认证,其实就是身份的识别,也就是登录
- Authorization:授权(鉴权),根据角色进行权限控制
2.1 凭证 Authentication
Authentication 直译是“认证”的意思,
在 Spring Security 中, Authentication 接口用来表示凭证或者令牌,可以理解为用户的用户名、密码、权限等信息。
Authentication 的代码如下:
public interface Authentication extends Principal, Serializable {
//权限集合
//可使用 AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_ADMIN")进行初始化
Collection<? extends GrantedAuthority> getAuthorities();
//用户名和密码认证时,可以理解为密码
Object getCredentials();
//认证时包含的一些详细信息,可以是一个包含用户信息的 POJO 实例
Object getDetails();
//用户名和密码认证时,可以理解为用户名
Object getPrincipal();
//是否认证通过,通过为 true
boolean isAuthenticated();
//设置是否认证通过
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Spring Security常见的内置的实现类
- UsernamePasswordAuthenticationToken: 用户名、密码认证的场景中作为验证的凭证
- RememberMeAuthenticationToken: “记住我”的身份认证场景
- AnonymousAuthenticationToken: 匿名访问的用户
除了以上内置凭证类外,还可以通过实现 Authentication 定制自己的身份认证实现类。
2.2 认证提供者 AuthenticationProvider
AuthenticationProvider 是一个接口,包含两个函数 authenticate 和 supports, 用于完成对凭证进行身份认证操作。
public interface AuthenticationProvider {
//对实参 authentication 进行身份认证操作
Authentication authenticate(Authentication authentication) throws AuthenticationException;
//判断是否支持该 authentication
boolean supports(Class<?> authentication);
}
AuthenticationProvider 接口常见内置的实现类:
- AbstractUserDetailsAuthenticationProvider: 对 UsernamePasswordAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于“用户名+密码”验证的场景。
- RememberMeAuthenticationProvider: 对 RememberMeAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于“记住我”的身份认证场景。
- AnonymousAuthenticationProvider: 这是一个对 AnonymousAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于匿名身份认证场景。
除此之外,可以通过实现 AuthenticationProvider 接口来扩展出自定义的认证提供者。
2.3 认证管理者 AuthenticationManager
AuthenticationManager 是一个接口,其唯一的 authenticate 验证方法是认证流程的入口,接收一个 Authentication 令牌对象作为参数。
public interface AuthenticationManager {
//认证流程的入口
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager 的一个实现类名为 ProviderManager,该类有一个 providers 成员变量,负责管理一个提供者清单列表,其源码如下:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
//提供者清单
private List<AuthenticationProvider> providers = Collections.emptyList();
//迭代提供者清单,找出支持令牌的提供者,交给提供者去执行令牌验证
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
}
}
认证管理者 ProviderManager 在进行令牌验证时,会对提供者列表进行迭代,找出支持令牌的认证提供者,并交给认证提供者去执行令牌验证。如果该认证提供者的 supports 方法返回 true,就会调用该提供者的 authenticate 方法。 如果验证成功, 那么整个认证过程结束;如果不成功, 那么继续处理列表中的下一个提供者。只要有一个验证成功, 就会认证成功。
3 Spring Security入门案例
- pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- HelloController
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "欢迎访问 hangge.com";
}
}
- 启动
生成密码如上图,用户名user
- 配置用户名和密码
如果对默认的用户名和密码不满意,可以在 application.properties 中配置默认的用户名、密码和角色。
spring.security.user.name=lxs
spring.security.user.password=1
spring.security.user.roles=admin
4 Spring Security进阶
4.1 简单认证流程实战
简单认证的处理流程大致包括以下步骤:
- 定制一个凭证/令牌类。
- 定制一个认证提供者类和凭证/令牌类进行配套,并完成对自制凭证/令牌实例的验证。
- 定制一个过滤器类,从请求中获取用户信息组装成定制凭证/令牌,交给认证管理者。
- 定制一个 HTTP 的安全认证配置类(AbstractHttpConfigurer 子类),将上一步定制的过滤器加入请求的过滤链。
- 定义一个 Spring Security 安全配置类(WebSecurityConfigurerAdapter 子类), 对 Web容器的 HTTP 安全认证机制进行配置。
入门案例,功能如下。当系统资源被访问时,过滤器从 HTTP 的 token 请求头获取用户名和密码,然后与系统中的用户信息进行匹配,如果匹配成功, 就可以访问系统资源,否则返回 403 响应码,表示未授权。
- DemoToken
定义自定义令牌类代码如下:
public class DemoToken extends AbstractAuthenticationToken
{
//用户名称
private String userName;
//密码
private String password;
...
}
- DemoAuthProvider
与DemoToken匹配的验证提供者DemoAuthProvider代码如下:
public class DemoAuthProvider implements AuthenticationProvider {
public DemoAuthProvider(){
}
//模拟的数据源,实际场景从 DB 中获取
private Map<String, String> map = new LinkedHashMap<>();
//初始化模拟的数据源,放入两个用户
{
map.put("zhangsan", "123456" );
map.put("lisi", "123456" );
}
//具体的验证令牌方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
DemoToken token = (DemoToken) authentication;
//从数据源 map 中获取用户密码
String rawPass = map.get(token.getUserName());
//验证密码,如果不相等,就抛出异常
if (!token.getPassword().equals(rawPass))
{
token.setAuthenticated(false);
throw new BadCredentialsException("认证有误:令牌校验失败" );
}
//验证成功
token.setAuthenticated(true);
return token;
}
/**
*判断令牌是否被支持
*@param authentication 这里仅仅 DemoToken 令牌被支持
*@return
*/
@Override
public boolean supports(Class<?> authentication)
{
return authentication.isAssignableFrom(DemoToken.class);
}
}
- DemoAuthFilter
定制一个过滤器类DemoAuthFilter,从请求头中获取 token 字段,解析之后组装成 DemoToken 令牌实例,提交给 AuthenticationManager 进行验证。
public class DemoAuthFilter extends OncePerRequestFilter
{
//认证失败的处理器
private AuthenticationFailureHandler failureHandler = new AuthFailureHandler();
...
//authenticationManager 是认证流程的入口,接收一个 Authentication 令牌对象作为参数
private AuthenticationManager authenticationManager;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
{
...
AuthenticationException failed = null;
try
{
Authentication returnToken=null;
boolean succeed=false;
//从请求头中获取认证信息
String token = request.getHeader(SessionConstants.AUTHORIZATION_HEAD);
String[] parts = token.split(",");
//组装令牌
DemoToken demoToken = new DemoToken(parts[0],parts[1]);
//提交给 AuthenticationManager 进行令牌验证
returnToken = (DemoToken) this.getAuthenticationManager().authenticate(demoToken);
//获取认证成功标志
succeed=demoToken.isAuthenticated();
if (succeed)
{
//认证成功,设置上下文令牌
SecurityContextHolder.getContext().setAuthentication(returnToken);
//执行后续的操作
filterChain.doFilter(request, response);
return;
}
} catch (Exception e)
{
logger.error("认证有误", e);
failed = new AuthenticationServiceException("请求头认证消息格式错误",e );
}
if(failed == null)
{
failed = new AuthenticationServiceException("认证失败");
}
//认证失败了
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
...
}
- AbstractHttpConfigurer
为了使得过滤器能够生效,必须将过滤器加入 Web 容器的 HTTP 过滤处理责任链,此项工作可以通过实现一个 AbstractHttpConfigurer 配置类来完成。
public class DemoAuthConfigurer<T extends DemoAuthConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T,B>
{
//创建认证过滤器
private DemoAuthFilter authFilter = new DemoAuthFilter();
//将过滤器加入 http 过滤处理责任链
@Override
public void configure(B http) throws Exception
{
//获取 Spring Security 共享的 AuthenticationManager 认证管理者实例
//将其设置到认证过滤器
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
DemoAuthFilter filter = postProcess(authFilter);
//将过滤器加入 http 过滤处理责任链
http.addFilterBefore(filter, LogoutFilter.class);
}
}
- DemoWebSecurityConfig
定义一个 Spring Security 安全配置类(WebSecurityConfigurerAdapter 子类), 对 Web 容器的 HTTP 安全认证机制进行配置。有两项工作:
- 应用DemoAuthConfigurer 配置类;
- 构造 AuthenticationManagerBuilder 认证管理者实例。
@EnableWebSecurity
public class DemoWebSecurityConfig extends WebSecurityConfigurerAdapter
{
//配置 HTTP 请求的安全策略,应用 DemoAuthConfigurer 配置类实例
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
...
.and()
//应用 DemoAuthConfigurer 配置类
.apply(new DemoAuthConfigurer<>())
.and()
.sessionManagement().disable();
}
//配置认证 Builder,由其负责构造 AuthenticationManager 认证管理者实例
//Builder 将构造 AuthenticationManager 实例,并且作为 HTTP 请求的共享对象存储
//在代码中可以通过 http.getSharedObject(AuthenticationManager.class) 来获取管理者实例
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
//加入自定义的 Provider 认证提供者实例
auth.authenticationProvider(demoAuthProvider());
}
//自定义的认证提供者实例
@Bean("demoAuthProvider" )
protected DemoAuthProvider demoAuthProvider()
{
return new DemoAuthProvider();
}
}
- 执行测试
通过swagger-ui界面直接访问返回403,表示认证失败
输入用户名,密码(zhangsan,123456),认证成功
4.2 基于数据源认证流程实战
4.2.1 常见内置类
生产场景中,用户信息都存储在某个数据源(如数据库) 中,认证过程中涉及从数据源加载用户信息的环节。 Spring Security 为这种场景内置了一套解决方案,主要涉及以下几个内置类。
- UsernamePasswordAuthenticationToken
实现了 Authentication 接口,主要封装用户输入的用户名和密码信息,提供给支持的认证提供者进行认证。
- AbstractUserDetailsAuthenticationProvider
与 UsernamePasswordAuthenticationToken 凭证/令牌类配套,但这是一个抽象类,具体的验证逻辑需要由子类完成。
常用子类为 DaoAuthenticationProvider 类, 该类依赖一个 UserDetailsService用户服务数据源,用于获取 UserDetails 用户信息,其中包括用户名、密码和所拥有的权限等。此认证提供者子类从数据源 UserDetailsService 中加载用户信息后,将待认证的令牌中的“用户名+密码”信息和所加载的数据源用户信息进行匹配和验证。
- UserDetailsService
UserDetailsService 有一个 loadUserByUsername 方法,其作用是根据用户名从数据源中查询用户实体(用户名,密码,角色)。一般情况下,需要实现一个自定义的 UserDetailsService 接口的实现类来从特定的数据源获取用户信息。用户信息服务接口的源码如下:
public interface UserDetailsService {
//通过用户名从数据源加载用户信息
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
- UserDetails
UserDetails 是一个接口,主要封装用户名、密码、是否过期、是否可用等信息。此接口的源码如下:
public interface UserDetails extends Serializable {
//权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//密码,一般为密文
String getPassword();
//用户名
String getUsername();
//用户名是否未过期
boolean isAccountNonExpired();
//用户名是否未锁定
boolean isAccountNonLocked();
//用户密码是否未过期
boolean isCredentialsNonExpired();
//账号是否可用(可理解为是否删除)
boolean isEnabled();
}
UserDetails 接口的密码属性和 UsernamePasswordAuthenticationToken 的密码属性的区别在于:前者的密码来自数据源,是密文;后者的密码来自用户请求,是明文。明文和密文的匹配工作由PasswordEncoder 加密器完成。
- PasswordEncoder
PasswordEncoder 是一个负责明文加密、 判断明文和密文匹配的接口,源码如下:
public interface PasswordEncoder {
//对明文 rawPassword 加密
String encode(CharSequence rawPassword);
//判断 rawPassword 与 encodedPassword 是否匹配
boolean matches(CharSequence rawPassword, String encodedPassword);
}
PasswordEncoder 的内置实现类有多个,推荐BCryptPasswordEncoder,其采用 SHA-256 +密钥+盐的组合方式对密码明文进行 Hash编码处理。
注意, SHA-256 是 Hash 编码算法,不是加密算法。这里是对明文编码而不是加密,这是因为加密算法往往可以解密,只是解密的复杂度不同;而编码算法则不一样,其过程是不可逆的。
密码明文编码之后,只有用户知道密码,甚至后台管理员都无法直接看到用户的密码明文。当用户忘记密码后,只能重置密码(通过手机验证码或者邮箱的形式)。
所以, 即使数据库泄露,黑客也很难破解密码。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
4.2.2 基于数据源认证实战
- 定制一个凭证/令牌类
在DemoAuthFilter封装UsernamePasswordAuthenticationToken,代码如下:
//方式二:数据库 认证演示
UserDetails userDetails = User.builder()
.username(parts[0])
.password(parts[1])
.authorities(USER_INFO)
.build();
//创建一个用户名+密码的凭证,一般情况下,这里的密码需要明文
Authentication userPassToken = new UsernamePasswordAuthenticationToken(userDetails,
userDetails.getPassword(),
userDetails.getAuthorities());
//进入认证流程
returnToken = this.getAuthenticationManager().authenticate(userPassToken);
succeed = userPassToken.isAuthenticated();
//方式二end
- 认证提供者
配置DaoAuthenticationProvider匹配验证UsernamePasswordAuthenticationToken
@EnableWebSecurity
public class DemoWebSecurityConfig extends WebSecurityConfigurerAdapter
{
...
//注入全局 BCryptPasswordEncoder 加密器容器实例
@Resource
private PasswordEncoder passwordEncoder;
//注入数据源服务容器实例
@Resource
private DemoAuthUserService demoUserAuthService;
@Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception
{
//创建一个数据源提供者
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
//设置加密器
daoProvider.setPasswordEncoder(passwordEncoder);
//设置用户数据源服务
daoProvider.setUserDetailsService(demoUserAuthService);
return daoProvider;
}
}
- DemoAuthUserService
认证提供者依赖DemoAuthUserService,和PasswordEncoder,定义DemoAuthUserService自定义数据源服务类,模拟从数据源获取用户信息。
@Slf4j
@Service
public class DemoAuthUserService implements UserDetailsService
{
//模拟的数据源,实际从 DB 中获取
private Map<String, String> map = new LinkedHashMap<>();
//初始化模拟的数据源,放入两个用户
{
map.put("zhangsan", "123456");
map.put("lisi", "123456");
}
/**
*装载系统配置的加密器
*/
@Resource
private PasswordEncoder passwordEncoder;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
//实际场景中需要从数据库加载用户
//这里出于演示的目的, 用 map 模拟真实的数据源
String password = map.get(username);
if (password == null)
{
return null;
}
if (null == passwordEncoder)
{
passwordEncoder = new BCryptPasswordEncoder();
}
/**
*返回一个用户详细实例,包含用户名、加密后的密码、用户权限清单、用户角色
*/
UserDetails userDetails = User.builder()
.username(username)
.password(passwordEncoder.encode(password))
.authorities(SessionConstants.USER_INFO)
.roles("USER")
.build();
return userDetails;
}
}
5 授权(鉴权)控制
Spring Security中定义了四个支持权限控制的表达式注解,分别是
- @PreAuthorize
- @PostAuthorize
- @PreFilter和
- @PostFilter。
其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。在需要控制权限的方法上,我们可以添加@PreAuthorize注解,用于方法执行前进行权限检查,校验用户当前角色是否能访问该方法。
- EnableGlobalMethodSecurity
使用上述注解控制权限需要设置@EnableGlobalMethodSecurity(prePostEnabled = true)
- TestController
@RestController
public class TestController {
Logger logger = LoggerFactory.getLogger(TestController.class);
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "product id : " + id;
}
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
return "order id : " + id;
}
@GetMapping("/book/{id}")
public String getBook(@PathVariable String id) {
return "book id : " + id;
}
@GetMapping("/anno/{id}")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String getAnno(@PathVariable String id) {
return "admin id :" + id;
}
@RequestMapping("/hello")
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
public String hello() {
return "hello you ...";
}
@GetMapping("/getPrinciple")
public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, Principal principal, Authentication authentication) {
logger.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
logger.info(oAuth2Authentication.toString());
logger.info("principal.toString() " + principal.toString());
logger.info("principal.getName() " + principal.getName());
logger.info("authentication: " + authentication.getAuthorities().toString());
return oAuth2Authentication;
}
}
- 控制权限方式
(1)过滤器授权控制
(2)注解授权控制
(3)权限测试
使用Postman测试,分别获得管理员和刘国梁的令牌,其中管理员用户属于ADMIN角色,刘国梁不属于USER角色,这时使用刘国梁用户的令牌访问上述请求,会被拒绝
注意:此时关闭授权中心,可以看到资源中心依然可以使用JWT实现授权控制,说明使用资源中心在使用公钥验证令牌
如果希望一个方法能被多个角色访问,使用@PreAuthorize("hasAnyAuthority('admin','user')")
得到当前用户的方法:
SecurityContextHolder.getContext().getAuthentication() //使用工具方法
@Resource
Authentication authentication //注入的方法
6 Spring Security过滤器
在DemoAuthFilter设置断点,通过分析线程栈帧得出Spring Security中过滤器链结构如下:
外置过过滤器链
spring security内置过滤器链
从上图我们可以看出 Spring Security 以一个单 Filter(FilterChainProxy) 存在于整个过滤器链中,而这个 FilterChainProxy 实际内部代理着众多的 Spring Security Filter 。
这简直就是套娃啊!
过滤器链的形成过程,如下图
WebSecurity类的performBuild()方法通过securityFilterChainBuilder将HttpSecurity构建的SecurityFilterChain整合到一起,生成FilterChainProxy
Spring Security就是引入了一系列的SecurityFilter,将其添加到Spring中去了;
在有请求时,根据URL是否符合每个Filter的规则来判断是否需要该Filter来进行处理,加载过程大致如下:
Security默认的Filter入口在HttpSecurity对象中;在HttpSecurity对象中,实际提供的是各默认Filter的配置类,通过配置类来控制对应Filter的各个属性配置;在配置完成将Filter加载到HttpSecurity中的FilterChain中去。
Configurer | Filter | 功能说明 |
---|---|---|
OpenIDLoginConfigurer | OpenIDAuthenticationFilter | 处理OpenID授权请求 |
HeaderWriterFilter | HeadersConfigurer | 在返回报文头中添加Security相关信息 |
CorsConfigurer | CorsFilter | 提供跨域访问配置支持的Filter |
SessionManagementConfigurer | SessionManagementFilter | 会话管理Filter |
PortMapperConfigurer | 无 | 用于在Http及Https请求之间重定向时的端口判定 |
JeeConfigurer | J2eePreAuthenticatedProcessingFilter | 添加J2EE预授权处理机制支持 |
X509Configurer | X509AuthenticationFilter | 添加X509预授权处理机制支持 |
RememberMeConfigurer | RememberMeAuthenticationFilter | 记住用户名及密码功能支持 |
ExpressionUrlAuthorizationConfigurer | FilterSecurityInterceptor | Security的主要Filter,通过调用权限管理器等进行Http访问的权限判断 |
RequestCacheConfigurer | RequestCacheAwareFilter | 缓存请求并在必要的时候使用缓存的请求 |
ExceptionHandlingConfigurer | ExceptionTranslationFilter | 处理AccessDeniedException及AuthenticationException异常 |
SecurityContextConfigurer | SecurityContextPersistenceFilter | SecurityContext对象持久化Filter,用于在请求开始阶段初始化并持久化该对象,在后续的Filter中可以使用该对象来获取信息 |
ServletApiConfigurer | SecurityContextHolderAwareRequestFilter | 在原始请求基础上包装一些方法供后续调用 |
CsrfConfigurer | CsrfFilter | 跨站请求伪造保护Filter; |
LogoutConfigurer | LogoutFilter | 退出登录请求处理Filter |
AnonymousConfigurer | AnonymousAuthenticationFilter | 匿名请求控制Filter |
FormLoginConfigurer | UsernamePasswordAuthenticationFilter | 表单登录请求处理Filter |
OAuth2LoginConfigurer | OAuth2AuthorizationRequestRedirectFilter | OAuth2请求权限控制处理Filter,为其它网站提供本网站Oauth2方式登录,即其它网站通过本网站的账户密码进行登录授权 |
ChannelSecurityConfigurer | ChannelProcessingFilter | 通道选择Filter,确保请求是通过正确的通道过来的,如Http或者Https |
HttpBasicConfigurer | BasicAuthenticationFilter | Security基础登录授权Filter,将其结果保存在SecurityContextHolder中 |
- EnableWebSecurity
在继承了WebSecurityConfigurerAdapter的配置类上,加上@EnableWebSecurity注解,WebSecurityConfigurerAdapter将会自动添加如表单登录、记住用户名密码等Filter。
导入WebSecurityConfiguration,WebSecurityConfiguration对象会被spring容器托管
- WebSecurityConfiguration
- 创建WebSecurity
- 创建springSecurityFilterChain
- WebSecurity
WebSecurity的performBuild方法负责创建filterChains(责任链)
WebSecurity对象在WebSecurityConfiguration中初始化后,在生成名称为springSecurityFilterChain 的Filter时,会调用其Build方法;Build方法实际上为WebSecurity的父类提供的方法,最终调用的为其本身的performBuild 方法:
- HttpSecurity
在WebSecurityConfigurerAdapter类中,存在默认的configure方法,它会提供一些默认的权限控制配置,默认方法实现如下:
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using default configure(HttpSecurity). "
+ "If subclassed this will potentially override subclass configure(HttpSecurity).");
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin();
http.httpBasic();
}
HttpSecurity用于提供一系列的Security默认的Filter,最终在WebSecurity对象中,组装到最终产生的springSecurityFilterChain 对象中去;
@Override
protected DefaultSecurityFilterChain performBuild() {
this.filters.sort(OrderComparator.INSTANCE);
List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
for (Filter filter : this.filters) {
sortedFilters.add(((OrderedFilter) filter).filter);
}
return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}
实际就组装了一个DefaultSecurityFilterChain对象并返回
面试题:说说Spring Security的原理?
Spring Security是一个基于Spring框架的安全性认证和授权框架,它提供了全面的安全性解决方案,可以保护Web应用程序中的所有关键部分。
Spring Security的核心原理是拦截器(Filter)。
Spring Security会在Web应用程序的过滤器链中添加一组自定义的过滤器,这些过滤器可以实现身份验证和授权功能。
当用户请求资源时,Spring Security会拦截请求,并使用配置的身份验证机制来验证用户身份。如果身份验证成功,Spring Security会授权用户访问所请求的资源。
Spring Security的具体工作原理如下:
1.用户请求Web应用程序的受保护资源。
2.Spring Security拦截请求,并尝试获取用户的身份验证信息。
3.如果用户没有经过身份验证,Spring Security将向用户显示一个登录页面,并要求用户提供有效的凭据(用户名和密码)。
4.一旦用户提供了有效的凭据,Spring Security将验证这些凭据,并创建一个已认证的安全上下文(SecurityContext)对象。
5.安全上下文对象包含已认证的用户信息,包括用户名、角色和授权信息。
6.在接下来的请求中,Spring Security将使用已经认证的安全上下文对象来判断用户是否有权访问受保护的资源。
7.如果用户有权访问资源,Spring Security将允许用户访问资源,否则将返回一个错误信息。
7 Spring Security Oauth2
凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。
而单点登录——便是我们搭建统一认证中心的关键。
7.1 什么是单点登录?解决什么问题?
举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, 为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统。
单点登录——就是为了解决这个问题而生!
简而言之,单点登录可以做到: 在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。
spring security框架没有专门的SSO模块可以使用Spring Security Oauth2模块实现单点登录(SSO)
7.2 OAuth2是什么?
OAuth简单说就是一种授权的协议,只要授权方和被授权方遵守这个协议去写代码提供服务,那双方就是实现了OAuth模式。
举个例子,你想登录京东,但你又从来没注册过京东账号,又不想新注册新账号访问京东,怎么办呢?不用担心,京东已经为你这种懒人做了准备,用你的qq号可以授权给京东进行登录。
- 在京东官网点击qq登录
- 跳转到qq登录页面输入用户名密码,然后点授权并登录
- 调回到京东页面,成功登录
上述例子中的京东就是客户端,QQ就是认证服务器,
OAuth2.0就是客户端和认证服务器之间由于相互不信任而产生的一个授权协议,要是相互信任那QQ直接把自己数据库给京东好了,你直接在京东输入qq账号密码查下数据库验证就可以登录,还跳来跳去的多麻烦。
7.3 OAuth2核心概念
OAuth2是目前最流行的授权协议,用来授权第三方应用,获取用户数据
7.3.1 OAuth2角色
- 客户端 Client:第三方应用,比如上面的 京东
- 资源所有者 Resource Owner:资源所有者,即用户
- 授权服务器 Authorization Server:授权服务器,即提供第三方登录服务的服务器,如QQ
- 资源服务器 Resource Server:拥有资源信息的服务器,通常和授权服务器属于同一应用,如QQ
7.3.2 OAuth2授权模式
Oauth2授权模式分为授权码模式、简化模式、密码模式和客户端模式,分别解析如下。
-
授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。
它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本 都是使用这种模式。
-
简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。
-
密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
-
客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
7.4. Spring Security Oauth2.0实战
7.4.1 授权服务器(SSO-Server)
- pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
- oauth2配置类
@Configuration
//开启授权服务
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
private static final String CLIENT_ID = "cms";
private static final String SECRET_CHAR_SEQUENCE = "{noop}secret";
private static final String SCOPE_READ = "read";
private static final String SCOPE_WRITE = "write";
private static final String TRUST = "trust";
private static final String USER ="user";
private static final String ALL = "all";
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 30*60;
private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 30*60;
// 密码模式授权模式
private static final String GRANT_TYPE_PASSWORD = "password";
//授权码模式
private static final String AUTHORIZATION_CODE = "authorization_code";
//refresh token模式
private static final String REFRESH_TOKEN = "refresh_token";
//简化授权模式
private static final String IMPLICIT = "implicit";
//指定哪些资源是需要授权验证的
private static final String RESOURCE_ID = "resource_id";
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
// 使用内存存储
.inMemory()
//标记客户端id
.withClient(CLIENT_ID)
//客户端安全码
.secret(SECRET_CHAR_SEQUENCE)
//为true 直接自动授权成功返回code
.autoApprove(true)
.redirectUris("http://127.0.0.1:8084/cms/login") //重定向uri
//允许授权范围
.scopes(ALL)
//token 时间秒
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
//刷新token 时间 秒
.refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS)
//允许授权类型
.authorizedGrantTypes(GRANT_TYPE_PASSWORD,AUTHORIZATION_CODE);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 使用内存保存生成的token
endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore());
}
/**
* 认证服务器的安全配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//.realm(RESOURCE_ID)
// 开启/oauth/token_key验证端口认证权限访问
.tokenKeyAccess("isAuthenticated()")
// 开启/oauth/check_token验证端口认证权限访问
// .checkTokenAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
//允许表单认证
.allowFormAuthenticationForClients();
}
@Bean
public TokenStore memoryTokenStore() {
// 最基本的InMemoryTokenStore生成token
return new InMemoryTokenStore();
}
}
- Spring Security配置类
@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { //auth.inMemoryAuthentication()
auth.inMemoryAuthentication()
.withUser("lxs")
.password("{noop}123")
.roles("admin");
}
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/asserts/**");
web.ignoring().antMatchers("/favicon.ico");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http // 配置登录页并允许访问
.formLogin().permitAll()
// 配置Basic登录
//.and().httpBasic()
// 配置登出页面
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
// 配置允许访问的链接
.and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**", "/api/**").permitAll()
// 其余所有请求全部需要鉴权认证
.anyRequest().authenticated()
// 关闭跨域保护;
.and().csrf().disable();
}
}
- 测试
测试验证令牌的端点
7.4.2 资源服务器 (SSO-Client)
- pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
- 启动器配置
package com.lxs.oauth2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableResourceServer
public class CmsApplication {
public static void main(String[] args) {
SpringApplication.run(CmsApplication.class, args);
}
}
- oauth2配置类
@Configuration
public class Oauth2ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
private static final String CHECK_TOKEN_URL = "http://localhost:8888/oauth/check_token";
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl(CHECK_TOKEN_URL);
tokenService.setClientId("cms");
tokenService.setClientSecret("secret");
resources.tokenServices(tokenService);
}
}
- spring security配置类
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").authenticated();
// 禁用CSRF
http.csrf().disable();
}
}
- Controller
用于测试的结构
@RestController
public class HelloController {
@GetMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
@GetMapping("/index")
public String index() {
return "index";
}
}
- 测试
访问资源服务器api,没有令牌提示报错
7.4.3 授权模式实战
Oauth2授权模式分为授权码模式、简化模式、密码模式和客户端模式,分别解析如下。
- 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本 都是使用这种模式。
- 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。
- 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
7.4.3.1 授权码模式
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
授权码模式功能最完整、使用最广泛和流程最严密的授权模式。
QQ登录的授权码模式(Authorization Code Grant)登录流程
下看一下,QQ登录的授权码模式(Authorization Code Grant)登录流程
当用户访问资源时,比如在网易云音乐中使用第三方登录功能,例如QQ登录,那么这里的资源就是用户的QQ昵称和头像等信息。
此时第三方应用(网易云音乐)将发送请求到授权服务器(QQ)去获取授权,此时授权服务器(QQ)将返回一个界面给用户,用户需要登录到QQ,并同意授权网易云音乐获得某些信息(资源)。
当用户同意授权后,授权服务器将返回一个授权码(Authorization Code)给第三方应用(网易云音乐),此时第三方应用在通过client_id、client_secret(这是需要第三方应用在授权服务器去申请的)和授权码(Authorization Code)去获得Access Token和Refresh Token,然后授权码将失效。
最后,第三方应用通过Access Token去资源服务器请求资源了,资源服务器校验Access Token成功后将返回资源给第三方应用。
Spring Security Oauth2.0 授权码模式(Authorization Code Grant)登录流程
官网介绍的 授权码模式流程,如图:
官网介绍的 授权码模式流程,步骤如下:
- (A)客户端携带client_id、redirect_uri,中间通过代理者user-Agent (如浏览器 )访问授权服务器,如果已经登录过会直接返回redirect_uri,没有登录过,就重定向 跳转到登录页面
- (B)授权服务器对客户端进行身份验证(通过用户代理,让用户输入用户名和密码)
- (C)授权通过,会重定向到redirect_uri, 并携带授权码code作为uri参数
- (D)客户端携带授权码访问授权服务器
- (E)验证授权码通过,返回acceptToken
假设: 授权服务器 上的 用户账号 密码, 应用id和密码 如下:
用户账号 密码 和 应用id 密码,都是 授权服务器管理的。
从调接口的维度来说 ,Oauth2.0 授权码模式 简单来说分为三步:
-
第一步:从授权服务器,获取授权码 code:
eg:oauthServer+"/oauth/authorize?client_id="+clientId+"&response_type=code&redirect_uri="+redirectUrl+"&scope=all"
如果没有登录,则会跳转到统一身份认证登录页面。
如果用户登录了,调用接口后,会拿到授权码code,然后 重定向到redirect_uri,授权码会作为redirect_uri的参数
-
第二步:获取access_token
eg:oauthServer+"/oauth/token?code="+code+"&grant_type=authorization_code&client_secret="+clientSecret+"&redirect_uri="+redirectUri+"&client_id="+clientId
大致的响应信息
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1MzQ5NzMsInVzZXJfbmFtZSI6Im5pY2t5IiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJmMjM0M2Q0NC1hODViLTQyOGYtOWE1ZS1iNTE4NTAwNTM5ODgiLCJjbGllbnRfaWQiOiJvYSIsInNjb3BlIjpbImFsbCJdfQ.LWkN2gC2dBrGTn5uSPzfdW6yRj7jhlX87EE8scY02hI",
"token_type": "bearer",
"expires_in": 59,
"scope": "all",
"user_name": "nicky",
"jti": "f2343d44-a85b-428f-9a5e-b51850053988"
}
- 第三步:访问系统资源,此时统一认证服务会根据该认证客户端权限信息判断,决定是否返回信息。
访问:
http://localhost:8084/api/userinfo?access_token=${accept_token}
Spring Security Oauth2.0 角色设置
Spring Security Oauth2.0 主要包括如下角色:
- 资源所有者(Resource Owner)
- 用户代理(User Agent)
- 客户端(Client)
- 授权服务器(Authorization Server)
- 资源服务器(Resource Server)
Spring Security Oauth2.0 有两个注解,定义 资源服务器和授权服务器角色:
-
定义资源服务器,用注解 @EnableResourceServer;
-
定义授权服务器,用注解 @EnableAuthorizationServer;
生产环境、资源服务器和授权服务器一般是分开的,不过学习的可以放在一起
定义资源服务器,用注解@EnableResourceServer;
定义授权服务器,用注解@EnableAuthorizationServer;
支持授权码模式的配置
授权码模式的第一步:申请授权码
假设: 授权服务器 上的 用户账号 密码, 应用id和密码 如下:
用户账号 密码 和 应用id 密码,都是 授权服务器管理的。
申请授权码 , 需要 访问授权服务的 授权链接,授权码模式response_type参数传code:
Get请求:
http://localhost:8888/oauth/authorize?client_id=cms&client_secret=secret&response_type=code
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
因为 用户在 oauth服务器 没登录,所以授权服务器 会返回 SpringSecurity的默认登录页面,这个过程是通过 重定向完成的。
通过 idea 的http client 插件,可以看到,输入的地址是
http://localhost:8888/oauth/authorize?client_id=cms&client_secret=secret&response_type=code
服务端进行重定向了,重定向到了
http://localhost:8888/login
这个页面,在浏览器看到,是一个登录页面
SpringSecurity 重定向到的登录页面 , 具体代码是
http .formLogin().permitAll();
SpringSecurity 如果要 弹窗登录的,可以配置http.httpBasic();
当然,弹窗登录 配置是没有登录页面的,
如果不是弹窗登录,也不是默认的login页面,而是自定义登录页面,怎么办?
可以这样配置loginPage : http.formLogin().loginPage("/login").permitAll()
,
loginPage 的配置,可以参考OAuth2Config代码
如图,输入SpringSecurity配置的静态账号密码:lxs/123
登录之后,展示用户授权界面
登录成功后,返回redirect_uri (为了简单,这里配置为了 百度的地址),地址后面,带上了 授权码
代码里边的配置如下:
上面的代码中, OAuth2 客户端 是 inmemory类型, 仅仅作为演示和学习用。
生产环境, OAuth2 客户端 的 id、密码、 重定向的url 是 通过数据库配置的。
如何 通过数据库来管理 OAuth2客户端? 稍后给大家介绍。
授权码模式的第2步:申请令牌
拿到授权码后,申请令牌。
Post请求
http://localhost:8888/oauth/token?code=H45yPy&grant_type=authorization_code&redirect_uri=http://127.0.0.1:8084/cms/login&scope=all
http://127.0.0.1:8084/cms/login?code=H45yPy
url的 参数说明
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
直接使用 idea 的http client 插件,可以看到,401
使用浏览器去看,可以看到,需要basic 验证
此链接需要使用 http Basic认证。
什么是http Basic认证?
http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码” 的格式拼接,并用base64编 码,放在header中请求服务端,
Base64.getEncoder.encodeToString("客户端ID:客户端密码"),
一个 用户名:密码的base64编码 例子:
Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA=
否则, 认证失败服务端返回 401 Unauthorized。
在浏览器输入应用的 id和密码
输入之后, 报另一个错误
原因是,在浏览器使用了 get请求,这里需要发post请求。 所以,只能使用postman完成:
http basic认证:
客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。
点击发送: 申请令牌成功
返回信如下:
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
jti:当前token的唯一标识
授权码模式的第3步:令牌校验
Spring Security Oauth2提供校验令牌的端点,如下:
Get:
http://localhost:8888/oauth/check_token?token=171ce96e-7492-4a27-becd-8ccbdc69666b
授权码模式的第4步:使用令牌
- 使用正确令牌访问/index服务
- 不用令牌访问/index服务
- 使用错误令牌访问
如何 通过数据库来管理 OAuth2客户端?
在Spring Security中,ClientDetailsServiceConfigurer
是用来配置OAuth2客户端详情的,可以通过数据库来管理这些客户端的详细信息。
如何使用数据库来配置 ClientDetailsServiceConfigurer
呢?
大致的步骤如下:
1. 创建客户端详细信息的数据库表
Spring Security OAuth2期望在数据库中有一个表来存储客户端的详细信息,通常这个表叫做 oauth_client_details
。
表的结构如下:
CREATE TABLE oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
2. 配置 ClientDetailsServiceConfigurer
使用数据库
在你的 AuthorizationServerConfigurerAdapter
实现类中,通过 ClientDetailsServiceConfigurer
来配置客户端详细信息服务。
示例如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用JDBC方式从数据库加载客户端信息
clients.withClientDetails(clientDetailsService());
}
public JdbcClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
}
3. 配置数据源
确保 application.properties
或 application.yml
中正确配置了数据库连接:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
以上配置将使你的OAuth2客户端信息从数据库中读取,而不是硬编码在配置文件或代码中。通过这种方式,你可以轻松管理客户端的信息,如客户端ID、密钥、授权类型、重定向URI等。
7.4.3.2 密码模式
首先资源所有者(用户)提供自己的用户名和密码给客户端(Client),然后客户端(Client)携带从用户那里获取的凭证去授权服务器请求Token,
授权服务器对客户端进行身份认证,并校验资源所有者的凭证,如果都校验通过,则发放Token。
适用范围:只适用于应用是受信任的场景。
一个典型的例子是同一个企业内部的不同产品要使用本企业的 Oauth2.0 体系。
在这种情况下,由于是同个企业,不需要向用户展示“xxx将获取以下权限”等字样并询问用户的授权意向,而只需进行用户的身份认证即可。
这个时候,只需要用户输入凭证并直接传递给鉴权服务器进行授权即可。
密码模式(resource owner password credentials):密码模式中,用户向客户端提供自己的用户名和密码,这通常用在用户对客户端高度信任的情况
密码授权一般就是授权码模式,流程如下:
- (A)用户访问客户端,提供URI连接并包含授权服务器的 用户名和密码信息 给 业务服务,业务服务会带着这些信息去访问 授权服务器
- (B)授权服务器对客户端进行身份验证
- (C)授权通过,返回acceptToken给客户端
密码模式配置
密码模式的核心要点
老架构师尼恩给大家揭秘一下,密码模式的核心要点:
密码模式 相当于把 用户的账号和密码, 直接告诉了 业务服务器。
就相当于把qq的账号和密码,告诉其他的 业务应用。
跳过了 申请授权码的环节,直接申请令牌。
密码模式的第一步:申请令牌
grant_type:授权类型,填写password,表示密码模式
username:用户名
password:密码
以上测试使用postman完成:
http basic认证:
此链接需要使用 http Basic认证。
http Basic认证使用到 客户端Id和客户端密码,生产环境,会匹配数据库oauth_client_details表中的客户端id及客户端密码。
点击发送: 申请令牌成功
返回信如下:
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
jti:当前token的唯一标识
密码模式的第2步:令牌校验
Spring Security Oauth2提供校验令牌的端点,如下:
Get:
http://localhost:8888/oauth/check_token?token=1e628350-5711-4983-9b10-da7a7e8b9558
密码模式的第3步:使用令牌
- 使用正确令牌访问/index服务
- 不用令牌访问/index服务
- 使用错误令牌访问
7.4.3.3 简化模式
简化模式,也是相对于授权码模式而言的
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此称简化模式。
简化模式,流程如下:
- (A):客户端携带client_id、redirect_uri,中间通过代理者访问授权服务器,如果已经登录过会直接返回redirect_uri,没有登录过就跳转到登录页面
- (B)授权服务器对客户端进行身份验证(通过用户代理如浏览器,让用户输入用户名和密码)
- (C)授权通过,会重定向到redirect_uri并携带授权码token作为uri参数
- (D)客户端携带授权码访问资源服务器
- (E)验证token通过,返回资源
认证服务简化模式配置
简化模式的要点:
老架构师尼恩给大家揭秘一下,简化模式的核心要点:
简化模式 不会 用户的账号和密码, 直接告诉了 业务服务器。
保证了 账号和密码 的安全。
但是,简化模式 省略了授权码,直接返回了 令牌。
也是 跳过了 申请授权码的环节,又 节省了一个 通过授权换取令牌的 验证步骤。
既简单又安全。
简化模式第一步:申请令牌
访问授权链接,在浏览器访问就可以,授权码模式response_type参数传token:
Get请求:
http://localhost:8888/oauth/authorize?client_id=cms&redirect_uri=http://127.0.0.1:8084/cms/login&response_type=token&scope=all
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:简化模式固定为token
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址
因为没登录,所以会返回SpringSecurity的默认登录页面,具体代码是http .formLogin().permitAll();
,如果要弹窗登录的,可以配置http.httpBasic();
,这种配置是没有登录页面的,自定义登录页面可以这样配置http.formLogin().loginPage("/login").permitAll()
,参考OAuth2Config代码
如图,输入SpringSecurity配置的静态账号密码:lxs/123
登录成功,返回redirect_uri,直接拿到令牌
简化模式第2步:使用令牌
- 使用正确令牌访问/index服务
- 不用令牌访问/index服务
- 使用错误令牌访问
简化模式和授权码模式的区别
授权码模式User-agent(浏览器)只是持有授权码(code)使用授权码获得令牌,授权码,只能校验一次,这样即使授权码泄露,令牌相对安全,而简化模式由user agent(浏览器),直接持有令牌,相对不安全
7.4.3.4 客户端模式
客户端授权模式(Client Credentials Grant) 主要流程如下:
客户端(Client)通过Client_id和Client_secret去授权服务器请求Token,授权服务器认证Client_id和Client_secret是否正确,若正确则发放Token给客户端(Client)。
最后客户端通过AccessToken请求资源。
客户端模式的要点:
老架构师尼恩给大家揭秘一下,客户端模式的核心要点:
客户端模式 直接把 客户端应用的 id 和密码 告诉用户。
简单是简单, 这个也特不安全了。
适用范围:只适用于应用是受信任的场景。
客户端模式(client credentials):客户端模式(client credentials)适用于没有前端的命令行应用,即在命令行下请求令牌
客户端模式,流程如下:
- 第一步: 获取token
http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all
- 第二步:拿到acceptToken之后,就可以直接访问资源
认证服务客户端模式配置
- 申请令牌
访问授权链接,在浏览器访问就可以,授权码模式response_type参数传token:
post请求:
http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
client_secret:客户端秘钥,和授权配置类中设置的客户端secret一致。
response_type:密码模式固定为client_credentials
scop:客户端范围,和授权配置类中设置的scop一致。
-
使用令牌
-
使用正确令牌访问/index服务
- 不用令牌访问/index服务
- 使用错误令牌访问
说在最后:有问题找老架构取经
15大圣经 ,使得大家内力猛增,
可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》