项目服务接口设计_Spring Boot Security 整合 OAuth2 设计安全API接口服务

8c864f00dab77d20d1e04d73dd4aa2eb.png

简介

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.ymljava|____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-securityorg.springframework.boot    spring-boot-starter-thymeleaforg.springframework.boot    spring-boot-starter-oauth2-clientorg.springframework.boot    spring-boot-starter-oauth2-resource-serverorg.springframework.security.oauth.boot    spring-security-oauth2-autoconfigure    2.1.3.RELEASE
SecurityConfig

支持password模式要配置AuthenticationManager

@Configuration@EnableWebSecuritypublic 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@EnableAuthorizationServerpublic 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@EnableResourceServerpublic 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 , granttype , clientid , 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"}

客户端授权模式

[ 客户端模式需要参数:granttype , clientid , 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

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

5dc303cecfbc66a187cd1fc2a9baa553.png

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

eeb8ce8eabfd0ff095ea231e3553e140.png

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

8f0c00643a1cc7ced781fcd3ab191e43.png

通过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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值