SpringCloud微服务安全(三)网关安全 3-5 重构代码以使用真实环境

1. 通过OAuth2 Toke的Scope参数控制权限

1.1 在服务端认证服务器里,通过配置客户端的Scope,可以控制给这个客户端生成的token有哪些权限

1.2 在客户端,申请令牌的时候,可以指定scope

示例:在资源服务器 (this-order-api)里,控制post请求的token ,其scope必须包含write权限,get请求的token必须包含read权限。

用postman客户端(clientId=orderApp)去认证服务器申请一个scpoe=read的token,去调用资源服务器里的Post请求:

2. 将token转换为用户信息

目前在资源服务器里,想要获取用户信息,在Controller里,可以通过 @AuthenticationPrincipal 注解,获取生成token的用户名。但是获取不到用户的其他信息,如userId等。

主要方式为:

  1. 在资源服务器的安全配置: OAuth2WebSecurityConfig 里,的  tokenServices方法里,配置一个 AccessTokenConverter,用来将token信息转换为 User 信息
  2. 新建UserDetailsService的实现类

2.1 添加注解后的接口

    @PostMapping
    public OrderInfo create(@RequestBody OrderInfo info,
                            @AuthenticationPrincipal User user){

        log.info("user is {}" ,user.getId());
        OrderInfo orderInfo = new OrderInfo();

        return orderInfo;
    }

2.2 OAuth2WebSecurityConfig.java

/**
 * @ClassName OAuth2WebSecurityConfig
 * @Description TODO token验证
 * @Author wushaopei
 * @Date 2021/5/3 13:38
 * @Version 1.0
 */
/**
 * 怎么验发往本服务的请求头的令牌
 * 1,自定义tokenServices ,说明去哪里去验token
 * 2,重写authenticationManagerBean()方法,将AuthenticationManager暴露为一个Bean
 *    要认证跟用户相关的信息,一般用 AuthenticationManager
 *
 * 这样配置了后,所有发往this-order-api的请求,
 * 需要验token的时候就会发请求去http://localhost:9090/oauth/check_token验token,获取到token对应的用户信息
 */
@Configuration
@EnableWebSecurity
public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 要认证跟用户相关的信息,一般用 AuthenticationManager
     * 覆盖这个方法,可以将AuthenticationManager暴露为一个Bean
     * @return
     * @throws Exception
     */
    @Bean
    public ResourceServerTokenServices tokenServices(){
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId("orderService");
        tokenServices.setClientSecret("123456");
        tokenServices.setCheckTokenEndpointUrl("http://localhost:9090/oauth/check_token");
        tokenServices.setAccessTokenConverter(getAccessTokenConverter());
        return tokenServices;
    }

    //转换器,将token转换为用户信息
    private AccessTokenConverter getAccessTokenConverter() {
        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        //这个类的目的是设UserDetailsService,来将token转换为用户信息,不设默认为空
        DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
        userTokenConverter.setUserDetailsService(userDetailsService);
        accessTokenConverter.setUserTokenConverter(userTokenConverter);
        return accessTokenConverter;
    }

    /**
     * 要认证跟用户相关的信息,一般用 AuthenticationManager
     * 覆盖这个方法,可以将AuthenticationManager暴露为一个Bean
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        OAuth2AuthenticationManager auth2AuthenticationManager = new OAuth2AuthenticationManager();
        auth2AuthenticationManager.setTokenServices(tokenServices());
        return auth2AuthenticationManager;
    }
}
UserDetailsServiceImpl.java
/**
 * @ClassName UserDetailsServiceImpl
 * @Description TODO
 * @Author wushaopei
 * @Date 2021/5/3 15:26
 * @Version 1.0
 */

@Component("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //这里就不去读数据库了
        User user = new User();
        user.setId(1L);
        user.setUsername(username);
        return user;
    }
}

User对象,实现UserDetails接口:

/**
 * @ClassName User
 * @Description TODO
 * @Author wushaopei
 * @Date 2021/5/3 15:22
 * @Version 1.0
 */
@Data
public class User implements UserDetails {

    private Long id;
    private String username;
    private String password;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN");
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;  //账号没过期
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;//账号没被锁定
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;//密码没过期
    }
    @Override
    public boolean isEnabled() {
        return true;//是否可用
    }
    @Override
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    @Override
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}

然后在订单Controller里,就可以取到用户的 id等其他属性了:

用 @AuthenticationPrincipal User user 注解可以取出User对象。

用 @AuthenticationPrincipal(expression = "#this.id") Long id  可以取出User里面的属性

2.3 测试结果:

3. token 持久化到数据库

3.1 说明

客户端应用 持久化到数据库

 之前的章节里,客户端信息是在配置在代码里的,是存在内存里的,这样新增或删除一个客户端应用,都要改代码,然后还要重启认证服务器。

token 持久化到数据库

  之前的章节里,token信息都是存在内存里的,这样的话重启服务器后,token就没了。而且如果认证服务器是集群的话,发令牌的是A机器,验令牌的可能是B机器,这样也是不行的,需要将token持久化到数据库或者redis。 

 Spring默认提供了OAuth2相关的表,建表语句如下( mysql ):

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_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id 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)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

create table oauth_code (
  code VARCHAR(256), authentication BLOB
);

create table oauth_approvals (
    userId VARCHAR(256),
    clientId VARCHAR(256),
    scope VARCHAR(256),
    status VARCHAR(10),
    expiresAt DATETIME,
    lastModifiedAt DATETIME
);

将以上脚本执行到MYSQL。

3.2 代码重构

(1)由于要连数据,所以保证认证服务器配置了jdbc相关配置,我之前已配置过,用的是mybatis-plus,这里就不再赘述。

(2)将客户端信息保存到表里

之前是在代码里配置:

(3)将客户端配置在数据库里:

(4)配置TokenStore,将token信息持久化

 配置TokenStore

   @Autowired
    private DataSource dataSource;

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

告诉服务器,存取token的时候,去自定义的tokenStore里去存取token

  /**
     * @Description TODO 配置用戶信息
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 传给他一个authenticationManager用来校验传过来的用户信息是不是合法的,注进来一个,自己实现
//        endpoints.authenticationManager(authenticationManager);  原来的代码
        endpoints.tokenStore(tokenStore())
                .authenticationManager(authenticationManager);
    }

(5)完整代码
 

package com.imooc.security.server.auth;

import jdk.nashorn.internal.ir.annotations.Reference;
import org.checkerframework.checker.units.qual.A;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

/**
 * @ClassName OAuth2AuthServerConfig
 * @Description TODO 认证服务器
 * @Author wushaopei
 * @Date 2021/5/2 21:44
 * @Version 1.0
 */
@Configuration  //这是一个配置类
@EnableAuthorizationServer  // 当前应用是一个认证服务器
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter  {
    //Spring 对密码加密的封装,自己配置下
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

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

    public static void main(String[] args) {
        System.out.println(new BCryptPasswordEncoder().encode("123456"));
    }

    /***
     * @Description 配置客户端应用的信息,让认证服务器知道有哪些客户端应用来申请令牌。
     * @param clients 客户端的详情服务的配置
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
//        clients.inMemory()  //添加客户端应用,配置在内存里,后面修改为数据库里
//                .withClient("orderApp")// 指定client的id,应用的用户名,这里添加的是客户端应用
//                .secret(passwordEncoder.encode("123456")) // 应用的密码
//                .scopes("read", "write") // 应用的权限
//                .accessTokenValiditySeconds(3600) // 令牌的有效期,单位为秒s
//                .resourceIds("order-server")  // 资源服务器的id。指:我发给orderApp的token可以访问哪些资源服务器
//                .authorizedGrantTypes("password") // 授权方式,指可以用哪种方式去实现
//                .and()
//                .withClient("orderService")// 指定client的id,应用的用户名,这里添加的是订单服务。微服务中,订单服务应该具备访问其他服务的权利,同样需要获取令牌
//                .secret(passwordEncoder.encode("123456")) // 应用的密码
//                .scopes("read") // 应用的权限
//                .accessTokenValiditySeconds(3600) // 令牌的有效期,单位为秒s
//                .resourceIds("order-server")  // 资源服务器的id。指:我发给orderApp的token可以访问哪些资源服务器
//                .authorizedGrantTypes("password") // 授权方式,指可以用哪种方式去实现
//                .and()
//                .withClient("gateway")// 指定client的id,应用的用户名,这里添加的是订单服务。微服务中,订单服务应该具备访问其他服务的权利,同样需要获取令牌
//                .secret(passwordEncoder.encode("123456")) // 应用的密码
//                .scopes("read", "write") // 应用的权限
//                .accessTokenValiditySeconds(3600) // 令牌的有效期,单位为秒s
//                .resourceIds("order-server")  // 资源服务器的id。指:我发给orderApp的token可以访问哪些资源服务器
//                .authorizedGrantTypes("password"); // 授权方式,指可以用哪种方式去实现
    }


    /**
     * @Description TODO 配置用戶信息
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 传给他一个authenticationManager用来校验传过来的用户信息是不是合法的,注进来一个,自己实现
//        endpoints.authenticationManager(authenticationManager);
        endpoints.tokenStore(tokenStore())
                .authenticationManager(authenticationManager);
    }


    /**
     * @Description TODO 配置资源服务器过来验token的规则
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        /**
         * 过来验令牌有效性的请求,不是谁都能验的,必须要是经过身份认证的。
         * 所谓身份认证就是,必须携带clientId,clientSecret,否则随便一请求过来验token是不验的
         */
        security.checkTokenAccess("isAuthenticated()");
    }

}

重新请求令牌:

Guess you like

Origin blog.csdn.net/weixin_42405670/article/details/116377069