一些收藏springcloudOauth

11. SpringCloud之集成Oauth2.0权限校验之密

2022-04-11  本文已影响0人  天还下着毛毛雨

1、Oauth2.0密码模式

在SpringCloud项目里 ,Oauth2.0密码模式 校验权限的总体流程还是 一样的。

image.png

只不过使用 密码模式获取 token,也就是说在获取 token 过程中必须带上用户的用户名和密码,获取到 的 token 是跟用户绑定的。

2、认证服务器搭建

客户端信息和用户信息 既可以存在内存里,也可以存在 数据库里, 存在内存的方式 我们已经在 上一篇 客户端模式 演示过了,接下来 密码模式就 看下 如何在数据库 存储。

2.1、表结构

2.2.1、Oauth2.0表结构:

可以直接从官网上扒下来,是Oauth2.0用来权限校验 预设的表。主要用来存token,授权码和客户端信息。

1、用户token表

CREATE TABLE `oauth_access_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(48) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  `authentication` blob,
  `refresh_token` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

2、客户端信息表

CREATE TABLE `oauth_client_details` (
  `client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用',
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密',
  `scope` varchar(256) DEFAULT NULL COMMENT '对应的范围',
  `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证模式',
  `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '认证后重定向地址',
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',
  `refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期',
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

插入 micro-order 客户端记录, 密钥为 123456的 {bcrypt}+ bcrypt加密后的 密文

INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('micro-order', 'micro-order', '{bcrypt}$2a$10$8HBBphskF43dijSHs8KQg./BWUnxeqRaFr0jbDwCcqJo0FNM6YZn2', 'all,read,writr,aa', 'client_credentials,refresh_token,password', NULL, 'oauth2', NULL, NULL, NULL, NULL);

3、客户端token

CREATE TABLE `oauth_client_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(48) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

4、授权码表

CREATE TABLE `oauth_code` (
  `code` varchar(256) DEFAULT NULL,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

5、刷新token表

CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

6、这个表不知道干嘛

CREATE TABLE `oauth_approvals` (
  `userId` varchar(256) DEFAULT NULL,
  `clientId` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `status` varchar(10) DEFAULT NULL,
  `expiresAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `lastModifiedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

2.2.2、用户角色表

用户信息包括角色权限 和我们的业务有关,由我们自己创建, 只需要向认证服务器框架提供 获取用户 信息 的 方法即可。

经典 用户 , 角色,用户_角色 关系表

1、用户表

最重要是用户名和密码

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

插入用户信息,用户名为 admin,密码为123456的 {bcrypt} + bcrypt加密后的密文

INSERT INTO `user`(`id`, `username`, `password`) VALUES (1, 'admin', '{bcrypt}$2a$10$8HBBphskF43dijSHs8KQg./BWUnxeqRaFr0jbDwCcqJo0FNM6YZn2');

2、角色表

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

插入角色 ROLE_ADMIN,注意一定要 ROLE_ 为前缀。不然会报错

INSERT INTO `role`(`id`, `name`) VALUES (1, 'ROLE_ADMIN');

3、用户角色关系表

CREATE TABLE `user_role` (
  `user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2、pom依赖

和客户端模式的依赖相同,由于需要 与数据库交互, 多引入了 数据持久化框 之类的依赖

<dependencies>
            <!-- 注册到注册中心 -->
     <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <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>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-data</artifactId>
    </dependency>
    <!--数据库操作相关, 认证服务器 需要存储token,获取用户信息,客户端信息,这里采用myql存储 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

2.3、代码配置

2.3.1、认证服务配置

@Configuration
// 开启认证服务器
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private UserServiceDetail userServiceDetail;

    @Autowired
    private ClientDetailsService clientDetailsService;

    static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);

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

    @Bean // 声明 ClientDetails实现,用数据库存储,需要配置数据源对象
    public ClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // redisTokenStore
//        endpoints.tokenStore(new MyRedisTokenStore(redisConnectionFactory))
//                .authenticationManager(authenticationManager)
//                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);

        // 存数据库
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(userServiceDetail);

        // 配置tokenServices参数
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        //支持refreshtoken
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        tokenServices.setAccessTokenValiditySeconds(60 * 5);
        //重复使用
        tokenServices.setReuseRefreshToken(false);
        tokenServices.setRefreshTokenValiditySeconds(60 * 10);
        endpoints.tokenServices(tokenServices);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允许表单认证
        security.allowFormAuthenticationForClients()
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

}

2.3.2、安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    // 密码加密方式
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().anyRequest()
                .and()
                .authorizeRequests()
//                .antMatchers("/oauth/**").permitAll();
                .antMatchers("/").authenticated();

/*        http.csrf().disable().exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                .authorizeRequests().
                antMatchers("/favicon.ico").permitAll()
                .antMatchers("/oauth/**").permitAll()
                .antMatchers("/login/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .and()
                .httpBasic().disable();*/
        http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic();  //拦截所有请求 通过httpBasic进行认证

    }
}

2.3.3、UserServiceDetail

定义了如何获取用户信息,比如用户名,密码,角色权限等,这里我们用jpa从数据库查询,也就是user,role,user_role根据用户名查询出 用户信息 交由权限框架 校验。

@Service
public class UserServiceDetail implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username);
    }
}

2.3.4、token校验接口

下游服务 调认证服务器的这个接口来验证token是否有效,token有效的话 则返回 认证后的用户信息, 让下游服务 做更细致的权限控制,比如方法级别的控制。

@Slf4j
@RestController
@RequestMapping("/security")
public class SecurityController {

    @RequestMapping(value = "/check", method = RequestMethod.GET)
    public Principal getUser(Principal principal) {
        log.info(principal.toString());
        return principal;
    }
}

2.4、配置文件

spring.application.name=oauth-pwd-server
#spring.cloud.controller.uri= http://localhost:9009/
server.port=9052
#eureka.client.service-url.defaultZone=http://localhost:9001/eureka/
eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:9001/eureka/
# 数据源配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://*****:3306/oauth2.0?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456

logging.level.org.springframework.security=debug

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

spring.security.basic.enabled=true

2.5、启动类

@SpringBootApplication
// 认证服务器本身也作为一种 受保护资源
@EnableResourceServer
public class OauthPwdServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(OauthPwdServerApplication.class, args);
    }
}

3、向认证服务器获取token

获取token的 接口仍然是 /oauth/token

3.1、Authorization请求头

在密码模式中 ,Authorization请求头需要填写 加密后的 客户端id和客户端密钥, 只能 用来校验 客户端 是否合法。

用postman,选择Basic Auth加密方式

image.png

这里的客户端id和 密码对应 我们一开始 保存到 oauth_client_details 表里的客户端记录

数据库里存的密文, 页面上填明文。

image.png

之后就可以看到在请求头里 多出了 客户端id和客户端密钥 生成的密钥

image.png

3.2、body参数

用户名和密码对应 user表里的 用户名和密码

数据库里存的密文, 页面上填明文。

image.png image.png

最后调用该接口, 可以成功获取到token,证明 可以认证服务器 密码 模式 已经搭建成功。

4、资源服务器(下游服务)搭建

SpringCloud项目中的需要权限校验的下游服务 就是 对应 oauth2.0的模型中, 受保护的资源服务(Resource Server),需要进行权限校验后才能 正常访问。

我们把之前的micro-order服务改成 资源服务器,让他的接口需要权限校验。

之后 调用micro-order服务 就需要传有效的token才能访问。

4.1、pom依赖

新增oauth2.0权限校验 依赖

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

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

4.2、代码配置

4.2.1、客户端配置

/*
  鉴权过滤器
* @  OAuth2AuthenticationProcessingFilter
*
* */
@EnableOAuth2Client
@EnableConfigurationProperties
@Configuration
public class OAuth2ClientConfig {


    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }

//    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor(ClientCredentialsResourceDetails clientCredentialsResourceDetails) {
        return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails);
    }

    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails());
    }
}

4.2.2、刷新token配置

public class RefreshTokenAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Autowired
    private ClientCredentialsResourceDetails clientCredentialsResourceDetails;

    private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();

    @Autowired
    RestTemplate restTemplate;

    private static String oauth_server_url = "http://oauth-pwd-server/oauth/token";

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        try {
            //解析异常,如果是401则处理
            ResponseEntity<?> result = exceptionTranslator.translate(authException);
            if (result.getStatusCode() == HttpStatus.UNAUTHORIZED) {
                MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
                formData.add("client_id", clientCredentialsResourceDetails.getClientId());
                formData.add("client_secret", clientCredentialsResourceDetails.getClientSecret());
                formData.add("grant_type", clientCredentialsResourceDetails.getGrantType());
                formData.add("scope", String.join(",", clientCredentialsResourceDetails.getScope()));
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
                Map map = restTemplate.exchange(oauth_server_url, HttpMethod.POST,
                        new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
                //如果刷新异常
                if (map.get("error") != null) {
                    // 返回指定格式的错误信息
                    response.setStatus(401);
                    response.setHeader("Content-Type", "application/json;charset=utf-8");
                    response.getWriter().print("{\"code\":1,\"message\":\"" + map.get("error_description") + "\"}");
                    response.getWriter().flush();
                    //如果是网页,跳转到登陆页面
                    //response.sendRedirect("login");
                } else {
                    //如果刷新成功则存储cookie并且跳转到原来需要访问的页面

                    for (Object key : map.keySet()) {
                        response.addCookie(new Cookie(key.toString(), map.get(key).toString()));
                    }
                    request.getRequestDispatcher(request.getRequestURI()).forward(request, response);
//                    response.sendRedirect(request.getRequestURI());

                    //将access_token保存
                }
            } else {
                //如果不是401异常,则以默认的方法继续处理其他异常
                super.commence(request, response, authException);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

4.2.3、资源服务器配置

@Configuration
@EnableResourceServer
//启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

//    @Autowired
//    private TokenStore tokenStore;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        // 配置order访问控制,必须认证后才可以访问
        http.authorizeRequests()
                .antMatchers("/order/**").authenticated();
    }

    /*
    * 把token验证失败后,重新刷新token的类设置到 OAuth2AuthenticationProcessingFilter
    * token验证过滤器中
    * */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        super.configure(resources);
//        resources.authenticationEntryPoint(new RefreshTokenAuthenticationEntryPoint());
//        resources.tokenStore(tokenStore);
    }
}

4.3、配置文件

新增权限校验相关配置

# 配置 认证服务器 校验token的接口地址就行
security.oauth2.resource.user-info-uri=http://127.0.0.1:9052/security/check
security.oauth2.resource.prefer-token-info=false

4.4、测试下游服务 是否鉴权成功

利用 从认证服务器获取到的token,放到Authorization请求头里, 加前缀 bearer + 空格 +token,去直接请求 micro-order 的以下接口( 从网关路由到该服务的该接口也可以, 只要网关不过滤掉 Authorization 请求信息就行)。

@RequestMapping("/order")
@RestController
@RefreshScope
public class ConfigController {

    @Value("${username}")
    private String username;
    
    // 方法级别控制,必须是ROLE_ADMIN 角色才能访问
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @GetMapping("/getUsername")
    public String getUsername(HttpServletRequest request) {
        System.out.println(request.getHeader("Authorization"));
        return username;
    }
}

1、输入正确的token

image.png

发现是可以调用成功的

2、如果不输入token,报401未授权

image.png

3、输入错误的token,报401,token非法

image.png

至此, 资源服务器 确实 是 有权限校验的, 说明搭建成功。

5、认证服务器和下游系统权限校验流程

加上网关zuul, 大概的校验流程就是这样的:


image.png
  1. zuul 携带 token 请求下游系统,被下游系统 filter 拦截

  2. 下游系统过滤器根据配置中的 user-info-uri 请求到认证服务器

  3. 请求到认证服务器被 filter 拦截进行 token 校验,把 token 对应的用户、和权限从数据库

查询出来封装到 Principal .

  1. 认证服务器 token 校验通过后过滤器放行执行 security/check 接口,把 principal 对象返回

  2. 下游系统接收到 principal 对象后就知道该 token 具备的权限了,就可以进行相应用户对

应的 token 的权限执行

上一篇 下一篇

猜你喜欢

热点阅读