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

358 阅读6分钟

OAuth2概述 oauth2根据使用场景不同,分成了4种模式 授权码模式(authorization code) 简化模式(implicit) 密码模式(resource owner password credentials) 客户端模式(client credentials) 在项目中我们通常使用授权码模式,也是四种模式中最复杂的,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。 Oauth2授权主要由两部分组成: Authorization server:认证服务 Resource server:资源服务 在实际项目中以上两个服务可以在一个服务器上,也可以分开部署。下面结合spring boot来说明如何使用。

建表

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

创建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', '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 org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-oauth2-client org.springframework.boot spring-boot-starter-oauth2-resource-server org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure 2.1.3.RELEASE 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
    • 这些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换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" }