Spring Cloud Alibaba (5) - Spring Security oAuth2

1,937 阅读4分钟

代码地址:github.com/toyranger/l…

用户认证和授权

oAuth是一种协议,这种协议不会使第三方触及到用户的账号信息,即第三方无需使用用户的用户名和密码就可以申请获得该用户资源的授权。 Spring Security、Apache Shiro都是实现

1. Spring Security oAuth2令牌的访问和刷新

Access Token

Refresh Token的作用是用来刷新Access Token,认证服务器提供一个刷新接口

http://www.somesite.com/refresh?refresh_token=&client_id=

2. Spring Security oAuth2客户端授权模式

  • implicit 简化模式
  • authorization 授权码模式
  • resource owner password credentials 密码模式
  • client credentials 客户端模式

这个面试时候可能问到,查一下资料,理解一下。

3. 获取授权码和access_token(内存模式,用于测试)

## pom.xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
// 认证服务器
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Bean
  public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser("user").password(passwordEncoder().encode("123456"))
        .roles("USER").and().withUser("admin").password(passwordEncoder().encode("admin"))
        .roles("ADMIN");
  }

}

// 授权服务器
@Configuration
@EnableAuthorizationServer
public class AuthorizationConfiguration extends AuthorizationServerConfigurerAdapter {

  @Autowired
  private BCryptPasswordEncoder passwordEncoder;

  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

    clients.inMemory().withClient("client").secret(passwordEncoder.encode("secret"))
        .authorizedGrantTypes("authorization_code").scopes("app")
        .redirectUris("https://www.baidu.com");
  }
}

4. 获取授权码,获取token

http://localhost:8080/oauth/authorize?client_id=client&response_type=code
请求会重定向到指定的地址,地址最后就是授权码
http://client:secret@localhost:8080/oauth/token

用这个access_token就可以去请求资源服务器

5. 获取授权码和access_token(JDBC模式)

使用官方提供的sql初始化数据库 github.com/spring-proj… 把client/secret等信息填到表中

5.1 application.yml

spring:
  application:
    name: oauth2-resources
  datasource:
    jdbcUrl: jdbc:mysql://192.168.0.106:3306/oauth2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
    driverClassName: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      auto-commit: true
      minimum-idle: 2
      idle-timeout: 60000
      connection-timeout: 30000
      max-lifetime: 1800000
      pool-name: DatebookHikariCP
      maximum-pool-size: 5

5.2 授权服务器的信息从jdbc获取

@Configuration
@EnableAuthorizationServer
public class AuthorizationConfiguration extends AuthorizationServerConfigurerAdapter {

  @Autowired
  private BCryptPasswordEncoder passwordEncoder;

// 这里好像默认的就是HikariDataSource,直接autowired没试过行不行
  @Bean
  @Primary
  @ConfigurationProperties(prefix = "spring.datasource")
  public DataSource dataSource() {
    return DataSourceBuilder.create().build();
  }

  @Bean
  public TokenStore tokenStore() {
    return new JdbcTokenStore(dataSource());
  }

  @Bean
  public ClientDetailsService jdbcClientDetailsService() {
    return new JdbcClientDetailsService(dataSource());
  }

  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

    clients.withClientDetails(jdbcClientDetailsService());
  }

  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.tokenStore(tokenStore());
  }

}

此时调用获取授权码和token接口成功获取token之后,可以看到数据库中已经生成数据了

6. RBAC授权模型

Role-Based Access Control

sql在代码库中,可以使用mybatis-plus自动生成模板代码

6.1 用户认证 -- 根据用户名查询用户,根据用户id查询权限

WebSecurityConfiguration使用userDetailsService认证

 @Bean
  @Override
  public UserDetailsService userDetailsService() {
    return new UserDetailsServiceImpl();
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    auth.userDetailsService(userDetailsService());
  }

UserDetailsService实现类中访问数据库,查询用户和权限

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

  @Autowired
  private UserService userService;

  @Autowired
  private PermissionService permissionService;

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

    User userByUsername = userService.getUserByUsername(s);

    List<GrantedAuthority> grantedAuthorities = Lists.newArrayList();

    if (null != userByUsername) {
      List<Permission> permissions = permissionService.selectByUserId(userByUsername.getId());
      permissions.forEach(permission -> {
        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getEnname());
        grantedAuthorities.add(grantedAuthority);
      });
      return new org.springframework.security.core.userdetails.User(userByUsername.getUsername(),
          userByUsername.getPassword(), grantedAuthorities);
    }

    return null;
  }
<select id="selectByUserId" resultMap="BaseResultMap">
    select p.* from tb_user as u
    left join tb_user_role as ur on u.id = ur.user_id
    left join tb_role as r on r.id = ur.role_id
    left join tb_role_permission as rp on r.id = rp.role_id
    left join tb_permission as p on p.id = rp.permission_id
    where u.id = #{userId};
  </select>

然后 -- 获取授权码 -- 获取token,和之前的流程一样

7. 创建资源服务器

测试一下不同用户的token的权限,用到的表就是tb_permission表

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourcesServerConfiguration extends ResourceServerConfigurerAdapter {

  @Override
  public void configure(HttpSecurity http) throws Exception {

    http.exceptionHandling().and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        .antMatchers("/").hasAnyAuthority("System")
        .antMatchers("/content").hasAuthority("SystemContent");
  }
}

通过配置把资源服务器和认证授权服务器联系在一起:

security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
      access-token-uri: http://localhost:8080/oauth/token
      user-authorization-uri: http://localhost:8080/oauth/authorize
    resource:
      token-info-uri: http://localhost:8080/oauth/check_token

@RestController
public class HelloController {

  /***
   * 测试"/"需要的权限
   * @return
   */
  @GetMapping("/")
  public String hello() {

    return "hello, oauth2";
  }
}

根据资源服务器的配置,/需要System权限,和数据库配置的吻合,所以现在应该可以访问。

获取授权码 -- 获取token -- 访问/

而如果此时我把/的权限System改变一下-> System111,那么,"user"这个用户再访问/,就会没有权限,如图:

0. 感想

当时刚学Spring Security的时候,认证授权和资源服务器都配在一个configuration中,不易理解,这个oauth2就好多了,清晰明白。