当前位置:   article > 正文

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

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中首先定义认证服务器:

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

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

  1. CREATE TABLE `oauth_client_details` (
  2. `client_id` varchar(48) NOT NULL,
  3. `resource_ids` varchar(256) DEFAULT NULL,
  4. `client_secret` varchar(256) DEFAULT NULL,
  5. `scope` varchar(256) DEFAULT NULL,
  6. `authorized_grant_types` varchar(256) DEFAULT NULL,
  7. `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  8. `authorities` varchar(256) DEFAULT NULL,
  9. `access_token_validity` int(11) DEFAULT NULL,
  10. `refresh_token_validity` int(11) DEFAULT NULL,
  11. `additional_information` varchar(4096) DEFAULT NULL,
  12. `autoapprove` varchar(256) DEFAULT NULL,
  13. PRIMARY KEY (`client_id`)
  14. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

       接着配置资源服务器:

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

         然后配置WebSecurity

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

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

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

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

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

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

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

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

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

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

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

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

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

      

    整体的调用链为: 

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

  最终重定向到localhost:8101/login

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

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

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

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

      最终重定向到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,必选项

        3) 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,请求调用链为:

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

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

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

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

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

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

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

     在下面的方法中获取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方法中校验权限,整体的调用链为:

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

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

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

     整体的流程图如下:

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/745877
推荐阅读
相关标签
  

闽ICP备14008679号