写点什么

OAuth2.0 授权码模式实战

用户头像
码农参上
关注
发布于: 2021 年 08 月 10 日

OAuth2.0 是目前比较流行的一种开源授权协议,可以用来授权第三方应用,允许在不将用户名和密码提供给第三方应用的情况下获取一定的用户资源,目前很多网站或 APP 基于微信或 QQ 的第三方登录方式都是基于 OAuth2 实现的。本文将基于 OAuth2 中的授权码模式,采用数据库配置方式,搭建认证服务器与资源服务器,完成授权与资源的访问。

流程分析

在 OAuth2 中,定义了 4 种不同的授权模式,其中授权码模式(authorization code)功能流程相对更加完善,也被更多的系统采用。首先使用图解的方式简单了解一下它的授权流程:



  • 对上面的流程进行一下说明:1、用户访问客户端

  • 2、客户端将用户导向认证服务器

  • 3、用户登录,并对第三方客户端进行授权

  • 4、认证服务器将用户导向客户端事先指定的重定向地址,并附上一个授权码

  • 5、客户端使用授权码,向认证服务器换取令牌

  • 6、认证服务器对客户端进行认证以后,发放令牌

  • 7、客户端使用令牌,向资源服务器申请获取资源

  • 8、资源服务器确认令牌,向客户端开放资源


在对授权码模式的流程有了一定基础的情况下,我们开始动手搭建项目。

项目搭建

准备工作

1、在 Project 中创建两个 module,采用认证服务器和资源服务器分离的架构:



2、spring-security-oauth2是对Oauth2协议规范的一种实现,这里可以直接使用spring-cloud-starter-oauth2,就不需要分别引入spring-securityoauth2了。在父 pom 中引入:


<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-web</artifactId></dependency>
复制代码


3、数据库建表,OAuth2需要的表结构如下:


DROP TABLE IF EXISTS `oauth_access_token`;CREATE TABLE `oauth_access_token`  (  `token_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,  `token` blob NULL,  `authentication_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,  `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,  `authentication` blob NULL,  `refresh_token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `oauth_client_details`;CREATE TABLE `oauth_client_details` ( `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorities` varchar(255) 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, `additional_information` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL, `autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'false', PRIMARY KEY (`client_id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `oauth_code`;CREATE TABLE `oauth_code` ( `code` varchar(255) 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;
DROP TABLE IF EXISTS `oauth_refresh_token`;CREATE TABLE `oauth_refresh_token` ( `token_id` varchar(255) 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;
复制代码


  • oauth_access_token:存储生成的 access_token,由类JdbcTokenStore操作

  • oauth_client_details:存储客户端的配置信息,由类JdbcClientDetailsService操作

  • oauth_code:存储服务端系统生成的 code 的值,由类JdbcAuthorizationCodeServices操作

  • oauth_refresh_token:存储刷新令牌的 refresh_token,如果客户端的 grant_type 不支持 refresh_token,那么不会用到这张表,同样由类JdbcTokenStore操作


其余spring security相关的用户表、角色表以及权限表的表结构在这里省略,可以在文末的 Git 地址中下载。

认证服务器

认证服务器是服务提供者专门用来处理认证授权的服务器,主要负责获取用户授权并颁发token,以及完成后续的token认证工作。认证部分功能主要由spring security 负责,授权则由oauth2负责。


1、开启Spring Security配置


@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {    @Bean    public BCryptPasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }}
复制代码


通过@Configuration@EnableWebSecurity 开启Spring Security配置,继承WebSecurityConfigurerAdapter的方法,实现个性化配置。如果使用内存配置用户,可以重写其中的configure方法进行配置,由于我们使用数据库中的用户信息,所以不需要在这里进行配置。并且采用认证服务器和资源服务器分离,也不需要在这里对服务资源进行权限的配置。


在类中创建了两个Bean,分别是用于处理认证请求的认证管理器AuthenticationManager,以及配置全局统一使用的密码加密方式BCryptPasswordEncoder,它们会在认证服务中被使用。


2、开启并配置认证服务器


@Configuration@EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {    @Autowired    private AuthenticationManager authenticationManager;    //认证管理器    @Autowired    private BCryptPasswordEncoder passwordEncoder;//密码加密方式    @Autowired    private DataSource dataSource;  // 注入数据源    @Autowired    private UserDetailsService userDetailsService; //自定义用户身份认证
@Bean public ClientDetailsService jdbcClientDetailsService(){ //将client信息存储在数据库中 return new JdbcClientDetailsService(dataSource); }
@Bean public TokenStore tokenStore(){ //对token进行持久化存储在数据库中,数据存储在oauth_access_token和oauth_refresh_token return new JdbcTokenStore(dataSource); }
@Bean public AuthorizationCodeServices authorizationCodeServices() { //加入对授权码模式的支持 return new JdbcAuthorizationCodeServices(dataSource); }
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //设置客户端的配置从数据库中读取,存储在oauth_client_details表 clients.withClientDetails(jdbcClientDetailsService()); }
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore())//token存储方式 .authenticationManager(authenticationManager)// 开启密码验证,来源于 WebSecurityConfigurerAdapter .userDetailsService(userDetailsService)// 读取验证用户的信息 .authorizationCodeServices(authorizationCodeServices()) .setClientDetailsService(jdbcClientDetailsService()); }
@Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { // 配置Endpoint,允许请求,不被Spring-security拦截 security.tokenKeyAccess("permitAll()") // 开启/oauth/token_key 验证端口无权限访问 .checkTokenAccess("isAuthenticated()") // 开启/oauth/check_token 验证端口认证权限访问 .allowFormAuthenticationForClients()// 允许表单认证 .passwordEncoder(passwordEncoder); // 配置BCrypt加密 }}
复制代码


在类中,通过@EnableAuthorizationServer 注解开启认证服务,通过继承父类AuthorizationServerConfigurerAdapter,对以下信息进行了配置:


  • ClientDetailsServiceConfigurer:配置客户端服务,这里我们通过JdbcClientDetailsService从数据库读取相应的客户端配置信息,进入源码可以看到客户端信息是从表oauth_client_details中拉取。

  • AuthorizationServerEndpointsConfigurer:用来配置授权(authorization)以及令牌(token)的访问端点,以及令牌服务的配置信息。该类作为一个装载类,装载了Endpoints所有的相关配置。

  • AuthorizationServerSecurityConfigurer:配置令牌端点(endpoint)的安全约束,OAuth2开放了端点用于检查令牌,/oauth/check_token/oauth/token_key这些端点默认受到保护,在这里配置可被外部调用。


3、采用从数据库中获取用户信息的方式进行身份验证


@Servicepublic class UserDetailServiceImpl implements UserDetailsService {    @Autowired    private TbUserService userService;    @Autowired    private TbPermissionService permissionService;
@Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { TbUser tbUser = userService.getUserByUserName(userName); if (tbUser==null){ throw new UsernameNotFoundException("username : "+userName+" is not exist"); }
List<GrantedAuthority> authorities=new ArrayList<>(); //获取用户权限 List<TbPermission> permissions = permissionService.getByUserId(tbUser.getId()); permissions.forEach(permission->{ authorities.add(new SimpleGrantedAuthority(permission.getEname())); }); return new User(tbUser.getUsername(),tbUser.getPassword(),authorities); }}
复制代码


创建UserDetailServiceImpl 实现UserDetailsService接口,并实现loadUserByUsername方法,根据用户名从数据库查询用户信息及权限。


4、启动服务


首先发起请求获取授权码(code),直接访问下面的url


http://localhost:9004/oauth/authorize?client_id=client1&redirect_uri=http://localhost:8848/nacos&response_type=code&scope=select
复制代码


看一下各个参数的意义:


client_id:因为认证服务器要知道是哪一个应用在请求授权,所以client_id就是认证服务器给每个应用分配的id


redirect_uri:重定向地址,会在这个重定向地址后面附加授权码,让第三方应用获取code


response_typecode表明采用授权码认证模式


scope:需要获得哪些授权,这个参数的值是由服务提供商定义的,不能随意填写


首先会重定向到登录验证页面,因为之前的url中只明确了第三方应用的身份,这里要确定第三方应用要请求哪一个用户的授权。输入数据库表tb_user中配置的用户信息 admin/123456



注意url中请求的参数必须和在数据库中的表oauth_client中配置的相同,如果不存在或信息不一致都会报错,在参数填写错误时会产生如下报错信息:



如果参数完全匹配,会请求用户向请求资源的客户端client授权:



点击Authorize同意授权,会跳转到redirect_uri定义的重定向地址,并在 url 后面附上授权码code



这样,用户的登录和授权的操作都在浏览器中完成了,接下来我们需要获取令牌,发送 post 请求到/oauth/token接口,使用授权码获取access_token。在发送请求时,需要在请求头中包含clientIdclientSecret,并且携带参数 grant_typecoderedirect_uri,这里会对redirect_uri做二次验证:



这样,就通过/oauth/token端点获取到了access_token,并一同拿到了它的令牌类型、过期时间、授权范围信息,这个令牌将在请求资源服务器的资源时被使用。

资源服务器

资源服务器简单来说就是资源的访问入口,主要负责处理用户数据的api调用,资源服务器中存储了用户数据,并对外提供http服务,可以将用户数据返回给经过身份验证的客户端。资源服务器和认证服务器可以部署在一起,也可以分离部署,我们这里采用分开部署的形式。


1、配置资源服务器


@Configuration@EnableResourceServerpublic class ResourceConfig extends ResourceServerConfigurerAdapter {    @Bean    public BCryptPasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }
@Bean @Primary public RemoteTokenServices remoteTokenServices(){ final RemoteTokenServices tokenServices=new RemoteTokenServices(); //设置授权服务器check_token Endpoint 完整地址 tokenServices.setCheckTokenEndpointUrl("http://localhost:9004/oauth/check_token"); //设置客户端id与secret,注意:client_secret 值不能使用passwordEncoder加密 tokenServices.setClientId("client1"); tokenServices.setClientSecret("client-secret"); return tokenServices; }
@Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); http.authorizeRequests() .anyRequest().authenticated(); }
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("oauth2").stateless(true); }}
复制代码


在类中主要实现了以下功能:


  • @EnableResourceServer注解表明开启OAuth2资源服务器,在请求资源服务器的请求前,需要通过认证服务器获取access_token令牌,然后在访问资源服务器中的资源时需要携带令牌才能正常进行请求

  • 通过RemoteTokenServices实现自定义认证服务器,这里配置了我们之前创建的认证服务器

  • 重写configure(HttpSecurity http)方法,开启所有请求需要授权才可以访问

  • 配置资源相关设置configure(ResourceServerSecurityConfigurer resources),这里只设置resourceId,作为该服务资源的唯一标识


2、测试接口,负责提供用户信息


@RestControllerpublic class TestController {    @GetMapping("/user/{name}")    public User user(@PathVariable String name){        return new User(name, 20);    }}
复制代码


3、启动服务


不携带access_token,直接访问接口http://127.0.0.1:9005/user/hydra:



使用 Postman,在Authorization中配置使用Bearer Token,并填入从认证服务器获取的access_token(或在Headers中的Authorization字段直接填写Bearer 'access_token'),再次访问接口,可以正常访问接口资源:



项目Git地址:


https://github.com/trunks2008/oauth2


如果文章对您有所帮助,欢迎关注公众号 码农参上


发布于: 2021 年 08 月 10 日阅读数: 9
用户头像

码农参上

关注

公众号:码农参上 2021.03.30 加入

还未添加个人简介

评论

发布
暂无评论
OAuth2.0授权码模式实战