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

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

@TOC

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

平台

地址

CSDN

https://blog.csdn.net/sinat_28690417

简书

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-auth: 中心认证服务器

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

OAuth2流程

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

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

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

它的步骤如下

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

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

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

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

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

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

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

其中:

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

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

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

  • u212345分别为有权限登录中心认证服务的用户名和密码,用户需要获取资源服务器信息(调用资源获取接口时),会拿着自己的用户名和密码先向中心认证服务获取Token,然后用令牌访问资源服务器的有权限控制的接口。
  • 为了验证资源服务器有对自己的资源做保护,我们先发起一个获取图书信息的请求。
  • 然后我们用获取到的Token再尝试发起一次请求
{
    "access_token": "61ae35ff-d47d-46d8-ba27-2b7fabd30c50",
    "token_type": "bearer",
    "refresh_token": "358b62f2-be22-48d5-8bd7-31ff3da1f8cb",
    "expires_in": 1199,
    "scope": "book_info"}

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

  • Token过期后请求

核心配置类

中心认证服务器关键配置

/**
     * 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中。传递到TokenEndPointprincipal是client,paramters包含了user的信息和grantType。

资源服务器关键配置

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@Configurationpublic 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: {方法名}:{原因}
 * ...
 */@RestControllerpublic 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

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的校验

当请求资源服务器的时候,在通过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开发指南

原文发布于微信公众号 - 架构探险之道(zacsnz1314)

原文发表时间:2019-06-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券