一步步入门搭建SpringSecurity OAuth2(密码模式)

什么是OAuth2?

开放授权的一个标准,旨在让用户允许第三方应用去访问改用户在某服务器中的特定私有资源,而可以不提供其在某服务器的账号密码给到第三方应用

大概意思就是比如如果我们的系统的资源是受保护的,其他第三方应用想访问这些受保护的资源就要走OAuth2协议,通常是用在一些授权登录的场景,例如在其他网站上使用QQ,微信登录等。

OAuth2中的几个角色

用户:使用第三方应用(或者是我们自己的应用)的用户。

客户端:用户是用的第三方应用。

认证(授权)服务器:负责发放token的服务,以及想要访问受保护资源的客户端都需要向认证服务器注册信息。

资源服务器:受保护的API资源。

 可以通过一张图来说明这4个角色之间的关系:

 使用SpringSecurity OAuth2进行搭建认证服务器以及资源服务器

1.搭建认证服务器

现在的很多项目都是微服务架构的项目了,对于每一个提供API资源的微服务来说就是一个个的资源服务器,而认证服务器就是客户端访问每一个微服务的时候需要先去认证服务器中拿到token才能去访问。

每个微服务添加依赖:

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

其中一个服务作为认证服务器,设置关于认证的配置:

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 配置authenticationManager用于认证的过程
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager);
    }

    /**
     * 重写此方法用于声明认证服务器能认证的客户端信息
     * 相当于在认证服务器中注册哪些客户端(包括资源服务器)能访问
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("orderApp")  //声明访问认证服务器的客户端
                .secret(passwordEncoder.encode("123456"))  //客户端访问认证服务器需要带上的密码
                .scopes("read","write")  //获取token包含的哪些权限
                .accessTokenValiditySeconds(3600)  //token过期时间
                .resourceIds("order-service")  //指明请求的资源服务器
                .authorizedGrantTypes("password")  //密码模式
                .and()
                //资源服务器拿到了客户端请求过来的token之后会请求认证服务器去判断此token是否正确或者过期
                //所以此时的资源服务器对于认证服务器来说也充当了客户端的角色
                .withClient("order-service")
                .secret(passwordEncoder.encode("123456"))
                .scopes("read")
                .accessTokenValiditySeconds(3600)
                .resourceIds("order-service")
                .authorizedGrantTypes("password");
    }


    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("isAuthenticated()");
    }
}

首先加上@EnableAuthorizationServer注解声明这是一个认证服务器,然后在第二个重写的configure方法中去设置客户端的信息,相当于只有注册进来的客户端才能有权限访问认证服务器,客户端带着这些信息以及用户账号密码等信息向认证服务器发起请求,由于我们这里是密码模式(密码模式一般都是用在保护自己API安全的场景),当然要验证username和password了,而这些验证的过程就需要AuthenticationManager了(第一个重写的configure方法),而AuthenticationManager从哪里来的我们也是要设置的,新建一个配置类,该配置类是与SpringSecurity验证用户信息有关的:

@Configuration
@EnableWebSecurity
public class OAuth2AuthWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
        .passwordEncoder(passwordEncoder);
    }

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

}

UserDetailServiceImpl:

@Component
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    //认证的过程,由AuthenticationManager去调,从数据库中查找用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return User.withUsername(username)
                .password(passwordEncoder.encode("123456"))
                .authorities("ROLE_ADMIN")
                .build();
    }
}

启动应用,用postman访问http://localhost:9000/auth/token

username和password就是我们登陆的用户名和密码,如果和数据库中保存的用户名密码不一致就会认证不成功(UserDetailService里从数据库中根据传过来的username查找对应的密码与传过来的密码进行比对),grant_type为password,表明当前是密码模式,scope是申请的权限。

得到的响应,其中access_token就是我们认证成功后从认证服务器返回的token,expires_in就是过期剩余时间。每次请求资源服务器都要带上token,然后资源服务器再去请求认证服务器验证token是否正确,即在这个过程中,资源服务器的资源能否访问就取决于资源服务器信任的是哪个认证服务器。

1.2 把客户端信息与token保存在数据库中

此时认证服务器就完成了,不过上面我们客户端的信息和token是保存在内存中的,这明显不太适合在真实的使用中,我们可以将其保存在数据库中。

在数据库中创建对应的表:

create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

把客户端的信息添加到oauth_client_details这个表中:

添加数据库依赖:

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

修改认证服务器配置:

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private DataSource dataSource;

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

    /**
     * 配置authenticationManager用于认证的过程
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                //设置tokenStore,生成token时会向数据库中保存
                .tokenStore(tokenStore())
                .authenticationManager(authenticationManager);
    }

    /**
     * 重写此方法用于声明认证服务器能认证的客户端信息
     * 相当于在认证服务器中注册哪些客户端(包括资源服务器)能访问
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }


    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("isAuthenticated()");
    }
}

此时再启动认证服务器的话当客户端发起令牌请求的时候就会把生成的令牌存储到数据库中,当认证服务器重启的时候同一个客户端再次请求就会把数据库中的token拿出来判断token是否已经超时,如果没有就直接返回给客户端,否则就重新生成token并且更新数据库中旧的token。

2.搭建资源服务器

新建一个maven项目,添加上OAuth2的依赖,加上关于资源服务器的配置:

@Configuration
@EnableResourceServer  //声明该服务是一个资源服务器
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //声明该资源服务器的id,当请求过来是会首先判断token是否有访问该资源服务器的权限
        resources.resourceId("order-service");
    }

    /**
     * 设置访问权限需要重写该方法
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //访问post请求的接口必须要有write权限
                .antMatchers(HttpMethod.POST).access("#oauth2.hasScope('write')")
                //访问get请求的接口必须要有read权限
                .antMatchers(HttpMethod.GET).access("#oauth2.hasScope('read')");
    }
}
@Configuration
//资源服务器拿到token之后需要向认证服务器发出请求判断该token是否正确,开启SpringSecurity认证的过程
@EnableWebSecurity
public class OAuth2ResourceWebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public ResourceServerTokenServices tokenServices() {
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        //认证时资源服务器就相当于客户端,需要向认证服务器声明自己的信息是否匹配
        remoteTokenServices.setClientId("order-service");
        remoteTokenServices.setClientSecret("123456");
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9000/oauth/check_token");
        return remoteTokenServices;
    }


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
        oAuth2AuthenticationManager.setTokenServices(tokenServices());
        return oAuth2AuthenticationManager;
    }
}

2.1 获取认证之后用户的信息

我们在资源服务器中的一些接口中可能会使用到认证用户的信息,而SpringSecurity OAuth2中也提供了相应的方法来获取,我们可以在接口参数中加上@AuthenticationPricipal注解,例如:

@PostMapping("/create")
    public OrderInfo order(@RequestBody OrderInfo orderInfo, @AuthenticationPrincipal String username){
        PriceInfo priceInfo = restTemplate.getForObject("http://localhost:9002/price/get", PriceInfo.class);
        orderInfo.setPrice(priceInfo.getPrice());
        System.out.println("order() username is ===================" + username);
        return orderInfo;
    }

因为token中包含username,默认是只能获取username的,而如果我们想要获取整个user对象的信息的话,则可能利用UserDetailService去数据库中根据username查询,修改下配置类:

@Configuration
//资源服务器拿到token之后需要向认证服务器发出请求判断该token是否正确,开启SpringSecurity认证的过程
@EnableWebSecurity
public class OAuth2ResourceWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public ResourceServerTokenServices tokenServices() {
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        //认证时资源服务器就相当于客户端,需要向认证服务器声明自己的信息是否匹配
        remoteTokenServices.setClientId("order-service");
        remoteTokenServices.setClientSecret("123456");
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9000/oauth/check_token");
        remoteTokenServices.setAccessTokenConverter(getAccessTokenConverter());
        return remoteTokenServices;
    }

    //设置了AccessTokenConverter就能在认证之后通过token里面的username去调用UserDetailsService去数据库查找出完整的用户信息了
    private AccessTokenConverter getAccessTokenConverter() {
        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
        userTokenConverter.setUserDetailsService(userDetailsService);
        accessTokenConverter.setUserTokenConverter(userTokenConverter);
        return accessTokenConverter;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
        oAuth2AuthenticationManager.setTokenServices(tokenServices());
        return oAuth2AuthenticationManager;
    }
}

添加UserDetailService之后,资源服务器在向认证服务器发送令牌认证成功之后会调用里面的loadUserByUsername方法并且返回user对象,接口方法参数此时就能获取到user对象了:

@PostMapping("/create")
    public OrderInfo order(@RequestBody OrderInfo orderInfo, @AuthenticationPrincipal User user){
        PriceInfo priceInfo = restTemplate.getForObject("http://localhost:9002/price/get", PriceInfo.class);
        orderInfo.setPrice(priceInfo.getPrice());
        System.out.println("order() user is ===================" + user);
        return orderInfo;
    }

如果我们想要这个对象中的某个属性也是可以的,在注解上使用el表达式即可:

@PostMapping("/create")
    public OrderInfo order(@RequestBody OrderInfo orderInfo, @AuthenticationPrincipal(expression = "#this.id") Long id){
        PriceInfo priceInfo = restTemplate.getForObject("http://localhost:9002/price/get", PriceInfo.class);
        orderInfo.setPrice(priceInfo.getPrice());
        System.out.println("order() useId is ===================" + id);
        return orderInfo;
    }

自此,一个简单的利用SpringSecurity OAuth2搭建的认证服务器和资源服务器就完成了。

猜你喜欢

转载自blog.csdn.net/weixin_37689658/article/details/104149499