SpringBoot——安全管理(四)

一、OAuth 2简介

OAuth是一个开放的标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时间段内访问特定的资源。这样,OAuth让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。例如用户想通过QQ登录知乎,这时知乎就是一个第三方应用,知乎要访问用户的一些基本信息就需要得到用户的授权,如果用户把自己的QQ用户名和密码告诉知乎,那么知乎就能访问用户的所有数据,并且只有用户修改密码才能收回授权,这种授权方式安全隐患很大,如果使用OAuth,就能很好地解决这一问题。
采用令牌的方式可以让用户灵活地对第三方应用授权或者收回权限。OAuth2是OAuth协议的下一版本,但不向下兼容OAuth1.0。OAuth2关注客户端开发者的简易性,同时为Web应用、桌面应用、移动设备、起居室设备提供专门的认证流程。传统的Web开发登录认证一般都是基于Session的,但是在前后端分离的架构中继续使用Session会有许多不便,因为移动端(Android、iOS、微信小程序等),要么使用非常不便,对于这些问题,使用OAuth 2认证都能解决。

二、OAuth2 角色

要了解OAuth2,需要先了解OAuth 2中几个基本的角色。

  • 资源所有者: 资源所有者即用户,具有头像、照片、视频等资源。
  • 客户端:客户端即第三方应用,例如上文提到的知乎。
  • 授权服务器:授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。
  • 资源服务器:资源服务器是提供给用户资源的服务器,例如头像、照片、视频等。
    一般来说,授权服务器和资源服务器可以是同一台服务器。

三、OAuth 2授权流程

OAuth2的授权流程到底是什么样的呢?如下图
这是OAuth2一个大致的授权流程图,具体步骤如下:

  1. 客户端(第三方应用)向用户请求授权
  2. 用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端
  3. 客户端拿着授权许可凭证去授权服务器申请令牌
  4. 授权服务器验证信息无误后,发放令牌给客户端
  5. 客户端拿着令牌去资源服务器访问资源
  6. 资源服务器验证令牌无误后开放资源
    这是一个大致的流程,因为OAuth2中有4中不同的授权模式,每种授权模式的授权流程又会有差异,基本流程如图所示:
    在这里插入图片描述

四、授权模式

OAuth协议的授权模式共分为4种,分别说明如下:

  • 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互国内常见的第三方平台登录功能基本都是使用这种模式
  • 简化模式简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌,一般若网站是纯静态页面,则可以采用这种方式。
  • 密码模式密码模式就是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
  • 客户端模式客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作OAuth协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者移动端提供的认证授权服务器上使用这种模式还是非常方便的
    这4种模式各有千秋,分别适用于不同的开发场景,开发者要根据实际情况进行选择。

五、实践

本案例要介绍的是在前后端分离应用(或者移动端、微信小程序等)提供的认证服务器中如何搭建OAuth服务,因此主要介绍密码模式。搭建步骤如下:

  1. 创建项目,添加依赖
    创建Spring Boot Web项目,添加如下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.oauthtest</groupId>
    <artifactId>oauthtest</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
    </dependencies>
</project>

由于SpringBoot中的OAuth协议是在Spring Security的基础上完成的,因此首先要添加Spring Security依赖,要用到OAuth2,因此添加OAuth 2相关依赖,令牌可以存储在Redis缓存服务器上,同时Redis具有过期等功能,很适合令牌的存储,因此也加入Redis依赖。
项目创建成功后,接下来在application.properties中配置一下Redis服务器的连接信息,代码如下:

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0

  1. 配置授权服务器
    授权服务器和资源服务器可以是同一台服务器,也可以不同服务器,本案例中假设是同一台服务器,通过不同的配置分别开启授权服务器和资源服务器,首先是授权服务器:
/**
 * 自定义类继承自AuthorizationServerConfigurerAdapter,完成对授权服务器的配置,
 * 然后通过@EnableAuthorizationServer注解开启授权服务器
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    //注入authenticationManager,该对象将用来支持password模式
   @Autowired
    AuthenticationManager authenticationManager;

    //注入redisConnectionFactory,该对象将用来完成Redis缓存,将令牌信息
    //存储到Redis缓存中
    @Autowired
    RedisConnectionFactory redisConnectionFactory;
    //注入了UserDetailsService,该对象将为刷新token提供支持
    @Autowired
    UserDetailsService userDetailsService;
    @Bean
    PasswordEncoder passwordEncoder() {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        System.out.println("password = " + password);
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置password授权模式,
     * authorizedGrantTypes 表示OAuth 2中的授权模式为"password"
     * 和"refresh_token"两种,在标准OAuth 2协议中,授权模式并不包括"refresh_token"
     * 但是在SpringSecurity的实现中将其归为一种,因此如果要实现access_token的刷新
     * 就需要添加这样一种授权模式;accessTokenValiditySeconds方法配置了access_token
     * 的过期时间;resourceIds配置了资源id;
     * @param clients
     * @throws Exception
     */
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("password")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(1800)//配置了access_token过期时间
                .resourceIds("rid")//配置资源id
                .scopes("all")
                //加密后的密码
              .secret("$2a$10$zozsK7CgtOeinXRdOwiiCOjZsICagZBA1yvJ2PrUiMBpwlUz4zpCa");
              // .secret(new BCryptPasswordEncoder().encode("123"));
               // .secret(passwordEncoder().encode("123"));
      //  将ClientDetailsServiceConfigurer的configure里的.secret("secret")
   // 改为.secret(passwordEncoder().encode("secret"))即可。
    }

    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //配置了令牌存储,authenticationManager 和userDetailsService主要用于
        //支持password模式以及令牌刷新
        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //表示支持client_id和client_secret做登录认证。
        security.allowFormAuthenticationForClients();
    }
}

  1. 配置资源服务器
    接下里配置资源服务器,代码如下:
/**
 * 自定义类继承ResourceServerConfigurerAdapter并添加@EnableResourceServer
 * 注解开启资源服务器配置
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    /**
     * 这里的资源id和授权服务器中的资源id一致,然后设置这些资源仅基于
     * 令牌认证
     * @param resources
     * @throws Exception
     */
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("rid").stateless(true);
    }

    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated();
    }
}

  1. 配置Security
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 重新实例化bean
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }
    @Bean
    protected UserDetailsService userDetailsService() {
        return super.userDetailsService();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("$2a$10$zozsK7CgtOeinXRdOwiiCOjZsICagZBA1yvJ2PrUiMBpwlUz4zpCa")
                .roles("admin")
                .and()
                .withUser("sang")
                .password("$2a$10$zozsK7CgtOeinXRdOwiiCOjZsICagZBA1yvJ2PrUiMBpwlUz4zpCa")
                .roles("user");
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/oauth/**").authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .and()
                .csrf()
                .disable();
    }

}

这里Spring Security的配置基本上和前文一致,唯一不同的是多了两个Bean,这里两个Bean将注入服务器配置类中使用。另外,这里的HttpSecurity配置主要是配置“/oauth/** ”模式的URL,这样一类的请求直接方行。在Spring Security配置和资源服务器配置中,一共涉及两个HttpSecurity,其中Spring Security中的配置优先级高度资源服务器中的位置,即请求地址先经过Spring Security的HttpSecurity,再经过资源服务器的HttpSecurity。
5. 测试验证

@Controller
public class HelloController {
    @GetMapping("/admin/hello")
    @ResponseBody
    public String admin() {
        return "hello admin!";
    }
    @GetMapping("/user/hello")
    @ResponseBody
    public String user() {
        return "hello user!";
    }
    @GetMapping("/db/hello")
    @ResponseBody
    public String dba() {
        return "hello dba!";
    }
}
  1. 实验结果
    根据前文的配置,要请求这三个地址,分别需要admin角色、user角色以及登录后访问的。
    所有配置完成后,启动Redis服务器,再启动Spring Boot项目,首先发送一个POST请求获取token,
    请求地址如下(注意这里是一个POST请求,为了显示方便,将参数写在地址栏中)
    :http://localhost:8080/oauth/token?username=admin&password=123&grant_type=password&client_id=password&scope=all&client_secret=123
    请求地址中包含参数有用户名、密码、授权模式、客户端id、scope以及客户端密码,基本就是授权
    服务器中所配置的数据,请求结果如下:
    在这里插入图片描述
    返回结果有access_token、token_type、refresh_token、expires_in、scope,其中access_token是获取其他资源时要用的令牌,refresh_token用来刷新令牌,expires_in表示access_token的过期时间,当access_token过期后,使用refresh_token重新获取新的access_token(前提是refresh_token未过期),请求地址如下(注意这里是POST请求):
    http://localhost:8080/oauth/token?username=sang&password=123&grant_type=refresh_token&refresh_token=3b975d69-61b3-47d6-b9ba-0b322f4e3dd7&client_id=password&scope=all&client_secret=123
    获取新的access_token时需要携带上refresh_token,同时授权模式设置为refresh_token,在获取结果中access_token会变化,同时access_token有效期也会变化,如下图:
    在这里插入图片描述

访问结果如下:

在这里插入图片描述

  1. 问题
  • authenticationManager无法注入问题 参照如下解决方法
    在配置spring cloud security的过程中出现如下异常信息导致无法启动项目
Field authenticationManager in com.clark.online.edu.config.AuthorizationServerConfig required a bean of type 'org.springframework.security.authentication.AuthenticationManager' that could not be found.
 
 
Action:
 
Consider defining a bean of type 'org.springframework.security.authentication.AuthenticationManager' in your configuration.

看错误信息就很明白了,就是需要authenticationManager,但是没有在config里面注入,但是实际上在我的config配置里面已经注入了,代码如下:

	/**
	 * 认证管理
	 */
	@Autowired
	private AuthenticationManager authenticationManager;
	/**
	 * redis连接工具
	 */
	@Autowired
	private RedisConnectionFactory redisConnectionFactory;
	
	/**
	 * redis存储token
	 * @return
	 */
	@Bean
	RedisTokenStore redisTokenStore() {
		return new RedisTokenStore(redisConnectionFactory);
	}
	
	/**
	 * 配置
	 */
	@Override 
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.tokenStore(redisTokenStore()).authenticationManager(authenticationManager);
	}

其实原因我不知道,解决方法很简单就是重新实例化bean就OK

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	/**
	 * 重新实例化bean
	 */
	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
}
  • 解决升级 Spring Boot 2.6后,因循环引用导致启动时报错 BeanCurrentlyInCreationException 的问题
    在application.properties中添加如下配置:
spring.main.allow-circular-references=true
  • Encoded password does not look like BCrypt 异常处理
    spirng boot 1.5.* 升级到spring boot 2.0.*,当再次访问授权服务器时出现Encoded password does not look like BCrypt异常
    分析:断点跟踪,发现是密码格式不匹配导致的。
    在这里插入图片描述
    在这里插入图片描述
    在系统调用matches的时候,会对密码的格式进行校验。
    解决:修改密码加密的方式。示例:
    原先:
    在这里插入图片描述
    改为:
    在这里插入图片描述
    或者可以采用这种方式生成密码,将生成的密码复制粘贴进行测试使用
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值