OAuth

Starter

OAuth

OAuth2.0 是一种协议,提供认证和授权的标准 SpringSecurity,Shiro等框架有自己的实现

OAuth2.0 涵盖两个服务,可放于一个应用程序中实现,也可用于 1(认证服务)+ N(资源服务)分别部署

  • 认证服务 Authorization Server:认证合法性,颁发token,即包含两个必须要实现的endpoints
    • AuthorizationEndpoint 用于认证的请求,默认URL:/auth/authorize
    • TokenEndpoint 用于获取token的请求,默认URL:/auth/token
  • 资源服务 Resource Server:拦截保护资源,token鉴权
    • OAuth2AuthenticationProcessingFilter 拦截鉴权token

配置:认证授权服务

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }
}

需配置以下三个对象:

  • ClientDetailsServiceConfigurer 配置哪些客户端可使用此服务
  • AuthorizationServerEndpointsConfigurer 配置获取token的访问端点,token服务(token如何发放,存储等)
    • 通过以下属性决定支持的授权类型(Grant Types)
      • authenticationManager 为"password"模式服务
      • userDetailsService
      • authorizationCodeServices 为“authorization_code"授权码模式服务
      • implicitGrantService 设置隐式授权模式
      • tokenGranter 自定义模式使用
    • pathMapping()配置端点URL,默认:
      • /oauth/authorize 认证
      • /oauth/token 获取token
      • /oauth/confirm_access 确认授权提交
      • /oauth/error 错误
      • /oauth/check_token 校验token
      • /oauth/token_key 公有密钥
  • AuthorizationServerSecurityConfigurer 配置token端点的安全约束(授权哪些可以访问token端点)

认证授权模式:

  • 授权码模式 authorization_code
  • 简化模式 implicit
  • 密码模式 password
  • 客户端模式 client_credentials

  • 认证:验证账号密码

  • 授权:这个角色能操作哪些数据
    • 角色:人(运营,编辑,管理员,...)/ 系统 (日志系统,监控系统,...)/ 时间(定时清理,...)
    • 权限控制模型:
      • RBAC 基于角色的访问控制 (Role Based Access Control)
      • ACL 访问控制列表
      • ABAC 基于属性
      • PBAC 基于策略

配置:资源服务

public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
    }
}

ResourceServerTokenServices 验证token

  • DefaultTokenService 在资源服务器本地,配置token存储,解码等服务(认证和资源服务在同一个应用程序时使用)
  • RemoteTokenService 资源服务器通过Http方式,请求认证服点(/oauth/check_token)来完成解码token(认证和资源服务不在同一个应用程序时使用)

Doc

https://spring.io/projects/spring-security-oauth https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

Sample

认证服务: 基于内存方式

  1. application.yml

     server:
       port: 5000
       servlet:
         context-path: /dear-auth
    
  2. 配置OAuth认证授权服务器

     @Configuration
     @EnableAuthorizationServer
     public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
    
         /// 配置Client
         @Autowired
         private PasswordEncoder passwordEncoder;
    
         @Override
         public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
             // 使用内存方式
             clients.inMemory()
                 .withClient("DearApp")//客户端id
                 .secret(passwordEncoder.encode("DearApp"))
                 .authorizedGrantTypes("authorization_code", "password","refresh_token", ) // 该客户端允许的授权类型(authorization_code,password,client_credentials,implicit,refresh_token)
                 .scopes("app")//允许的授权范围,名称自定义,是个标识,必填
                 .redirectUris("http://www.baidu.com") //验证回调地址
                 ;
         }
     }
    
  3. 配置Security(提供认证授权页面进行认证: http://localhost:5000/dear-auth/login

     @Configuration
     @EnableWebSecurity
     @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
     class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
         // 采用bcrypt对密码进行编码
         @Bean
         public PasswordEncoder passwordEncoder() {
             return new BCryptPasswordEncoder();
         }
    
         @Override
         protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             auth.inMemoryAuthentication()
                     .withUser("admin").password(passwordEncoder().encode("123")).roles("ADMIN")
                     .and()
                     .withUser("user").password(passwordEncoder().encode("123")).roles("USER")
                     ;
         }
     }
    
  4. AuthApp.java

     @SpringBootApplication
     public class AuthApp {
         public static void main(String[] args){
             SpringApplication.run(AuthApp.class);
         }
     }
    

测试:

  1. Browser visit: GET http://localhost:5000/dear-auth/oauth/authorize?client_id=DearApp&response_type=code
    • 默认跳转到认证页面 http://localhost:5000/dear-auth/login,输入账户密码(admin,123)进行验证
    • 认证成功后,跳转回授权页面 http://localhost:5000/dear-auth/oauth/authorize?client_id=DearApp&response_type=code,选择是否同意授权
    • 同意授权后,则跳转到 redirect_uri+code https://www.baidu.com/?code=3Tgl8q,即获得授权码3Tgl8q
  2. 获取token

    • 使用cmd命令:

        > curl -d 'grant_type=authorization_code&code=3Tgl8q' -X POST http://DearApp:DearApp@localhost:5000/dear-auth/oauth/token -i
        HTTP/1.1 200
        Cache-Control: no-store
        Pragma: no-cache
        X-Content-Type-Options: nosniff
        X-XSS-Protection: 1; mode=block
        X-Frame-Options: DENY
        Content-Type: application/json;charset=UTF-8
        Transfer-Encoding: chunked
        Date: Thu, 21 Jan 2021 13:32:58 GMT
      
        {"access_token":"e775d74e-9e8c-437d-974d-6d8286b37396","token_type":"bearer","refresh_token":"6b9f3fa3-143b-47a0-b356-3f577e8c7fed","expires_in":43199,"scope":"app"}
      
    • 使用Postman
      • POST http://DearApp:DearApp@localhost:5000/dear-auth/oauth/token
      • Authorization -> Basic Auth -> Username(DearApp) & Password(DearApp)
      • Body -> x-www-form-urlencoded -> grant_type & code
    • 注意:
      • 授权码使用一次就失效了
      • 获取token的body参数 redirect_uri & scope可加可不加,但加了后必须和配置的match上,否则获取token失败
      • AuthorizationServerConfig配置了public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients(); },则表示可以使用表单认证,可不用Basic Auth,直接在body中再加入client_id & client_secret参数

认证服务: 基于JDBC存储数据库方式

  1. 数据库 https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

     /*
     SQLyog  v12.2.6 (64 bit)
     MySQL - 8.0.15 : Database - dear_v1_auth
     *********************************************************************
     */
    
     /*!40101 SET NAMES utf8 */;
    
     /*!40101 SET SQL_MODE=''*/;
    
     /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
     /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
     /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
     /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
     CREATE DATABASE /*!32312 IF NOT EXISTS*/`dear_v1_auth` /*!40100 DEFAULT CHARACTER SET utf8 */;
    
     USE `dear_v1_auth`;
    
     /*Table structure for table `clientdetails` */
    
     DROP TABLE IF EXISTS `clientdetails`;
    
     CREATE TABLE `clientdetails` (
       `appId` varchar(128) NOT NULL,
       `resourceIds` varchar(256) DEFAULT NULL,
       `appSecret` varchar(256) DEFAULT NULL,
       `scope` varchar(256) DEFAULT NULL,
       `grantTypes` varchar(256) DEFAULT NULL,
       `redirectUrl` varchar(256) DEFAULT NULL,
       `authorities` varchar(256) DEFAULT NULL,
       `access_token_validity` int(11) DEFAULT NULL,
       `refresh_token_validity` int(11) DEFAULT NULL,
       `additionalInformation` varchar(4096) DEFAULT NULL,
       `autoApproveScopes` varchar(256) DEFAULT NULL,
       PRIMARY KEY (`appId`)
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
     /*Table structure for table `oauth_access_token` */
    
     DROP TABLE IF EXISTS `oauth_access_token`;
    
     CREATE TABLE `oauth_access_token` (
       `token_id` varchar(256) DEFAULT NULL,
       `token` blob,
       `authentication_id` varchar(128) 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`)
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
     /*Table structure for table `oauth_approvals` */
    
     DROP TABLE IF EXISTS `oauth_approvals`;
    
     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 NULL DEFAULT NULL,
       `lastModifiedAt` timestamp NULL DEFAULT NULL
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
     /*Table structure for table `oauth_client_details` */
    
     DROP TABLE IF EXISTS `oauth_client_details`;
    
     CREATE TABLE `oauth_client_details` (
       `client_id` varchar(128) NOT NULL,
       `resource_ids` varchar(256) DEFAULT NULL,
       `client_secret` varchar(256) DEFAULT NULL,
       `scope` varchar(256) DEFAULT NULL,
       `authorized_grant_types` varchar(256) DEFAULT NULL,
       `web_server_redirect_uri` varchar(256) DEFAULT NULL,
       `authorities` varchar(256) DEFAULT NULL,
       `access_token_validity` int(11) DEFAULT NULL,
       `refresh_token_validity` int(11) DEFAULT NULL,
       `additional_information` varchar(4096) DEFAULT NULL,
       `autoapprove` varchar(256) DEFAULT NULL,
       PRIMARY KEY (`client_id`)
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
     /*Table structure for table `oauth_client_token` */
    
     DROP TABLE IF EXISTS `oauth_client_token`;
    
     CREATE TABLE `oauth_client_token` (
       `token_id` varchar(256) DEFAULT NULL,
       `token` blob,
       `authentication_id` varchar(128) NOT NULL,
       `user_name` varchar(256) DEFAULT NULL,
       `client_id` varchar(256) DEFAULT NULL,
       PRIMARY KEY (`authentication_id`)
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
     /*Table structure for table `oauth_code` */
    
     DROP TABLE IF EXISTS `oauth_code`;
    
     CREATE TABLE `oauth_code` (
       `code` varchar(256) DEFAULT NULL,
       `authentication` blob
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
     /*Table structure for table `oauth_refresh_token` */
    
     DROP TABLE IF EXISTS `oauth_refresh_token`;
    
     CREATE TABLE `oauth_refresh_token` (
       `token_id` varchar(256) DEFAULT NULL,
       `token` blob,
       `authentication` blob
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
     /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
     /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
     /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
     /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
    
  2. 插入client数据 table: oauth_client_details

    • client_id: DearApp
    • client_secret: $2a$10$i5enbghKJ1yqj9r7XuyJC.Rs0gGSTzjiv5vQvz/ShghJhjomHaqUa (DearApp加密和的字符串)
    • scope: app
    • authorized_grant_types: authorization_code,password,refresh_token
    • web_server_redirect_uri: http://www.baidu.com
  3. 配置数据源

    • pom.xml

         <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
      
        <!-- for @ConfigurationProperties : optional ! -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
      
    • application.yml
        spring:
           datasource:
             druid:
               url: jdbc:mysql://localhost:3306/dear_v1_auth?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowMultiQueries=true
               username: cj
               password: 123
               driver-class-name: com.mysql.cj.jdbc.Driver
         #      type: com.alibaba.druid.pool.DruidDataSource
               initial-size: 8
               min-idle: 1
               max-active: 20
               max-wait: 60000
               time-between-eviction-runsMillis: 60000
               min-evictable-idle-timeMillis: 300000
               validation-query: select 'x'
               test-while-idle: true
               test-on-borrow: false
               test-on-return: false
               pool-prepared-statements: false
               max-open-prepared-statements: 20
               max-pool-prepared-statement-per-connection-size: 20
               filters: stat,wall
               use-global-data-source-stat: true
               connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      
    • config/DruidConfig

        import com.alibaba.druid.pool.DruidDataSource;
        import org.springframework.boot.context.properties.ConfigurationProperties;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import javax.sql.DataSource;
      
        @Configuration
        public class DruidConfig {
            //https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
            @ConfigurationProperties(prefix = "spring.datasource.druid")
            @Bean
            public DataSource druidDataSource(){
                return new DruidDataSource();
            }
        }
      
  4. 配置授权认证服务器: client & token 存储到数据库

     @Configuration
     @EnableAuthorizationServer
     public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
         // 1. 配置 client
         @Override
         public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
             clients.withClientDetails(jdbcClientDetailsService());
         }
    
         @Autowired
         DataSource dataSource;
    
         @Bean
         public ClientDetailsService jdbcClientDetailsService(){
             return new JdbcClientDetailsService(dataSource);
         }
    
         // 2. 配置 endpoints,token存储
         @Override
         public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
             endpoints.tokenStore(jdbcTokenStore());
         }
    
         @Bean
         public TokenStore jdbcTokenStore(){
             return new JdbcTokenStore(dataSource);
         }
     }
    

RBAC

  1. 数据库 (https://github.com/topsale/spring-boot-samples/blob/master/spring-security-oauth2/spring-security-oauth2-server/db/rbac.sql)

     /*
     SQLyog  v12.2.6 (64 bit)
     MySQL - 8.0.15 : Database - dear_v1_auth
     *********************************************************************
     */
    
     /*!40101 SET NAMES utf8 */;
    
     /*!40101 SET SQL_MODE=''*/;
    
     /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
     /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
     /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
     /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
     CREATE DATABASE /*!32312 IF NOT EXISTS*/`dear_v1_auth` /*!40100 DEFAULT CHARACTER SET utf8 */;
    
     USE `dear_v1_auth`;
    
     /*Table structure for table `tb_permission` */
    
     DROP TABLE IF EXISTS `tb_permission`;
    
     CREATE TABLE `tb_permission` (
       `id` bigint(20) NOT NULL AUTO_INCREMENT,
       `parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
       `name` varchar(64) NOT NULL COMMENT '权限名称',
       `enname` varchar(64) NOT NULL COMMENT '权限英文名称',
       `url` varchar(255) NOT NULL COMMENT '授权路径',
       `description` varchar(200) DEFAULT NULL COMMENT '备注',
       `created` datetime NOT NULL,
       `updated` datetime NOT NULL,
       PRIMARY KEY (`id`)
     ) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8 COMMENT='权限表';
    
     /*Data for the table `tb_permission` */
    
     insert  into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values 
     (37,0,'系统管理','System','/',NULL,'2019-04-04 23:22:54','2019-04-04 23:22:56'),
     (38,37,'用户管理','SystemUser','/users/',NULL,'2019-04-04 23:25:31','2019-04-04 23:25:33'),
     (39,38,'查看用户','SystemUserView','/users/view/**',NULL,'2019-04-04 15:30:30','2019-04-04 15:30:43'),
     (40,38,'新增用户','SystemUserInsert','/users/insert/**',NULL,'2019-04-04 15:30:31','2019-04-04 15:30:44'),
     (41,38,'编辑用户','SystemUserUpdate','/users/update/**',NULL,'2019-04-04 15:30:32','2019-04-04 15:30:45'),
     (42,38,'删除用户','SystemUserDelete','/users/delete/**',NULL,'2019-04-04 15:30:48','2019-04-04 15:30:45'),
     (44,37,'内容管理','SystemContent','/contents/',NULL,'2019-04-06 18:23:58','2019-04-06 18:24:00'),
     (45,44,'查看内容','SystemContentView','/contents/view/**',NULL,'2019-04-06 23:49:39','2019-04-06 23:49:41'),
     (46,44,'新增内容','SystemContentInsert','/contents/insert/**',NULL,'2019-04-06 23:51:00','2019-04-06 23:51:02'),
     (47,44,'编辑内容','SystemContentUpdate','/contents/update/**',NULL,'2019-04-06 23:51:04','2019-04-06 23:51:06'),
     (48,44,'删除内容','SystemContentDelete','/contents/delete/**',NULL,'2019-04-06 23:51:08','2019-04-06 23:51:10');
    
     /*Table structure for table `tb_role` */
    
     DROP TABLE IF EXISTS `tb_role`;
    
     CREATE TABLE `tb_role` (
       `id` bigint(20) NOT NULL AUTO_INCREMENT,
       `parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
       `name` varchar(64) NOT NULL COMMENT '角色名称',
       `enname` varchar(64) NOT NULL COMMENT '角色英文名称',
       `description` varchar(200) DEFAULT NULL COMMENT '备注',
       `created` datetime NOT NULL,
       `updated` datetime NOT NULL,
       PRIMARY KEY (`id`)
     ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';
    
     /*Data for the table `tb_role` */
    
     insert  into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values 
     (37,0,'超级管理员','admin',NULL,'2019-04-04 23:22:03','2019-04-04 23:22:05');
    
     /*Table structure for table `tb_role_permission` */
    
     DROP TABLE IF EXISTS `tb_role_permission`;
    
     CREATE TABLE `tb_role_permission` (
       `id` bigint(20) NOT NULL AUTO_INCREMENT,
       `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
       `permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
       PRIMARY KEY (`id`)
     ) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8 COMMENT='角色权限表';
    
     /*Data for the table `tb_role_permission` */
    
     insert  into `tb_role_permission`(`id`,`role_id`,`permission_id`) values 
     (37,37,37),
     (38,37,38),
     (39,37,39),
     (40,37,40),
     (41,37,41),
     (42,37,42),
     (43,37,44),
     (44,37,45),
     (45,37,46),
     (46,37,47),
     (47,37,48);
    
     /*Table structure for table `tb_user` */
    
     DROP TABLE IF EXISTS `tb_user`;
    
     CREATE TABLE `tb_user` (
       `id` bigint(20) NOT NULL AUTO_INCREMENT,
       `username` varchar(50) NOT NULL COMMENT '用户名',
       `password` varchar(64) NOT NULL COMMENT '密码,加密存储',
       `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
       `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
       `created` datetime NOT NULL,
       `updated` datetime NOT NULL,
       PRIMARY KEY (`id`),
       UNIQUE KEY `username` (`username`) USING BTREE,
       UNIQUE KEY `phone` (`phone`) USING BTREE,
       UNIQUE KEY `email` (`email`) USING BTREE
     ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';
    
     /*Data for the table `tb_user` */
    
     insert  into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values 
     (37,'admin','$2a$10$9ZhDOBp.sRKat4l14ygu/.LscxrMUcDAfeVOEPiYwbcRkoB09gCmi','15888888888','lee.lusifer@gmail.com','2019-04-04 23:21:27','2019-04-04 23:21:29');
    
     /*Table structure for table `tb_user_role` */
    
     DROP TABLE IF EXISTS `tb_user_role`;
    
     CREATE TABLE `tb_user_role` (
       `id` bigint(20) NOT NULL AUTO_INCREMENT,
       `user_id` bigint(20) NOT NULL COMMENT '用户 ID',
       `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
       PRIMARY KEY (`id`)
     ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
    
     /*Data for the table `tb_user_role` */
    
     insert  into `tb_user_role`(`id`,`user_id`,`role_id`) values 
     (37,37,37);
    
     /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
     /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
     /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
     /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
    
  2. 配置MyBatis: application.yml,main上加@MapperScan("com.cj.dear.auth.mapper")注解

     mybatis:
        mapper-locations: classpath:mapper/*Mapper.xml
        config-location:  classpath:mybatis-config.xml
    
  3. 编写entity,mapper,service

  4. config/WebSecurityConfig.java

     @Configuration
     @EnableWebSecurity
     @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
     class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
         // 采用bcrypt对密码进行编码
         @Bean
         public PasswordEncoder passwordEncoder() {
             return new BCryptPasswordEncoder();
         }
    
          @Autowired
         UserDetailsServiceImpl userDetailsServiceImpl;
    
         @Override
         protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             auth.userDetailsService(userDetailsServiceImpl);
         }
     }
    
  5. UserDetailsServiceImpl.java

     @Service
     public class UserDetailsServiceImpl implements UserDetailsService {
    
         @Autowired
         private TbUserService tbUserService;
    
         @Autowired
         private TbPermissionService tbPermissionService;
    
         @Override
         public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
             TbUser tbUser = tbUserService.getByUsername(username);
             if(tbUser!=null){
                 List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
                 List<TbPermission> tbPermissions = tbPermissionService.listPermisionsByUserId(tbUser.getId());
                 if(tbPermissions!=null){
                     tbPermissions.forEach(tbPermission -> {
                         grantedAuthorityList.add(new SimpleGrantedAuthority(tbPermission.getEnname()));
                     });
                 }
                 return new User(tbUser.getUsername(),tbUser.getPassword(),grantedAuthorityList);
             }
             return null;
         }
     }
    

资源服务器

  1. pom.yml

     <!-- OAuth -->
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-oauth2</artifactId>
     </dependency>
    
  2. application.yml

     security:
       oauth2:
         client:
           client-id: DearApp
           client-secret: DearApp
           access-token-uri: http://localhost:5000/dear-auth/oauth/token
           user-authorization-uri: http://localhost:5000/dear-auth/authorize
         resource:
           token-info-uri: http://localhost:5000/dear-auth/oauth/check_token
    
  3. config/ResourceServerConfig

     @Configuration
     @EnableResourceServer
     @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
     public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
         @Override
         public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/").hasAuthority("SystemProject")
                    .antMatchers("/projects/view/**").hasAuthority("SystemProjectView")
                    ;
         }
     }
    
  4. ProjectController

     @RestController
     public class ProjectController {
    
         @GetMapping("/")
         public Object index(){
             return "Project Hello World";
         }
    
         @GetMapping("/projects/view")
         public Object listProjects(){
             return "List Projects";
         }
    
         @GetMapping("/projects/insert")
         public Object insertProject(){
             return "Insert Projects";
         }
     }
    
  5. visit http://localhost:5001/dear-project/?access_token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25 => Exception (springframework.web.client.HttpClientErrorException$Forbidden: 403 : [{"timestamp":"2021-01-22T15:01:31.342+00:00","status":403,"error":"Forbidden","message":"","path":"/dear-auth/oauth/check_token"}])

    • 方式一: dear-auth项目 config/WebSecurityConfig => visit http://localhost:5000/dear-auth/oauth/check_token?token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25 成功
        /// Sample 3: /oauth/check_token 401/403 配置这个或者在AuthorizationServerConfig中配置security.checkTokenAccess策略
        @Override
        public void configure(WebSecurity web) throws Exception {
           web.ignoring().antMatchers("/oauth/check_token");
        }
      
    • 放式二: dear-auth项目 config/AuthorizationServerConfig => visit http://localhost:5000/dear-auth/oauth/check_token?token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25 + Basic Auth 成功
        /// Sample 3: 此认证服务器的安全策略
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security.tokenKeyAccess("permitAll()") // /oauth/token_key 完全开放
                    .checkTokenAccess("isAuthenticated()") // /oauth/check_token 需要认证通过,可采用http basic认证
                    .allowFormAuthenticationForClients() // 允许表单认证
                    ;
        }
      
  6. test

    • visit http://localhost:5000/dear-auth/oauth/authorize?client_id=DearApp&response_type=code => 获取code:nfGdkg
    • POST http://localhost:5000/dear-auth/oauth/token => 获取access_token: 1e9aedd1-4e92-4444-96a7-edc3b55a3d25
    • visit http://localhost:5001/dear-project/ => unauthorized
    • visit http://localhost:5001/dear-project/?access_token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25 => Success!
    • visit http://localhost:5001/dear-project/ + Bearer Token => Success!
    • visit http://localhost:5000/dear-auth/oauth/check_token?token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25 =>
        {
            "active": true,
            "exp": 1611353145,
            "user_name": "admin",
            "authorities": [
                "SystemProjectView",
                "SystemProject",
                "SystemUserView",
                "SystemProjectInsert",
                "SystemProjectUpdate",
                "SystemUser",
                "SystemUserInsert",
                "SystemUserDelete",
                "SystemUserUpdate",
                "System",
                "SystemProjectDelete"
            ],
            "client_id": "DearApp",
            "scope": [
                "app"
            ]
        }
      

Test

GET http://localhost:5000/dear-auth/oauth/authorize?client_id=DearApp&response_type=code&scope=app&redirect_uri=http://www.baidu.com

POST http://localhost:5000/dear-auth/oauth/token

curl -d 'grant_type=authorization_code&code=3Tgl8q' -X POST http://DearApp:DearApp@localhost:5000/dear-auth/oauth/token -i

GET http://localhost:5001/dear-project/?access_token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25

GET http://DearApp:DearApp@localhost:5001/dear-project/?access_token=1e9aedd1-4e92-4444-96a7-edc3b55a3d25

ISSUE

SpringSecurityOAuth2登录后无法跳转获取授权码地址,直接跳转根路径原因详解 https://blog.csdn.net/CSDN877425287/article/details/110948221

SpringBoot集成SpringSecurity - 异常处理(三) https://www.jianshu.com/p/5b412418b864/

SpringBoot2.x统一异常捕获@RestControllerAdvice https://blog.csdn.net/fuu123f/article/details/107249708

Spring Security OAuth2 授权失败(401) 问题整理 https://www.cnblogs.com/mxmbk/p/9782409.html

SpringBoot /error Error Page status 为 999 的问题 https://learnku.com/java/t/39683

Spring Cloud OAuth2 实现用户认证及单点登录 https://www.cnblogs.com/fengzheng/p/11724625.html

官方 spring-security-oauth2 https://github.com/spring-projects/spring-security-oauth/tree/master/spring-security-oauth2

Demo https://github.com/topsale/spring-boot-samples