• Spring Boot Security 整合 OAuth2 设计安全API接口服务


    简介

    OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。本文重点讲解Spring Boot项目对OAuth2进行的实现,如果你对OAuth2不是很了解,你可以先理解 OAuth 2.0 - 阮一峰,这是一篇对于oauth2很好的科普文章。

    OAuth2概述

    oauth2根据使用场景不同,分成了4种模式

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

    在项目中我们通常使用授权码模式,也是四种模式中最复杂的,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。

    Oauth2授权主要由两部分组成:

    • Authorization server:认证服务
    • Resource server:资源服务

    在实际项目中以上两个服务可以在一个服务器上,也可以分开部署。下面结合spring boot来说明如何使用。

    快速上手

    之前的文章已经对 Spring Security 进行了讲解,这一节对涉及到 Spring Security 的配置不详细讲解。若不了解 Spring Security 先移步到 Spring Boot Security 详解

    建表

    客户端信息可以存储在内存、redis和数据库。在实际项目中通常使用redis和数据库存储。本文采用数据库。Spring 0Auth2 己经设计好了数据库的表,且不可变。表及字段说明参照:Oauth2数据库表说明

    创建0Auth2数据库的脚本如下:

    DROP TABLE IF EXISTS `clientdetails`;
    DROP TABLE IF EXISTS `oauth_access_token`;
    DROP TABLE IF EXISTS `oauth_approvals`;
    DROP TABLE IF EXISTS `oauth_client_details`;
    DROP TABLE IF EXISTS `oauth_client_token`;
    DROP TABLE IF EXISTS `oauth_refresh_token`;
    
    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;
    
    
    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;
    
    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` datetime DEFAULT NULL,
      `lastModifiedAt` datetime DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    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;
    
    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;
    
    DROP TABLE IF EXISTS `oauth_code`;
    CREATE TABLE `oauth_code` (
      `code` varchar(256) DEFAULT NULL,
      `authentication` blob
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `oauth_refresh_token` (
      `token_id` varchar(256) DEFAULT NULL,
      `token` blob,
      `authentication` blob
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    

    为了测试方便,我们先插入一条客户端信息。

    INSERT INTO `oauth_client_details` VALUES ('dev', '', 'dev', 'app', 'password,client_credentials,authorization_code,refresh_token', 'http://www.baidu.com', '', 3600, 3600, '{"country":"CN","country_code":"086"}', 'false');
    

    用户、权限、角色用到的表如下:

    DROP TABLE IF EXISTS `user`;
    DROP TABLE IF EXISTS `role`;
    DROP TABLE IF EXISTS `user_role`;
    DROP TABLE IF EXISTS `role_permission`;
    DROP TABLE IF EXISTS `permission`;
    
    CREATE TABLE `user` (
    `id` bigint(11) NOT NULL AUTO_INCREMENT,
    `username` varchar(255) NOT NULL,
    `password` varchar(255) NOT NULL,
    PRIMARY KEY (`id`) 
    );
    CREATE TABLE `role` (
    `id` bigint(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(255) NOT NULL,
    PRIMARY KEY (`id`) 
    );
    CREATE TABLE `user_role` (
    `user_id` bigint(11) NOT NULL,
    `role_id` bigint(11) NOT NULL
    );
    CREATE TABLE `role_permission` (
    `role_id` bigint(11) NOT NULL,
    `permission_id` bigint(11) NOT NULL
    );
    CREATE TABLE `permission` (
    `id` bigint(11) NOT NULL AUTO_INCREMENT,
    `url` varchar(255) NOT NULL,
    `name` varchar(255) NOT NULL,
    `description` varchar(255) NULL,
    `pid` bigint(11) NOT NULL,
    PRIMARY KEY (`id`) 
    );
    
    INSERT INTO user (id, username, password) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e'); 
    INSERT INTO user (id, username , password) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e'); 
    INSERT INTO role (id, name) VALUES (1,'USER');
    INSERT INTO role (id, name) VALUES (2,'ADMIN');
    INSERT INTO permission (id, url, name, pid) VALUES (1,'/**','',0);
    INSERT INTO permission (id, url, name, pid) VALUES (2,'/**','',0);
    INSERT INTO user_role (user_id, role_id) VALUES (1, 1);
    INSERT INTO user_role (user_id, role_id) VALUES (2, 2);
    INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1);
    INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
    

    项目结构

    resources
    |____templates
    | |____login.html
    | |____application.yml
    java
    |____com
    | |____gf
    | | |____SpringbootSecurityApplication.java
    | | |____config
    | | | |____SecurityConfig.java
    | | | |____MyFilterSecurityInterceptor.java
    | | | |____MyInvocationSecurityMetadataSourceService.java
    | | | |____ResourceServerConfig.java
    | | | |____WebResponseExceptionTranslateConfig.java
    | | | |____AuthorizationServerConfiguration.java
    | | | |____MyAccessDecisionManager.java
    | | |____entity
    | | | |____User.java
    | | | |____RolePermisson.java
    | | | |____Role.java
    | | |____mapper
    | | | |____PermissionMapper.java
    | | | |____UserMapper.java
    | | | |____RoleMapper.java
    | | |____controller
    | | | |____HelloController.java
    | | | |____MainController.java
    | | |____service
    | | | |____MyUserDetailsService.java
    

    关键代码

    pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.3.RELEASE</version>
    </dependency>
    

    SecurityConfig

    支持password模式要配置AuthenticationManager

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private MyUserDetailsService userService;
    
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
            //校验用户
            auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() {
                //对密码进行加密
                @Override
                public String encode(CharSequence charSequence) {
                    System.out.println(charSequence.toString());
                    return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
                }
                //对密码进行判断匹配
                @Override
                public boolean matches(CharSequence charSequence, String s) {
                    String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
                    boolean res = s.equals( encode );
                    return res;
                }
            } );
    
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
            http.requestMatchers()
                    .antMatchers("/oauth/**","/login","/login-error")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/oauth/**").authenticated()
                    .and()
                    .formLogin().loginPage( "/login" ).failureUrl( "/login-error" );
        }
    
    
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception{
            return super.authenticationManager();
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new PasswordEncoder() {
                @Override
                public String encode(CharSequence charSequence) {
                    return charSequence.toString();
                }
    
                @Override
                public boolean matches(CharSequence charSequence, String s) {
                    return Objects.equals(charSequence.toString(),s);
                }
            };
        }
    
    
    }
    

    AuthorizationServerConfiguration 认证服务器配置

    /**
     * 认证服务器配置
     */
    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    
    
        /**
         * 注入权限验证控制器 来支持 password grant type
         */
        @Autowired
        private AuthenticationManager authenticationManager;
    
        /**
         * 注入userDetailsService,开启refresh_token需要用到
         */
        @Autowired
        private MyUserDetailsService userDetailsService;
    
        /**
         * 数据源
         */
        @Autowired
        private DataSource dataSource;
    
        /**
         * 设置保存token的方式,一共有五种,这里采用数据库的方式
         */
        @Autowired
        private TokenStore tokenStore;
    
        @Autowired
        private WebResponseExceptionTranslator webResponseExceptionTranslator;
    
        @Bean
        public TokenStore tokenStore() {
            return new JdbcTokenStore( dataSource );
        }
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            /**
             * 配置oauth2服务跨域
             */
            CorsConfigurationSource source = new CorsConfigurationSource() {
                @Override
                public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                    CorsConfiguration corsConfiguration = new CorsConfiguration();
                    corsConfiguration.addAllowedHeader("*");
                    corsConfiguration.addAllowedOrigin(request.getHeader( HttpHeaders.ORIGIN));
                    corsConfiguration.addAllowedMethod("*");
                    corsConfiguration.setAllowCredentials(true);
                    corsConfiguration.setMaxAge(3600L);
                    return corsConfiguration;
                }
            };
    
            security.tokenKeyAccess("permitAll()")
                    .checkTokenAccess("permitAll()")
                    .allowFormAuthenticationForClients()
                    .addTokenEndpointAuthenticationFilter(new CorsFilter(source));
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.jdbc(dataSource);
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            //开启密码授权类型
            endpoints.authenticationManager(authenticationManager);
            //配置token存储方式
            endpoints.tokenStore(tokenStore);
            //自定义登录或者鉴权失败时的返回信息
            endpoints.exceptionTranslator(webResponseExceptionTranslator);
            //要使用refresh_token的话,需要额外配置userDetailsService
            endpoints.userDetailsService( userDetailsService );
    
        }
    
    
    }
    

    ResourceServerConfig 资源服务器配置

    /**
     * 资源提供端的配置
     */
    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
       /**
         * 这里设置需要token验证的url
         * 可以在WebSecurityConfigurerAdapter中排除掉,
         * 对于相同的url,如果二者都配置了验证
         * 则优先进入ResourceServerConfigurerAdapter,进行token验证。而不会进行
         * WebSecurityConfigurerAdapter 的 basic auth或表单认证。
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.requestMatchers().antMatchers("/hi")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/hi").authenticated();
        }
    
    
    }
    

    关键代码就是这些,其他类代码参照后面提供的源码地址。

    验证

    密码授权模式

    [ 密码模式需要参数:username , password , grant_type , client_id , client_secret ]

    请求token

    curl -X POST -d "username=admin&password=123456&grant_type=password&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
    

    返回

    {
    	"access_token": "d94ec0aa-47ee-4578-b4a0-8cf47f0e8639",
    	"token_type": "bearer",
    	"refresh_token": "23503bc7-4494-4795-a047-98db75053374",
    	"expires_in": 3475,
    	"scope": "app"
    }
    

    不携带token访问资源,

    curl http://localhost:8080/hi?name=zhangsan
    

    返回提示未授权

    {
    	"error": "unauthorized",
    	"error_description": "Full authentication is required to access this resource"
    }
    

    携带token访问资源

    curl http://localhost:8080/hi?name=zhangsan&access_token=164471f7-6fc6-4890-b5d2-eb43bda3328a
    

    返回正确

    hi , zhangsan
    

    刷新token

    curl  -X POST -d 'grant_type=refresh_token&refresh_token=23503bc7-4494-4795-a047-98db75053374&client_id=dev&client_secret=dev' http://localhost:8080/oauth/token
    

    返回

    {
        "access_token": "ef53eb01-eb9b-46d8-bd58-7a0f9f44e30b",
        "token_type": "bearer",
        "refresh_token": "23503bc7-4494-4795-a047-98db75053374",
        "expires_in": 3599,
        "scope": "app"
    }
    

    客户端授权模式

    [ 客户端模式需要参数:grant_type , client_id , client_secret ]

    请求token

    curl -X POST -d "grant_type=client_credentials&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
    

    返回

    {
    	"access_token": "a7be47b3-9dc8-473e-967a-c7267682dc66",
    	"token_type": "bearer",
    	"expires_in": 3564,
    	"scope": "app"
    }
    

    授权码模式

    获取code

    浏览器中访问如下地址:

    http://localhost:8080/oauth/authorize?response_type=code&client_id=dev&redirect_uri=http://www.baidu.com
    

    跳转到登录页面,输入账号和密码进行认证:

    认证后会跳转到授权确认页面(oauth_client_details 表中 “autoapprove” 字段设置为true 时,不会出授权确认页面):

    确认后,会跳转到百度,并且地址栏中会带上我们想得到的code参数:

    通过code换token

    curl -X POST -d "grant_type=authorization_code&code=qS03iu&client_id=dev&client_secret=dev&redirect_uri=http://www.baidu.com" http://localhost:8080/oauth/token
    

    返回

    {
        "access_token": "90a246fa-a9ee-4117-8401-ca9c869c5be9",
        "token_type": "bearer",
        "refresh_token": "23503bc7-4494-4795-a047-98db75053374",
        "expires_in": 3319,
        "scope": "app"
    }
    

    参考

    https://segmentfault.com/a/1190000012260914

    https://stackoverflow.com/questions/28537181/spring-security-oauth2-which-decides-security

    源码

    https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-security-oauth2





    欢迎扫码或微信搜索公众号《程序员果果》关注我,关注有惊喜~

  • 相关阅读:
    大数据开发速查表
    静态图像介绍
    get请求如何传递数组参数
    Redis 6.0 新增功能
    JVM 出现 StackOverflowError和OutOfMemoryError的可能
    golang超级mapper包
    dotnet vs java vs go
    [翻译]Jupyter notebook .NET Core 内核预览1
    .NET Core到底有多强?
    .net core 运行时事件(Runtime Events)
  • 原文地址:https://www.cnblogs.com/huanchupkblog/p/10598166.html
Copyright © 2020-2023  润新知