• SpringBoot OAuth2.0认证管理流程详解


    OAuth2.0是一个开放的授权协议,可以用来实现第三方应用对我们API的访问控制,OAuth2.0中定义的角色包括:

    1. Resource Owner(资源拥有者)

    2. Client (第三方接入平台,请求者)

    3. Resource Server (服务器资源: 数据中心)

    4. Authorization Server (认证服务器)

    授权认证的基本流程包括: 首先Client需要向Resource Owner申请授权凭据,Resource Owner同意之后会发放**授权凭据(code码)给Client,Client然后会将授权凭据和身份信息(AppId)一起发给Authorization Server申请访问令牌,Authorization Server在验证身份信息无误之后,发送访问令牌(Access Token)**给Client,Client最后根据携带Access Token去访问Resource Server中受保护的API资源。

    OAuth2.0的认证模式包括四种:授权码模式密码模式简化模式客户端模式。其中授权码模式是功能最完善,流程最严密的授权模式,其基本流程图如下:

    1. 用户访问客户端,被导向认证服务器

    2. 用户给予客户端授权,认证服务器将用户导向客户端事先指定的重定向URL,并附上一个授权码

    3. 客户端收到授权码,附上早先的URL,向认证服务器申请令牌

    4. 认证服务器核对了授权码和URL,确认无误之后,向客户端发送访问令牌(access token)和更新令牌(refresh token)

    SpringBoot对OAuth2.0进行了很好的支持,下面详细说明下SpringBoot中OAuth2.0的基本配置和流程。

    首先搭建oauth server和oauth client的框架

    在oauth server中首先定义认证服务器:

    @Configuration
    @EnableAuthorizationServer
    public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Resource
        private DataSource dataSource;
    
        
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            super.configure(security);
        }
    
        /**
         * 配置ClientDetailsService,这里使用jdbc模式 
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.jdbc(dataSource);
        }
    
        
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            super.configure(endpoints);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    这里我们配置从数据库中读取第三方应用的配置,数据表如下:

    CREATE TABLE `oauth_client_details` (
      `client_id` varchar(48) 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    接着配置资源服务器:

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.requestMatchers().antMatchers("/user/**") //对/user的路径进行保护
                    .and()
                    .authorizeRequests()
                    .anyRequest().authenticated();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    然后配置WebSecurity

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserServiceImpl userService;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/oauth/**","/login/**", "/logout").permitAll()
                    .anyRequest().authenticated()   // 其他地址的访问均需验证权限
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .and()
                    .logout().logoutSuccessUrl("/");
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/assets/**");
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    最后定义UserServiceImpl类,主要是根据用户名查询用户信息,为了简便,在内存中实现定义了2个用户,直接在内存中查询,实际项目中可以把用户信息存储到MySql或者Redis中:

    @Service
    public class UserServiceImpl implements UserDetailsService {
    
        private SysRole admin = new SysRole("ADMIN", "管理员");
    
        private SysRole developer = new SysRole("DEVELOPER", "开发者");
    
        {
            SysPermission p1 = new SysPermission();
            p1.setCode("orderInfo");
            p1.setName("订单信息");
            p1.setUrl("/order/info");
    
            SysPermission p2 = new SysPermission();
            p2.setCode("orderDetail");
            p2.setName("订单详情");
            p2.setUrl("/order/detail");
    
            admin.setPermissionList(Arrays.asList(p1, p2));
            developer.setPermissionList(Arrays.asList(p1));
        }
    
        private List userList;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @PostConstruct
        public void initData() {
            String password = passwordEncoder.encode("123456");
            userList = new ArrayList<>();
    
            SysUser user1 = new SysUser("admin", password);
            user1.setRoleList(Arrays.asList(admin));
    
            SysUser user2 = new SysUser("test", password);
            user2.setRoleList(Arrays.asList(developer));
    
            userList.add(user1);
            userList.add(user2);
        }
    
        @Override
        public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
            Optional sysUserOptional = userList.stream().filter(item -> item.getUsername().equals(userName))
                    .findFirst();
            if (!sysUserOptional.isPresent()) {
                throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
            }
            SysUser sysUser = sysUserOptional.get();
            List authorities = new ArrayList<>();
            for (SysRole role : sysUser.getRoleList()) {
                for (SysPermission permission : role.getPermissionList()) {
                    authorities.add(new SimpleGrantedAuthority(permission.getCode()));
                }
            }
    
            return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    接下来搭建oauth-client框架,首先定义SecurityConfig类,对客户端的API进行了访问限制,注解@EnabelOAuthSsso开启了sso单点登录

    @EnableOAuth2Sso
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class UiSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/**")
                    .authorizeRequests()
                    .antMatchers("/", "/login**").permitAll()
                    .anyRequest()
                    .authenticated();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    创建一个RestApi,对/order/info路径进行了权限校验

    @RestController
    @RequestMapping("/order")
    public class WebController {
    
        @PreAuthorize("hasAuthority('orderInfo')")
        @RequestMapping("/info")
        public String info() {
            return "order-service";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在配置文件中增加如下配置:

    security:
      oauth2:
        client:
          client-id: MemberSystem
          client-secret: 12345
          access-token-uri: http://localhost:8202/oauth/token            #获取token地址
          user-authorization-uri: http://localhost:8202/oauth/authorize  #认证地址
        resource:
          user-info-uri: http://localhost:8202/user/me    #获取当前用户信息地址
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    下面来分析下授权认证的基本流程:

    访问localhost:8101/order/info会自动调整到localhost:8202/login页面:

    Spring通过FilterChainProxy来管理各种Filter,通过循环调用各种的Filter的doFilter方法来实现过滤,其调用的最后一个Filter是FilterSecurityInterceptor:

    在该Filter中尝试获取token,获取失败,此处抛出异常,该异常将在ExceptionTranslationFilter类中进行处理:

    整体的调用链为:

    ExceptionTranslationFilter.handleSpringSecurityException? -->?
    ExceptionTranslationFilter.sendStartAuthentication --> 
    LoginUrlAuthenticationEntryPoint.commence -->
    DefaultRedirectStrategy.sendRedirect
    
    • 1
    • 2
    • 3
    • 4

    最终重定向到localhost:8101/login

    然后开始过滤localhost:8101/login,依然需要通过各种Filter来过滤,这次过滤匹配到的Filter是OAuth2ClientAuthenticationProcessingFilter,首先进入该方法获取token:

    顺着这个方法一直debug进去:

    此处抛出异常,并且将当前请求重定向到http://localhost:8202/oauth/token,即配置文件中定义的access-token-uri,抛出的异常在OAuth2ClientContextFilter中处理,在redirectUser方法中进行重定向,整体的调用链为:

    OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication -->
    OAuth2RestTemplate.getAccessToken --> OAuth2RestTemplate.acquireAccessToken -->
    AuthorizationCodeAccessTokenProvider.obtainAccessToken -->
    AuthorizationCodeAccessTokenProvider.getRedirectForAuthorization-->
    OAuth2ClientContextFilter.doFilter --> OAuth2ClientContextFilter.redirectUser
    
    • 1
    • 2
    • 3
    • 4
    • 5

    最终重定向到http://localhost:8202/oauth/authorize?client_id=MemberSystem&redirect_uri=http://localhost:8101/login&response_type=code&state=4mnKEY

    其中的参数说明:

    1)response_type:表示授权类型,必选项,此处的值固定为"code"

    2)client_id:表示客户端的ID,必选项

    1. redirect_uri:表示重定向URI,可选项,此处为localhost:8101/login,即认证完成之后会重定向到这个地址

    4)state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值

    到这里,客户端开始调用服务器端的/oauth/authorize接口,服务端在收到这个接口的请求之后,也会经历多个Filter的过滤,最终在FilterSecurityInterceptor.invoke方法中抛出异常,异常的处理在ExceptionTranslationFilter.handleSpringSecurityException中进行,这部分流程与前述相同,最终重定向到localhost:8202/login,由于在oauth-server的配置中指定了访问/login请求不需要验证权限,故可以直接访问到login页面

    至此,用户被重定向到localhost:8202/login的登录页面,我们在页面中输入用户名密码,点击登录,重新向oauth-server发送login登录请求,oauth-server在UsernamePasswordAuthenticationFilter类中处理该请求,入口为attemptAuthentication,请求调用链为:

    UsernamePasswordAuthenticationFilter.attemptAuthentication -->
    ProviderManager.authenticate --> 
    AbstractUserDetailsAuthenticationProvider.authenticate -->
    DaoAuthenticationProvider.retrieveUser -->
    UserServiceImpl.loadUserByUsername
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在我们自定义的方法中查找用户信息,若找到对应的用户,则最终程序会执行到SavedRequestAwareAuthenticationSuccessHandler.onAuthenticationSuccess方法中

    这里又会重定向到之前的/oauth/authorize地址:

    接着再来看下/oauth/authorize返回的结果:

    返回结果中包含state和code两个参数,state和请求中的参数一致,code表示授权码,客户端只能使用该授权码一次,该返回结果又将请求重定向到localhost:8101/login

    客户端开始执行/login请求,与前述执行/login的流程类型,由于本次请求携带了state和code参数,故请求调用链如下:

    OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication -->
    OAuth2RestTemplate.getAccessToken --> OAuth2RestTemplate.acquireAccessToken -->
    AuthorizationCodeAccessTokenProvider.obtainAccessToken -->
    AuthorizationCodeAccessTokenProvider.retrieveToken
    
    • 1
    • 2
    • 3
    • 4

    在下面的方法中获取token:

    看下请求token的参数: grant_type为authorization_code,code为前面获得的授权码,redirect_uri为http://localhost:8101/login

    获取到access-token之后,还需要获取登录用户信息:

    我们debug到loadAuthentication方法内部:

    其中userInfoEndpointUrl即配置文件中的user-info-uri,返回的用户信息中包含了权限信息,当前登录的用户具备orderDetail和orderInfo两个权限。

    获取到token和登录用户信息之后,再次重定向到localhost:8101/order/info,即最开始访问的地址,此时还有最后一个步骤,即校验用户是否有访问该api接口的权限:

    由于接口定义了注解@PreAuthorize(“hasAuthority(‘orderInfo’)”),故经过切面拦截器进行校验:

    在beforeInvocation方法中校验权限,整体的调用链为:

    MethodSecurityInterceptor.invoke --> AbstractSecurityInterceptor.beforeInvocation -->
    AffirmativeBased.decide --> PreInvocationAuthorizationAdviceVoter.vote -->
    ExpressionBasedPreInvocationAdvice.before
    
    • 1
    • 2
    • 3

    ExpressionBasedPreInvocationAdvice.before方法即根据定义的注解来校验是否有权限访问该接口,入下图所示,用户具备的权限包括orderDetail和orderInfo,而注解校验是否包含orderInfo权限,故校验通过。

    至此完成了所有的权限校验过程,最终得到API返回的结果:

    整体的流程图如下:

    代码地址:https://github.com/futao1991/springcloud-oauth

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    聊一聊redis奇葩数据类型与集群知识
    全网最牛自动化测试框架系列之pytest(10)-常用执行参数说明
    Zabbix自定义监控项与触发器
    充分利用学习平台,提升个人职业竞争力
    web3获取所有事件日志与解码
    Navicat导入SQL文件
    R语言使用pt函数生成t分布累积分布函数数据、使用plot函数可视化t分布累积分布函数数据(t Distribution)
    uniapp-自定义本地图标字体(1)
    java 选择排序
    一种基于傅里叶变换的横向与纵向剪切干涉仿真分析
  • 原文地址:https://blog.csdn.net/m0_67402731/article/details/126063591