前言:

        OAuth 2.0 是当前授权领域比较流行的标准,它可以为Web应用程序、桌面应用程序、移动设备等提供简单的客户端开发方式。在SpringCloud中提供了SpringCloud Security组件来构建安全的应用程序。SpringCloud Security主要封装了Spring Security、SpringCloud Security OAuth2 等相关实现,这篇文章我们就来介绍一下如何集成OAuth2.0。

正文:

零. OAuth2.0基础知识:

1. 角色:

  • Resource Owner(资源所有者):是能够对受保护的资源授予访问权限的实体,可以是一个用户,这时会称为终端用户。
  • Resource Server(资源服务器):持有受保护的资源,允许持有访问令牌的请求访问受保护资源。
  • Client(客户端):持有资源所有者的授权,代表资源所有者对受保护资源进行访问。
  • Authorization Server(授权服务器):对资源所有者的授权进行认证,成功后向客户端发送访问令牌。

2. 授权流程:

springcloud 集成 oauth2 oauth2.0 springcloud_ide

3. 客户端授权类型:

Oauth2.0默认定义了四种授权类型,当然也提供了用于定义额外授权类型的扩展机制。默认的四种授权类型为:

  • Authorization Code:授权码类型。
  • Implicit :简化类型,也称为隐式类型。
  • Resource Owner Password Credentials:密码类型。
  • Client Credential:客户端类型。

一、版本信息:

SpringCloud  Greenwich版本、Springboot  2.1.4.RELEASE版本、OAuth2.0  2.1.2版本

二、授权服务器配置:

1.引入依赖架包:

<!--oauth2-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

2.Springboot启动类上添加@EnableAuthorizationServer注解: 

springcloud 集成 oauth2 oauth2.0 springcloud_spring_02

3.配置授权服务器的Oauth2ServiceConfig类:

package com.hanxiaozhang.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

/**
 * 功能描述: <br>
 * 〈配置授权服务器〉
 *
 * @Author:hanxiaozhang
 * @Date: 2020/7/27
 */
@Configuration
public class Oauth2ServiceConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

    /**
     * 配置客户端详情服务
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                // 客户端id
                .withClient("hanxiaozhang")
                // 允许请求范围
                .scopes("hanxiaozhang")
                // 客户端可以使用的授权范围
                .authorizedGrantTypes("password", "authorization_code", "refresh_token")
                // 客户端安全码
                .secret(new BCryptPasswordEncoder().encode("hanxiaozhang"));

    }

    /**
     * 配置Endpoints相关信息:AuthorizationServer、TokenServices、TokenStore、
     * ClientDetailsService、UserDetailsService
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.authenticationManager(authenticationManager)
                .tokenStore(jdbcTokenStore())
                .tokenServices(tokenServices());
    }

    @Primary
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        // access_token:72个小时
        tokenServices.setAccessTokenValiditySeconds(60 * 60 * 72);
        // refresh_token:72个小时
        tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 12);
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(false);
        tokenServices.setTokenStore(jdbcTokenStore());
        return tokenServices;
    }

    @Bean
    JdbcTokenStore jdbcTokenStore() {
        return new JdbcTokenStore(dataSource);
    }


    /**
     * AuthorizationServerSecurityConfigurer继承SecurityConfigurerAdapter,是一个Spring Security安全配置
     * 提供给AuthorizationServer去配置AuthorizationServer的端点(/oauth/****)的安全访问规则、过滤器Filter
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 获取token的请求不在拦截
                .tokenKeyAccess("permitAll()")
                // 验证通过返回token信息
                .checkTokenAccess("isAuthenticated()")
                //  运行客户端使用client_id和client_secret获取token
                .allowFormAuthenticationForClients();
    }


    public static void main(String[] args) {
        System.out.println("加密: " + new BCryptPasswordEncoder().encode("hanxiaozhang"));
        System.out.println("比对: " + new BCryptPasswordEncoder().matches("123456", "$2a$10$N5WQVfjpdj.v00dgkBW9cu48iKvke8fu1IsuaKRj5rOc/olmrEetW"));
        System.out.println("比对: " + new BCryptPasswordEncoder().matches("123456", "$2a$10$Vp1CxPTT/QBmU88jZYzkXOYgIq04Kvfd.o.YYqFn0y6rC5hgO/Yqe"));

    }

}

 4.配置Spring Security的SecurityConfig类:

package com.hanxiaozhang.config;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;


/**
 * 功能描述: <br>
 * 〈配置Spring Security〉
 *
 * @Author:hanxiaozhang
 * @Date: 2020/7/27
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetail;

    /**
     * 配置认证管理器
     *
     * @return
     * @throws Exception
     */
    @Override
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭跨域保护
                .csrf().disable()
                // 配置HttpSecurity接收的请求
                .requestMatchers()
                .antMatchers("/login","/oauth/**")
                .and()
                // 配置URL的权限
                .authorizeRequests()
                // 访问/login,/oauth/**不要权限验证,这个配置没有卵用吧,应该配置资源服务的
                .antMatchers("/login","/oauth/**").permitAll()
                // 其他所有路径都需要权限校验
                .anyRequest().authenticated()
                .and()
                // [解决访问/oauth/authorize/**返回403的问题]
                .httpBasic();

    }


    /**
     * 配置用户
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetail)
                .passwordEncoder(passwordEncoder());
    }


    /**
     * 配置密码解密器
     *
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

  5.重写实现UserDetailsService类:

package com.hanxiaozhang.system.service.impl;

import com.hanxiaozhang.exception.UserNotLoginException;
import com.hanxiaozhang.security.CurrentUser;
import com.hanxiaozhang.system.dao.MenuDao;
import com.hanxiaozhang.system.dao.UserDao;
import com.hanxiaozhang.system.entity.UserEntity;
import com.hanxiaozhang.util.StringUtil;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 功能描述: <br>
 * 〈〉
 *
 * @Author:hanxiaozhang
 * @Date: 2020/7/27
 */
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private UserDao userDao;

    @Resource
    private MenuDao menuDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserEntity user = userDao.getByUsername(username);
        if (user == null) {
            throw new UserNotLoginException("用户名不存在!");
        }
        List<String> permission = menuDao.listPermsByUserId(user.getUserId());
        // 权限
        Set<GrantedAuthority> authorities = permission
                .stream().filter(StringUtil::isNotBlank).map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
        return new CurrentUser(username, user.getPassword(), user.getUserId(), user.getName(),authorities);
    }

}

6.获取当前用户工具类CurrentUserUtil:

package com.hanxiaozhang.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;

import java.util.LinkedHashMap;

/**
 * 功能描述: <br>
 * 〈权限工具类〉
 *
 * @Author:hanxiaozhang
 * @Date: 2021/3/24
 */
public class CurrentUserUtil {


    /**
     * 获取Principal
     *
     * @return
     */
    private static Object getPrincipal() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication instanceof OAuth2Authentication) {
            Object principal = ((OAuth2Authentication) authentication).getUserAuthentication().getPrincipal();
            if (principal instanceof CurrentUser) {
                return principal;
            }
            Object details = ((OAuth2Authentication) authentication).getUserAuthentication().getDetails();
            if (details instanceof LinkedHashMap && details != null) {
                principal = ((LinkedHashMap) details).get("principal");
                if (principal instanceof LinkedHashMap && principal != null) {
                    return principal;
                }
            }
        }
        return null;
    }


    /**
     * 获取当前用户id
     *
     * @return
     */
    public static Long getUserId() {

        if (getPrincipal() instanceof CurrentUser) {
            return ((CurrentUser) getPrincipal()).getUserId();
        }
        return getPrincipal() == null ? null : ((LinkedHashMap) getPrincipal()).get("userId") == null ? null : ((Integer) ((LinkedHashMap) getPrincipal()).get("userId")).longValue();
    }

    /**
     * 获取当前用户名
     *
     * @return
     */
    public static String getName() {
        if (getPrincipal() instanceof CurrentUser) {
            return ((CurrentUser) getPrincipal()).getName();
        }
        return getPrincipal() == null ? null : ((LinkedHashMap) getPrincipal()).get("name") == null ? null : (String) ((LinkedHashMap) getPrincipal()).get("name");
    }

    /**
     * 获取当前用户名称
     *
     * @return
     */
    public static String getUserName() {

        if (getPrincipal() instanceof CurrentUser) {
            return ((CurrentUser) getPrincipal()).getUsername();
        }
        return getPrincipal() == null ? null : ((LinkedHashMap) getPrincipal()).get("username") == null ? null : (String) ((LinkedHashMap) getPrincipal()).get("username");
    }

    public static void main(String[] args) {
        Object object = null;
        String str = null;

        System.out.printf((String) str);
        System.out.printf((String) object);
        System.out.printf(String.valueOf(object));

    }

}

三、资源服务器配置:

1.引入依赖架包:

<!--oauth2-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

2.资源服务器配置类ResourceServerConfig:

package com.hanxiaozhang.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.web.context.request.RequestContextListener;

import javax.servlet.http.HttpServletResponse;


/**
 * 功能描述: <br>
 * 〈资源服务器配置类〉
 *
 * @Author:hanxiaozhang
 * @Date: 2020/7/31
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // 配置异常处理器
                .antMatcher("/**")
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                // 所有请求都需要授权,除去/actuator/**
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }

    /**
     *  解决如下问题:
     *  Could not fetch user details: class org.springframework.beans.factory.BeanCreationException,
     *  Error creating bean with name 'scopedTarget.oauth2ClientContext':
     *  Scope 'request' is not active for the current thread;
     *  
     *
     * @return
     */
    @Bean
    @Order(0)
    public RequestContextListener requestContextListener() {
        return new RequestContextListener();
    }

}

 3.如果资源服务器与授权服务器不在一个服务,需要配置YAML文件:

security:
  oauth2:
    client:
      access-token-uri: http://localhost:9091/oauth/token
      user-authorization-uri: http://localhost:9091/oauth/authorize
      client-id: hanxiaozhang
      client-secret: $2a$10$pjIVEPMujuzQ1m1CWKkZneHhvQUAW1Y/DooXkbl6gApGakwQSy6..
      clientAuthenticationScheme: form
    resource:
      user-info-uri: http://localhost:9091/token/user  # 指定user info的URI
      prefer-token-info: true   # 是否使用token info,默认为true
    authorization:
      check-token-access: http://localhost:9091/oauth/check_token

四、使用:

1.模拟登陆,获取access_token:

http://localhost:9091/oauth/token?username=admin&password=123456&grant_type=password&scope=hanxiaozhang&client_id=hanxiaozhang&client_secret=hanxiaozhang

springcloud 集成 oauth2 oauth2.0 springcloud_java_03

2. 使用access_token获取资源,把access_token放到header中:

http://localhost:9091/currentUser

springcloud 集成 oauth2 oauth2.0 springcloud_spring_04

五、遇到问题:

1.springboot 2.x版本自己集成了spring security,但是在部分微服务中需要去除它,解决方法:

在@SpringBootApplication注解中移除SecurityAutoConfiguration.class和ManagementWebSecurityAutoConfiguration.class

springcloud 集成 oauth2 oauth2.0 springcloud_java_05