SpringBoot(七)—— 整合Oauth2

Oauth认证模式:

  • 授权码模式
//A 网站提供一个链接,用户点击后就会跳转到 B 网站
https://b.com/oauth/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read
//用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码
https://a.com/callback?code=AUTHORIZATION_CODE
//A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌
https://b.com/oauth/token?
 client_id=CLIENT_ID&
 client_secret=CLIENT_SECRET&
 grant_type=authorization_code&
 code=AUTHORIZATION_CODE&
 redirect_uri=CALLBACK_URL
//B 网站收到请求以后,就会以Json形式返回令牌
{    
  "access_token":"ACCESS_TOKEN",
  "token_type":"bearer",
  "expires_in":2592000,
  "refresh_token":"REFRESH_TOKEN",
  "scope":"read",
  "uid":100101,
  "info":{...}
}

 

最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成

  • 隐式授权模式/简化模式
//A 网站提供一个链接,要求用户跳转到 B 网站
https://b.com/oauth/authorize?
  response_type=token&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read
//用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。
https://a.com/callback#token=ACCESS_TOKEN

这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短 

  • 密码模式(A网页要求用户提供用户名和密码,拿到以后直接向后台请求令牌,后台验证身份通过后,直接返回令牌,这种方式需要用户给出自己的用户名/密码,显然风险很大)
https://oauth.b.com/token?
  grant_type=password&
  username=USERNAME&
  password=PASSWORD&
  client_id=CLIENT_ID
  • 客户端凭证模式(A 应用在命令行向 后台发出请求,后台验证通过以后,直接返回令牌,适用于没有前端的命令行应用
https://oauth.b.com/token?
  grant_type=client_credentials&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

Spring Security知识

只要添加了如下的依赖,当我们访问任一接口时都会需要登录

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

 

使用配置类指定用户名和密码

@Configuration
public class MyTestSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("test1").password("123").roles("admin1")
                .and()
                .withUser("test2").password("123").roles("admin2");
    }
}

本文采用的是第三种的密码模式,用于前后端分离项目

1.引入pom依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>        

        <!--整合oauth需要的依赖,redis用于存储token-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>

2.添加配置信息

spring: 
  redis:
    database: 0 #使用的redis库的编号
    host: 127.0.0.1
    port: 6379
#    password: 123456 
    jedis:
      pool:
        max-active: 8 #连接池最大连接数
        max-idle: 8 #连接池最大空闲数
        max-wait: -1ms #连接池最大阻塞等待时间
        min-idle: 0 #连接池最小空闲数

3.添加配置类

配置授权服务器

AuthorizationServerConfig

@Configuration
@EnableAuthorizationServer //开启授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    AuthenticationManager authenticationManager;//用于支持password模式

    @Autowired
    RedisConnectionFactory redisConnectionFactory;//将令牌信息存储到redis缓存

    @Autowired
    UserDetailsService userDetailsService;//刷新token

    @Bean
    PasswordEncoder passwordEncoder(){
        // 默认密钥的迭代次数为10,也可以指定具体的次数
        // 用户名密码从前端传过来后,通过BCryptPasswordEncoder实例中的encode方法对密码进行加密处理
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置客户端用于password认证
        clients.inMemory().withClient("password")//客户端的名称,这里随便写
                .authorizedGrantTypes("password","refresh_token")//指定授权模式为password模式并使用refresh_token刷新token
                .accessTokenValiditySeconds(1800)//token的有效时间
                .resourceIds("rid")
                .scopes("all")//授权范围
                .secret("$2a$10$1Sh2CCF8I/XoKBLn8xTWxuOoY8jx4Gjttxs/afactggQnUftM.yca");//加密后的密码
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))//保存token到redis
                .authenticationManager(authenticationManager)//启用oauth2管理
                .userDetailsService(userDetailsService);//用户管理
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
    }
}

配置资源服务器

ResourceServerConfig

@Configuration
@EnableResourceServer //开启资源服务器
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("rid").stateless(true); //资源id和授权服务器中资源id保持一致并设置资源仅基于令牌认证
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //表示用户访问'/admin/**'模式的URL时必须具备admin的角色
        //表示用户访问'/user/**'模式的URL时必须具备user的角色
        http.authorizeRequests().antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated();//配置所有的请求必须认证后才可以访问
    }

}

配置Security

WebSecurityConfig

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        return super.userDetailsService();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("$2a$10$1Sh2CCF8I/XoKBLn8xTWxuOoY8jx4Gjttxs/afactggQnUftM.yca")
                .roles("admin")
                .and()
                .withUser("sang")
                .password("$2a$10$1Sh2CCF8I/XoKBLn8xTWxuOoY8jx4Gjttxs/afactggQnUftM.yca")
                .roles("user");
    }

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

添加测试接口

@RestController
public class HelloController {

    @GetMapping("/admin/hello")
    public String admin(){
        return "Hello admin!";
    }

    @GetMapping("/user/hello")
    public String user(){
        return "Hello user!";
    }

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

}

测试

发送一个Post请求获取token:http://localhost:8088/oauth/token?username=admin&password=123&grant_type=password&client_id=password&scope=all&client_secret=123

需要注意的是如果token没有过期,每次请求得到的token都是一样的

发送一个Post请求刷新Token:

http://localhost:8088/oauth/token?grant_type=refresh_token&refresh_token=0323c476-dc96-4ef8-a158-f8d2339209c0&client_id=password&client_secret=123

如果token过期了我们需要刷新token(前提是refresh_token没有过期)

 

然后我们使用token来访问测试接口

 

前面我获取token时是使用username=admin获取的,如果我访问/user/hello则会被禁止访问

因为我在配置类中规定了不同的访问路径只能被指定的用户访问 

我们可以在redis中看到刚才保存的token信息 

但是我们发现这样的话传参未免也太多了吧,这不方便我们使用,所以我们把一些固定的值在后台写死就可以了

猜你喜欢

转载自blog.csdn.net/hzkcsdnmm/article/details/111166966