前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[Spring Security] Spring Security OAuth2(密码模式)

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

作者头像
架构探险之道
发布2019-07-25 17:50:08
4.2K0
发布2019-07-25 17:50:08
举报
文章被收录于专栏:架构探险之道

[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再尝试发起一次请求
代码语言:javascript
复制
{
    "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过期后请求

核心配置类

中心认证服务器关键配置

代码语言:javascript
复制
/**
     * 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,提取出客户端信息。

代码语言:javascript
复制
@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

代码语言:javascript
复制
/*
 * @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

代码语言:javascript
复制
/*
 * @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

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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文件中配置的路由

代码语言:javascript
复制
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开发指南

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构探险之道 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • [Spring Security] Spring Security OAuth2(密码模式)
    • 简介
      • 名词定义
        • 准备工作
          • OAuth2流程
            • 核心配置类
              • 总结
                • REFRENCES
                相关产品与服务
                访问管理
                访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档