[Spring Security] Spring Security OAuth2(密码模式)


目录


  • [Spring Security] Spring Security OAuth2(密码模式)
  • 简介
  • 名词定义
  • 准备工作
  • OAuth2流程
  • 核心配置类
  • 总结
  • REFRENCES
  • 更多




手机用户请​​横屏​​​获取最佳阅读体验,​​REFERENCES​​中是本文参考的链接,如需要链接和更多资源,可以关注其他博客发布地址。


平台

地址



简书

https://www.jianshu.com/u/3032cc862300

个人博客

https://yiyuery.github.io/NoteBooks/

简介

OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。

本文对OAuth 2.0的设计思路和运行流程,做一个简明通俗的解释,主要参考材料为RFC 6749。

>参考阮一峰的关于Oauth2的介绍

名词定义


  • Third-party application:第三方应用程序,本文中又称"客户端"(client)。
  • HTTP service:HTTP服务提供商,本文中简称"服务提供商"。
  • Resource Owner:资源所有者,本文中又称"用户"(user)。
  • User Agent:用户代理,本文中就是指浏览器。
  • Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
  • Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

准备工作

[Spring Security] Spring Security OAuth2(密码模式)_Spring Security

​spring-security-auth​​: 中心认证服务器

​spring-security-resources​​: 资源服务器(提供图书相关服务接口)

OAuth2流程


本文就OAuth2中客户端授权模式​​密码模式​​进行深入编码实战。


密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

[Spring Security] Spring Security OAuth2(密码模式)_服务器_02

​它的步骤如下​

(A)用户向客户端提供用户名和密码。

(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。

(C)认证服务器确认无误后,向客户端提供访问令牌。

​B步骤​​中,客户端发出的HTTP请求,包含以下参数:


  • grant_type:表示授权类型,此处的值固定为"password",必选项。
  • username:表示用户名,必选项。
  • password:表示用户的密码,必选项。
  • scope:表示权限范围,可选项。

​C步骤中,认证服务器向客户端发送访问令牌​​,包含以下参数


  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
    [Spring Security] Spring Security OAuth2(密码模式)_服务器_03

其中:

  • 获取Token时需要进行​​Basic​​认证

​http://localhost:8081/authServer/oauth/token?grant_type=password&username=u2&password=12345​

[Spring Security] Spring Security OAuth2(密码模式)_OAuth2_04

[Spring Security] Spring Security OAuth2(密码模式)_客户端_05

比对发现,其实Header中​​Authorization​​​字段中填写的就是​​Basic​​​+​​空格​​​+​​Base64(客户端ID:客户端密码)​


  • ​u2​​ 和​​12345​​分别为有权限登录中心认证服务的用户名和密码,用户需要获取资源服务器信息(调用资源获取接口时),会拿着自己的用户名和密码先向中心认证服务获取Token,然后用令牌访问资源服务器的有权限控制的接口。
  • 为了验证资源服务器有对自己的资源做保护,我们先发起一个获取图书信息的请求。

[Spring Security] Spring Security OAuth2(密码模式)_spring_06

  • 然后我们用获取到的Token再尝试发起一次请求
{
"access_token": "61ae35ff-d47d-46d8-ba27-2b7fabd30c50",
"token_type": "bearer",
"refresh_token": "358b62f2-be22-48d5-8bd7-31ff3da1f8cb",
"expires_in": 1199,
"scope": "book_info"
}

[Spring Security] Spring Security OAuth2(密码模式)_客户端_07

其中请求头中需保证​​Authorization​​​的值为​​Bearer​​​+​​空格​​​+​​access_token的值​

  • Token过期后请求

[Spring Security] Spring Security OAuth2(密码模式)_OAuth2_08

核心配置类


中心认证服务器关键配置


/**
* ClientDetailsServiceConfigurer 能够使用内存或 JDBC 方式实现获取已注册的客户端详情,有几个重要的属性:
* clientId:客户端标识 ID
* secret:客户端安全码
* scope:客户端访问范围,默认为空则拥有全部范围
* authorizedGrantTypes:客户端使用的授权类型,默认为空
* authorities:客户端可使用的权限
*
* @param clients
* @throws Exception
*/
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {

clients.inMemory()
.withClient("SampleClientId")
.secret(passwordEncoder.encode("secret"))
.authorizedGrantTypes("authorization_code")
.scopes("user_info")
.autoApprove(true)
.redirectUris("http://localhost:8082/client/login", "http://localhost:8083/client2/login", "http://www.example.com/")

.and()
.withClient("BookResourceClientId")
.secret(passwordEncoder.encode("secret"))
.authorizedGrantTypes("password","refresh_token")
.scopes("book_info")
.resourceIds("book_rest_api")
.accessTokenValiditySeconds(1200)
.refreshTokenValiditySeconds(50000);
}

​关键类​

BasicAuthenticationFilter会获取header中的Authorization Basic,提取出客户端信息。

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();

String header = request.getHeader("Authorization");

if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}

try {
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;

String username = tokens[0];

if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}

if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager
.authenticate(authRequest);

if (debug) {
this.logger.debug("Authentication success: " + authResult);
}

SecurityContextHolder.getContext().setAuthentication(authResult);

this.rememberMeServices.loginSuccess(request, response, authResult);

onSuccessfulAuthentication(request, response, authResult);
}

}
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();

if (debug) {
this.logger.debug("Authentication request for failed: " + failed);
}

this.rememberMeServices.loginFail(request, response);

onUnsuccessfulAuthentication(request, response, failed);

if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}

return;
}

chain.doFilter(request, response);
}

访问​​/oauth/token​​​,先验证了client信息,并作为​​authentication​​​存储在​​SecurityContextHolder​​​中。传递到​​TokenEndPoint​​​的​​principal​​是client,paramters包含了user的信息和grantType。

[Spring Security] Spring Security OAuth2(密码模式)_OAuth2_09


资源服务器关键配置


ResourceSecurityConfig

/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/5/20 20:57
* @email: xiazhaoyang@live.com
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.repo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.annotation.Resource;

/**
* <p>
* 资源服务配置
* ResourceServerConfigurerAdapter用于保护oauth要开放的资源,同时主要作用于client端以及token的认证(Bearer auth)
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019/5/20 20:57
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/5/20
* @modify reason: {方法名}:{原因}
* ...
*/
@EnableResourceServer
@Configuration
public class ResourceSecurityConfig extends ResourceServerConfigurerAdapter {

@Value("${security.oauth2.resource.id}")
public String resourceId;

@Resource
public RemoteTokenServices remoteTokenServices;

/**
* 资源服务器承载资源[REST API],客户端感兴趣的资源位于 /book/ 。
*
* @param http
* @throws Exception
* @EnableResourceServer注释,适用在OAuth2资源服务器, 实现了Spring Security的过滤器验证的请求传入OAuth2令牌。
* ResourceServerConfigurerAdapter类实现 ResourceServerConfigurer 提供的方法来
* 调整 OAuth2安全保护的访问规则和路径。
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.requestMatchers().antMatchers("/book/**")
.and()
.authorizeRequests()
.antMatchers("/book/**").authenticated();
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//如果关闭 stateless,则 accessToken 使用时的 session id 会被记录,后续请求不携带 accessToken 也可以正常响应
resources.resourceId(resourceId).stateless(true).tokenServices(remoteTokenServices);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

BookResourceController

/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/5/26 21:50
* @email: xiazhaoyang@live.com
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.repo;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* <p>
*
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019/5/26 21:50
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019/5/26
* @modify reason: {方法名}:{原因}
* ...
*/
@RestController
public class BookResourceController {

@GetMapping("/book/info")
@PreAuthorize("hasAnyAuthority('USER')")
public String getBookInfoById(String id) {
return String.format("get book info by id:%s", id);
}
}

application.yaml

server:
port: 8084
servlet:
context-path: /book-resources
session:
cookie:
name: BOOKRESOURCE

management:
security:
enabled: false

# 是否开启基本的鉴权,默认为true。 true:所有的接口默认都需要被验证,将导致 拦截器[对于 excludePathPatterns()方法失效]
security:
basic:
enabled: false

oauth2:
client:
client-id: BookResourceClientId
client-secret: secret
access-token-uri: http://localhost:8081/authServer/oauth/token
user-authorization-uri: http://localhost:8081/authServer/oauth/authorize
user-logout-uri: http://localhost:8081/authServer/logout
resource:
id: book_rest_api
preferTokenInfo: true
token-info-uri: http://localhost:8081/authServer/oauth/check_token
filter-order: 3

endpoints:
health:
sensitive: false
enabled: true

spring:
thymeleaf:
cache: false

​关键类​

OAuth2AuthenticationProcessingFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {

final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;

try {
//从request中提取Token
Authentication authentication = tokenExtractor.extract(request);

if (authentication == null) {
if (stateless && isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
// OAuth2AuthenticationManager 验证PreAuthenticatedAuthenticationToken
Authentication authResult = authenticationManager.authenticate(authentication);

if (debug) {
logger.debug("Authentication success: " + authResult);
}

eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);

}
}
catch (OAuth2Exception failed) {
SecurityContextHolder.clearContext();

if (debug) {
logger.debug("Authentication request failed: " + failed);
}
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));

return;
}

chain.doFilter(request, response);
}

BearerTokenExtractor

[Spring Security] Spring Security OAuth2(密码模式)_spring_10

public PreAuthenticatedAuthenticationToken(Object aPrincipal, Object aCredentials) {
super(null);
this.principal = aPrincipal;
this.credentials = aCredentials;
}

​BearerTokenExtractor​​​解析request,​​extractToken​​​方法从header参数​​Authorization​​​ **Bearer [tokenValue]**中抽取token,并返回一个​​principal​​​值为token的​​PreAuthenticatedAuthenticationToken​​对象。

再根据OAuth2AuthenticationManager校验​​authentication​​的合法性。


对于本例中,资源服务器和中心认证服务是分离开的,所以还需进行Token的校验


[Spring Security] Spring Security OAuth2(密码模式)_OAuth2_11

当请求资源服务器的时候,在通过​​OAuth2AuthenticationManager​​​校验完后​​authentication​​​合法性后,还会调用中心认证服务的​​/oauth/check_token​​接口进行token的校验。

Tips: 这些都依赖于资源服务器的yaml文件中配置的路由

security:
basic:
enabled: false

oauth2:
client:
client-id: BookResourceClientId
client-secret: secret
access-token-uri: http://localhost:8081/authServer/oauth/token
user-authorization-uri: http://localhost:8081/authServer/oauth/authorize
user-logout-uri: http://localhost:8081/authServer/logout
resource:
id: book_rest_api
preferTokenInfo: true
token-info-uri: http://localhost:8081/authServer/oauth/check_token
filter-order: 3

总结

本文总结了基于Spring Security 和 OAuth2的密码授权模式的主要流程和关键节点的参数。并提供了将资源服务器和中心认证服务器分开的配置方案。这样做的主要目的是考虑到现实场景中,往往各个模块的职责是单一的,当资源模型较多时,分离部署明显是更好的方式。

REFRENCES


  • OAuth2 源码分析(三.密码模式源码)
  • OAuth2整合redis和mysql
  • Spring Boot 与 OAuth2
  • Spring 官网OAuth2开发指南

更多


扫码关注“架构探险之道”,回复​​文章名称​​获取更多源码和文章资源


[Spring Security] Spring Security OAuth2(密码模式)_spring_12