第六章 Auth2微服务权限校验笔记

一、oauth2.0相关概念介绍

采用token认证的方式校验是否有接口调用权限,然后在下游系统设置访问白名单只允许zuul服务器访问。理论上zuul服务器是不需要进行权限校验的,因为zuul服务器没有接口,不需要从zuul调用业务接口,zuul只做简单的路由工作。下游系统在获取到token后,通过过滤器把token发到认证服务器校验该token是否有效,如果认证服务器校验通过就会携带这个token相关的验证信息传回给下游系统,下游系统根据这个返回结果就知道该token具有的权限是什么了。所以校验token的过程,涉及到下游系统和认证服务器的交互,这点不好。占用了宝贵的请求时间。

oauth2.0获取token的过程:

Token是通过一个单独的认证服务器来颁发的,只有具备了某种资质认证服务器才会把token给申请者。

1、平台认证申请

平台认证申请往往是一个比较繁琐的过程,需要申请方提供比较完整的认证申请材料,比如公司资质,营业执照等等信息,提交申请后认证方审核通过后,该平台才会允许申请token。

2、oauth2.0获取token的4种方式

获取token在oauth2.0里面有4中模式,这里我们讲三种:

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

3、OAuth2 角色

resource owner:资源所有者(指用户)

resource server:资源服务器存放受保护资源,要访问这些资源,需要获得访问令牌(下面例子中的 Twitter 资源服务器)

client:客户端代表请求资源服务器资源的第三方程序(下面例子中的 Quora)客户端同时也可能是一个资源服务器

authrization server:授权服务器用于发放访问令牌给客户端(下面例子中的 Twitter 授权服务器)

参考链接:https://www.jianshu.com/p/68f22f9a00ee

4、项目环境信息准备:

认证服务器项目:

micro-jwt认证服务jwt方式

micro-security-redis 是认证服务器,发布token信息存储到redis里面的,

micro-security-db 服务token存储DB

业务服务权限校验:

micro-order-jwt 用jwt方式进行权限校验

micro-web-jwt服务是jwt方式校验

micro-order-security非jwt方式校验

micro-web-security非jwt方式校验

二、客户端模式获取token

1、认证服务器搭建

1.1、配置依赖环境准备

1)新建项目springcloud-security-db项目(这里使用db存储)

2)pom依赖添加

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <!-- springboot 监控 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- Nacos服务发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <exclusions>
            <exclusion>
                <groupId>com.alibaba.nacos</groupId>
                <artifactId>nacos-client</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--  spring-cloud-starter-alibaba 默认引入的Nacos client版本较低,所以需要单独引用-->
    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
    </dependency>
    <!-- Nacos配置中心 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!-- 添加oauth2框架 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <!-- 添加JPA支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.46</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

3添加配置application.yaml

server:
  port: 8081
spring:
  application:
    name: springcloud-security-db
  main:
    allow-bean-definition-overriding: true
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/uni_meta_config?characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update
      show-sql: true

1.2、核心代码服务端Spring Security配置

 创建一个spring security 配置类,在配置类中注入了上面我们自定义的自定义UserDetailsService以及用户密码验证器。

/**
 * WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity>
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启security注解
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    /**
     * 自定义MyUserDetailsService用来从数据库中根据用户名查询用户信息以及角色信息
     */
    @Autowired
    public MyUserDetailsService myUserDetailsService;
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
    /**
     * 验证配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().exceptionHandling()
                .and().requestMatchers().anyRequest()
                .and().authorizeRequests()
                .antMatchers("/actuator/","/token/","/oauth/authorize","/oauth/**").permitAll();
//                .antMatchers("/").authenticated();
        http.authorizeRequests()
                .antMatchers("/**")
                .fullyAuthenticated()
//                .and().userDetailsService(myUserDetailsService)
                .and().httpBasic();  //拦截所有请求 通过httpBasic进行认证
    }
//    /**
//     * 用户信息校验
//     */
//    @Bean
//    @Override
//    protected UserDetailsService userDetailsService() {
//        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//        String finalPassword = "{bcrypt}" + bCryptPasswordEncoder.encode("123456");
////        JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        User.UserBuilder builder = User.builder();
//        UserDetails user = builder.username("hankin").password(finalPassword).roles("USER").build();
//        UserDetails admin = builder.username("admin").password(finalPassword).roles("USER", "ADMIN").build();
//        manager.createUser(user);
//        manager.createUser(admin);
//        return manager;
//    }
    /**
     *  密码加密
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

注意代码里面我们自定义MyUserDetailsService实现了oauth2的UserDetailsService接口,用来从数据库中根据用户名查询用户信息以及角色信息,代码详情如下:

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username);
    }
}

HttpSecurity配置属性说明:

access(String) 如果给定的SpEL表达式计算结果为true,就允许访问

anonymous() 允许匿名用户访问

authenticated() 允许认证的用户进行访问

denyAll() 无条件拒绝所有访问

fullyAuthenticated() 如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问

hasAuthority(String) 如果用户具备给定权限的话就允许访问

hasAnyAuthority(String…)如果用户具备给定权限中的某一个的话,就允许访问

hasRole(String) 如果用户具备给定角色(用户组)的话,就允许访问/

hasAnyRole(String…) 如果用户具有给定角色(用户组)中的一个的话,允许访问.

hasIpAddress(String 如果请求来自给定ip地址的话,就允许访问.

not() 对其他访问结果求反.

permitAll() 无条件允许访问

rememberMe() 如果用户是通过Remember-me功能认证的,就允许访问

1.3、授权服务器类代码实现Authorization Server授权服务器

授权服务器负责验证用户标识并提供令牌,使用@EnableAuthorizationServer注解启用授权服务器配置。 

@Configuration
@EnableAuthorizationServer
@Order(6)
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private ClientDetailsService clientDetailsService;
    static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);
    // token的保存方式,token是也存储在数据库中
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }
    /**
     * 客户端配置,客户端信息是存储在表oauth_client_details中的
     */
    @Bean // 声明 ClientDetails实现
    public ClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }
    /**
     *  用来配置客户端详情服务(ClientDetailsService),
     *  客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
     *  1.授权码模式(authorization code)
     *  2.简化模式(implicit)
     *  3.密码模式(resource owner password credentials)
     *  4.客户端模式(client credentials)
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //设置客户端的配置从数据库中读取,存储在oauth_client_details表
        clients.withClientDetails(clientDetailsService);
    }
    /**
     * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。
     * 1、申明token的存储方式
     * 2、定义了权限校验管理器
     * 3、定义了用户校验service
     * 4、token的属性信息
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 开启密码验证,来源于 WebSecurityConfigurerAdapter
//        endpoints.authenticationManager(authenticationManager)
//                .userDetailsService(myUserDetailsService); // 读取验证用户的信息
        // 存数据库
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(myUserDetailsService);
        logger.info("myUserDetailsService=={}", JSON.toJSONString(myUserDetailsService));
        // 配置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);
    }
    /**
     * 配置 token 节点的安全策略
     * AuthorizationServerEndpointsConfigurer:
     * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。
     * */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允许表单认证
        security.allowFormAuthenticationForClients()
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }
}

实体类...略。

1.4、添加启动类配置:@EnableResourceServer

@EnableResourceServer注解开启资源服务,因为程序需要对外暴露获取token的API和验证token的API所以该程序也是一个资源服务器。

@EnableResourceServer
@EnableDiscoveryClient
@SpringBootApplication
public class SecurityAuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityAuthApplication.class,args);
    }
}

2、系统脚本初始化

2.1、初始化对应的系统表

注意:用户角色表示我们自己 新建的,不是oauth2框架默认的表

这里我们使用了hibernate自动创建表,因此在认证服务器启动的时候就会初始化好对应的表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for clientdetails
-- ----------------------------
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails`  (
  `appId` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `resourceIds` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `appSecret` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `scope` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `grantTypes` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `redirectUrl` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorities` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(11) NULL DEFAULT NULL,
  `refresh_token_validity` int(11) NULL DEFAULT NULL,
  `additionalInformation` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `autoApproveScopes` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`appId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token`  (
  `token_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `token` blob NULL,
  `authentication_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authentication` blob NULL,
  `refresh_token` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals`  (
  `userId` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `clientId` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `scope` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `expiresAt` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
  `lastModifiedAt` timestamp(0) NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`  (
  `client_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `resource_ids` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `client_secret` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `scope` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `authorized_grant_types` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `web_server_redirect_uri` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `authorities` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `access_token_validity` int(11) NULL DEFAULT NULL,
  `refresh_token_validity` int(11) NULL DEFAULT NULL,
  `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  `autoapprove` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token`  (
  `token_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `token` blob NULL,
  `authentication_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code`  (
  `code` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authentication` blob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token`  (
  `token_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `token` blob NULL,
  `authentication` blob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `oauth_role`;
CREATE TABLE `oauth_role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `oauth_user`;
CREATE TABLE `oauth_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `UK_sb8bbouer5wak8vyiiy4pf2bx`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_user_role
-- ----------------------------
DROP TABLE IF EXISTS `oauth_user_role`;
CREATE TABLE `oauth_user_role`  (
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
INDEX `FK8wl1eq77d6noru05ccao0jody`(`role_id`) USING BTREE,
INDEX `FKbbbedu4u7x785lopv8mbttppw`(`user_id`) USING BTREE,
CONSTRAINT `FK8wl1eq77d6noru05ccao0jody` FOREIGN KEY (`role_id`) REFERENCES `oauth_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FKbbbedu4u7x785lopv8mbttppw` FOREIGN KEY (`user_id`) REFERENCES `oauth_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2.2、初始化一些测试数据

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('android', NULL, '{bcrypt}$2a$10$y.iyrX9c7lotqEqs4JF8ZuDfr06I.7Ryg7y7aUYFO.rqIhca32PBi', 'all', 'client_credentials,refresh_token', 'http://localhost:7200/auth2/callback', 'oauth2', NULL, NULL, NULL, 'true');
INSERT INTO `oauth_client_details` VALUES ('pc', NULL, '{bcrypt}$2a$10$y.iyrX9c7lotqEqs4JF8ZuDfr06I.7Ryg7y7aUYFO.rqIhca32PBi', 'all', 'password,refresh_token,client_credentials,authorization_code', 'http://localhost:7200/auth2/callback', 'oauth2', NULL, NULL, NULL, '');
-- ----------------------------
-- Records of oauth_role
-- ----------------------------
INSERT INTO `oauth_role` VALUES (1, 'ROLE_ADMIN');
INSERT INTO `oauth_role` VALUES (2, 'ROLE_MENU');
INSERT INTO `oauth_role` VALUES (3, 'ROLE_USER');
-- ----------------------------
-- Records of oauth_user
-- ----------------------------
INSERT INTO `oauth_user` VALUES (8, '{bcrypt}$2a$10$OXpjxbwQGUTCthXkobOPfOOoviqeKhBFhaYYqjVPZ7sTVAw8Jxtcu', 'admin');
INSERT INTO `oauth_user` VALUES (9, '{bcrypt}$2a$10$OXpjxbwQGUTCthXkobOPfOOoviqeKhBFhaYYqjVPZ7sTVAw8Jxtcu', 'hankin');
INSERT INTO `oauth_user` VALUES (10, '{bcrypt}$2a$10$OXpjxbwQGUTCthXkobOPfOOoviqeKhBFhaYYqjVPZ7sTVAw8Jxtcu', 'chj');
-- ----------------------------
-- Records of oauth_user_role
-- ----------------------------
INSERT INTO `oauth_user_role` VALUES (8, 1);
INSERT INTO `oauth_user_role` VALUES (9, 2);
INSERT INTO `oauth_user_role` VALUES (10, 3);
INSERT INTO `oauth_user_role` VALUES (8, 2);
INSERT INTO `oauth_user_role` VALUES (8, 3);

2.3、启动认证服务器获取token

首先在配置类方法com.chj.config.AuthorizationServerConfig#configure(...)里面配置好认证信息,这里客户端认证信息我们手动通过脚本已经初始化到数据库表oauth_client_details中,代码中中的配置会读取数据库中的信息。

然后访问:http://localhost:8081/oauth/token,可以返回token信息如下所示:

 

第六章 Auth2微服务权限校验笔记

我们可以看到客户端模式申请token,只要带上有平台资质的客户端id、客户端密码、然后带上授权类型是客户端授权模式,带上scope就可以了,这里要注意的是客户端必须是具有资质的

备注:基于redis存储token 信息的认证服务器搭建在springcloud-security-redis工程。

2.4、用户全权限校验接口:

接口地址:http://localhost:8081/security/check

1)注意请求头里面添加token信息,如下所示:

 

第六章 Auth2微服务权限校验笔记

2)请求body里面参数与 客户端模式获取token对应的参数相同,如下图所示:

 

第六章 Auth2微服务权限校验笔记

返回结果如下:

{

    "authorities": [

        {

            "authority""oauth2"

        }

    ],

    "details": {

        "remoteAddress""0:0:0:0:0:0:0:1",

        "sessionId"null,

        "tokenValue""f050d6b1-55e2-4b47-8caf-1ec36c4bea1f",

        "tokenType""Bearer",

        "decodedDetails"null

    },

    "authenticated"true,

    "userAuthentication"null,

    "oauth2Request": {

        "clientId""pc",

        "scope": [

            "all"

        ],

        "requestParameters": {

            "grant_type""client_credentials",

            "scope""all",

            "client_id""pc"

        },

        "resourceIds": [],

        "authorities": [

            {

                "authority""oauth2"

            }

        ],

        "approved"true,

        "refresh"false,

        "redirectUri"null,

        "responseTypes": [],

        "extensions": {},

        "grantType""client_credentials",

        "refreshTokenRequest"null

    },

    "principal""pc",

    "credentials""",

    "clientOnly"true,

    "name""pc"

}

3、客户端模块搭建

3.1、客户端环境配置

1)新建模块:springcloud-micro-security-web

2)添加pom依赖

<!-- 添加oath2框架依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

3)配置信息如下:

# 服务鉴权配置
security:
  oauth2:
    resource:
      user-info-uri: http://localhost:8081/security/check
      prefer-token-info: false
    client:
      clientId: pc
      client-secret: 123456
      access-token-uri: http://localhost:8081/oauth/token
      authorized-grant-types: client_credentials
      scope: all

3.2、oauth2客户端代码如下:

1)通过注解@EnableOAuth2Client声明该类为oauth2的一个客户端,注意@ConfigurationProperties(prefix = "security.oauth2.client")注解获取配置文件中配置前缀为security.oauth2.client的配置信息。

/**
 * 鉴权过滤器 @OAuth2AuthenticationProcessingFilter
 */
@EnableOAuth2Client
@EnableConfigurationProperties
@Configuration
public class OAuth2ClientConfig {
    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }
    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails());
    }
}

2)Resource Server -资源服务器配置

资源服务器,受OAuth2令牌保护的资源

@Configuration
@EnableResourceServer
//TODO 启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    //指定拦截路径
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        // 配置provider\auth访问控制,必须认证后才可以访问
        http.authorizeRequests().antMatchers("/provider/**","/auth/**").authenticated();
    }
    /*
    * 把token验证失败后,重新刷新token的类设置到OAuth2AuthenticationProcessingFilter--token验证过滤器中
    * */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        super.configure(resources);
        resources.authenticationEntryPoint(new RefreshTokenAuthenticationEntryPoint());
    }
}

3)刷新token代码:

public class RefreshTokenAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
    @Autowired
    private ClientCredentialsResourceDetails clientCredentialsResourceDetails;
    private WebResponseExceptionTranslator<?> exceptionTranslator = new DefaultWebResponseExceptionTranslator();
    @Autowired
    RestTemplate restTemplate;
    @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);
                // http://localhost:8081/oauth/token 认证服务器token校验通过后过滤器放行执行security/check接口,把principal对象返回
                Map map = restTemplate.exchange(
                        clientCredentialsResourceDetails.getAccessTokenUri(), 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)客户端添加测试类:

@Slf4j
@RestController
@RequestMapping("/provider")
public class UserController {
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
    @PreAuthorize("hasRole('ROLE_MENU')")
    @RequestMapping("/menu")
    public String menu() {
        return "ROLE_MENU";
    }
    @PreAuthorize("hasRole('ROLE_USER')")
    @RequestMapping("/user")
    public String user() {
        return "ROLE_USER";
    }
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @RequestMapping("/admin")
    public String admin() {
        return "ROLE_ADMIN";
    }
}

3.3、测试验证

分别启动服务端和客户端,客户端

1)首先从服务端获取token(客户端模式获取)

http://localhost:8081/oauth/token

{

    "access_token""3dc1bdfe-91cb-4b2e-bc51-f612a58a72b6",

    "token_type""bearer",

    "expires_in"200,

    "scope""all"

}

2)客户端访问测试接口

访问:http://localhost:7200/sentinel/hello,正常返回结果(没有被拦截)

访问:http://localhost:7200/provider/index,可以返回结果(被拦截但是不需要做接口权限校验)

返回结果“index”

访问如下加了权限校验的接口:

http://localhost:7200/provider/admin

http://localhost:7200/provider/menu

http://localhost:7200/provider/user

发现均无结果返回,查看后套报错信息如下:

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext

备注:因为我们在auth请求拦截配置了,只拦截指定路劲"/provider/**","/auth/**,因此其他路劲接口是可以正常访问的,这两个路劲下的接口上面添加了注解@PreAuthorize("hasRole('ROLE_MENU')")的接口会做权限校验。

/**
 * 指定拦截路径:配置provider、consumer访问控制,必须认证后才可以访问
 * CSRF(Cross-site request forgery),中文名称:跨站请求伪造
 */
@Override
public void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.authorizeRequests().antMatchers("/provider/**","/auth/**").authenticated();
}

三、密码模式获取token

1、认证服务器配置

认证服务器代码与前面的客户端模式服务端是一样的,这里使用的是同一个服务端。

1.1、密码模式获取token

密码模式获取token,也就是说在获取token过程中必须带上用户的用户名和密码,获取到的token是跟用户绑定的。客户端id和客户端密码必须要经过base64算法加密,并且放到header中,加密模式为Base64(clientId:clientPassword),如下:

 

第六章 Auth2微服务权限校验笔记

注意:上面的请求头里面一定要包含Basic Auth认证信息,以及客户端资质信息,这里我们用前面初始化好的客户端id密码:pc\123456做测试。

其他参数配置,最重要参数的注意授权类型为password,其次需要和用户绑定,输入用户信息:

 

第六章 Auth2微服务权限校验笔记

获取token地址:http://localhost:3030/oauth/token

返回结果如下:

{

    "access_token""0d22ca79-4edc-47a9-8948-8911d46e5699",

    "token_type""bearer",

    "refresh_token""f3b46e83-bc8d-484c-9c71-77cbadac16df",

    "expires_in"299,

    "scope""all"

}

对应的token存储表为:oauth_access_token表中数据:

 

第六章 Auth2微服务权限校验笔记

可以看到token是跟用户绑定的。

1.2、认证服务器token校验和校验结果返回接口

注意:该接口能返回信息的前天是认证服务器必须添加@EnableResourceServer注解,开启资源服务,因为程序需要对外暴露获取token的API和验证token的API所以该程序也是一个资源服务器。

@Slf4j
@RestController
@RequestMapping("/security")
public class SecurityController {
    @RequestMapping(value = "/check", method = RequestMethod.GET)
    public Principal getUser(Principal principal) {
//        log.info("security server check================>>>" + principal.toString());
        return principal;
    }
}

1.3、用户全权限校验接口:

接口地址:http://localhost:8081/security/check

1)注意请求头里面添加token信息,如下所示:

 

第六章 Auth2微服务权限校验笔记

2)请求body里面添加密码模式对应的相关参数,如下图所示:

 

第六章 Auth2微服务权限校验笔记

注意这里面的username与password是用户的信息。

返回的结果信息如下:

 {
    "authorities": [
        {
            "id"1,
            "name": "ROLE_ADMIN",
            "authority": "ROLE_ADMIN"
        }
    ],
    "details": {
        "remoteAddress""0:0:0:0:0:0:0:1",
        "sessionId"null,
        "tokenValue""0f475dec-878e-42d2-a047-46da5eb6901d",
        "tokenType""Bearer",
        "decodedDetails"null
    },
    "authenticated"true,
    "userAuthentication": {
        "authorities": [
            {
                "id"1,
                "name""ROLE_ADMIN",
                "authority""ROLE_ADMIN"
            }
        ],
        "details": {
            "grant_type": "password",
            "scope": "all",
            "username": "hankin"
        },
        "authenticated"true,
        "principal": {
            "id": 9,
            "username": "hankin",
            "password": "{bcrypt}$2a$10$OXpjxbwQGUTCthXkobOPfOOoviqeKhBFhaYYqjVPZ7sTVAw8Jxtcu",
            "authorities": [
                {
                    "id": 1,
                    "name": "ROLE_ADMIN",
                    "authority": "ROLE_ADMIN"
                }
            ],
            "enabled"true,
            "accountNonExpired"true,
            "accountNonLocked"true,
            "credentialsNonExpired"true
        },
        "credentials"null,
        "name""hankin"
    },
    "oauth2Request": {
        "clientId""pc",
        "scope": [
            "all"
        ],
        "requestParameters": {
            "grant_type""password",
            "scope""all",
            "username""hankin"
        },
        "resourceIds": [],
        "authorities": [
            {
                "authority""oauth2"
            }
        ],
        "approved"true,
        "refresh"false,
        "redirectUri"null,
        "responseTypes": [],
        "extensions": {},
        "grantType""password",
        "refreshTokenRequest"null
    },
    "principal": {
        "id"9,
        "username""hankin",
        "password""{bcrypt}$2a$10$OXpjxbwQGUTCthXkobOPfOOoviqeKhBFhaYYqjVPZ7sTVAw8Jxtcu",
        "authorities": [
            {
                "id"1,
                "name""ROLE_ADMIN",
                "authority""ROLE_ADMIN"
            }
        ],
        "enabled"true,
        "accountNonExpired"true,
        "accountNonLocked"true,
        "credentialsNonExpired"true
    },
    "credentials""",
    "clientOnly"false,
    "name""hankin"
    }

2、密码模式客户端配置

2.1、客户端配置

uni-meta-provider项目application.yml配置如下所示:

# 服务鉴权配置
security:
  oauth2:
    resource:
      user-info-uri: http://localhost:8081/security/check
      prefer-token-info: false
    client:
      clientId: pc
      client-secret: 123456
      access-token-uri: http://localhost:8081/oauth/token
      grant-type: password # password authorization_code
      scope: all
#      registered-redirect-uri:

代码部分与客户端模式一样,这里不再赘述。

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

1)zuul携带token请求下游系统,被下游系统filter拦截。

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

3)请求到认证服务器被filter拦截进行token校验,把token对应的用户、和权限从数据库查询出来封装到Principal。

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

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

3、测试验证

3.1、服务端启动获取token

使用admin用户获取token,客户端信息为pc/123456,结果如下所示:

 

第六章 Auth2微服务权限校验笔记

返回token信息如下:

{

    "access_token""da6cbee1-b676-4bf3-bfd2-8d801b6f4fc3",

    "token_type""bearer",

    "refresh_token""0b907f7c-bb39-444b-9655-8da4b17912ce",

    "expires_in"299,

    "scope""all"

}

3.2、客户端访问接口测试

1)然后访问分别访问:provider/admin,provider/menu,provider/user接口,结果只有menu接口可以正常返回结果,其他都是没权限(用户admin具有所有权限,hankin用户只有menu权限),可以看到正常返回结果:

 

第六章 Auth2微服务权限校验笔记

2)然后访问接口:/provider/admin或者/provider/user,可以看到返回结果如下:

<ForbiddenException>

    <error>access_denied</error>

    <error_description>不允许访问</error_description>

</ForbiddenException>

四、授权码模式获取token

授权码模式获取token,在获取token之前需要有一个获取code的过程。授权码模式是功能最完整、流程最严密的授权模式,它的特点是通过客户端的后台服务器,与“服务器提供”的认证服务器进行互动。

1、获取code的流程如下:

1.1、授权码流程如下所示:

 

A)用户访问客户端,后者将前者导向认证服务器

B)用户选择是否给予客户端授权

C)假设用户给予授权,认证服务器将用户导向客户端事先指定的“重定向 URI”,同时附上一个授权码

D)客户端收到授权码,附上早先的“重定向 URI”向认证服务器申请令牌,这一步是在客户端的后台服务器上完成的,对用户不可见

E)认证服务器核对了授权码和重定向URI,确认无误后向客户端发送令牌和更新令牌

授权服务器对客户端进行认证,校验授权码,并确保这个重定向URI和第三步(C)中那个URI匹配。如果校验通过,则发放访问令牌,以及可选的刷新令牌。

1)A步骤中,客户端申请认证的 URI,包含以下参数: 

repsone_type:授权类型,必选,此处固定值“code”

client_id:客户端的ID,必选

client_secret:客户端的密码,可选

redirect_uri:重定向URI,可选

scope:申请的权限范围,可选

state:客户端当前的状态,可以指定任意值,认证服务器会原封不动的返回这个值

2)C步骤中,服务器回应客户端的URI,包含以下参数:

code:表示授权码,必须按,该码有效期应该很短,通常10分钟,客户端只能使用一次,否则会被授权服务器拒绝,该码与客户端 ID 和 重定向 URI 是一一对应关系

state:如果客户端请求中包含着歌参数,认证服务器的回应也必须一模一样包含这个参数

3)D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

grant_type:表示使用的授权模式,必选,此处固定值为“authorization_code”

code:表示上一步获得的授权吗,必选

redirect_uri:重定向URI,必选,与步骤 A 中保持一致

client_id:表示客户端ID,必选

4)E步骤中,认证服务器发送的HTTP回复,包含以下参数: 

access_token:表示令牌,必选

token_type:表示令牌类型,该值大小写不敏感,必选,可以是 bearer 类型或 mac 类型

expires_in:表示过期时间,单位为秒,若省略该参数,必须设置其它过期时间

refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选

scope:表示权限范围

从上面代码可以看到,参数使用JSON格式发送(Content-Type: application/json),此外HTTP头信息中明确指定不得缓存。

1.2、用户请求获取code的链接

1)请求地址参数如下:

http://localhost:8081/oauth/authorize?grant_type=authorization_code&client_id=pc&response_type=code&redirect_uri=http://localhost:7200/auth2/callback

注意

该路劲后面的redirect_uri必须与表oauth_client_details中web_server_redirect_uri字段的值保持一致:

 

第六章 Auth2微服务权限校验笔记

如果方法AuthCodeServerConfig#configure()中客户端信息使用内存存储则不需要数据库中查询,配置代码如下所示:

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //设置客户端的配置从数据库中读取,存储在oauth_client_details表
        clients.inMemory() .withClient("pc")
                .secret("{bcrypt}$2a$10$y.iyrX9c7lotqEqs4JF8ZuDfr06I.7Ryg7y7aUYFO.rqIhca32PBi")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("all")
                .redirectUris("http://localhost:7200/auth2/callback");
//        clients.withClientDetails(clientDetailsService);
    }

2)提示要输入用户名密码(比如:hankin/123456)

3)用户名秘密成功则会弹出界面

 

第六章 Auth2微服务权限校验笔记

4)点击 approve则会回调redirect_uri对应的回调地址并且把code附带到该回调地址里面

例如:http://localhost:7200/auth2/callback?code=p9hRU8

 

第六章 Auth2微服务权限校验笔记

1.3、特别注意:

在获取授权码以后跳转到回到地址的时候,回调接口无法返回token信息,后台报错信息如下:

org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException: Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it.

这里使用授权码方式获取token通过RestTemplate方式还有问题,待解决。。

问题参考博客地址:

https://blog.csdn.net/u013360850/article/details/81879964

2、根据获取到的code获取token

这里必须带上redirect_uri和code,其他就跟前面的类似:

 

第六章 Auth2微服务权限校验笔记

 

返回的结果信息如下:

{

    "access_token""3e9b9a78-982a-4982-a56d-2474dca87fb9",

    "token_type""bearer",

    "refresh_token""b81674c7-9716-4454-b6d2-b337c6b39183",

    "expires_in"43199,

    "scope""all"

}

其他配置跟密码模式的是一样的,拿到token后就可以访问了。

注意:oauth_client_details的autoapprove字段值如果设置为true,则获取code的时候不会弹出确认页面,输入用户名密码后直接返回结果。

用户角色权限访问测试与面模式一样,比如hankin用户绑定的token只能访问menu接口:

访问:http://localhost:7200/provider/menu

返回结果:oauth/ROLE_MENU

分别访问:

http://localhost:7200/provider/admin

http://localhost:7200/provider/user

返回结果如下:

<ForbiddenException>

    <error>access_denied</error>

    <error_description>不允许访问</error_description>

</ForbiddenException>

3、三种模式总结:

1)客户端模式

一般用在无需用户登录的系统做接口的安全校验,因为token只需要跟客户端绑定,控制粒度不够细

2)密码模式

密码模式,token是跟用户绑定的,可以根据不同用户的角色和权限来控制不同用户的访问权限,相对来说控制粒度更细

3)授权码模式

授权码模式更安全,因为前面的密码模式可能会存在密码泄露后,别人拿到密码也可以照样的申请到token来进行接口访问,而授权码模式用户提供用户名和密码获取后,还需要有一个回调过程,这个回调你可以想象成是用户的手机或者邮箱的回调,只有用户本人能收到这个code,即使用户名密码被盗也不会影响整个系统的安全。