背景

前段时间有同学私信我,让我讲下Oauth2授权模式,并且还强调是零基础的那种,我也不太理解这个零基础到底是什么程度,但是我觉得任何阶段的同学看完我这个视频,对OAuth2的理解将会有很大的提升,并且也会熟练的使用SpringSecurity OAuth2,轻松搭建认证服务和资源服务。

大概了解下 SpringSecurity

Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,核心思想是通过一套filterChanin进行拦截和过滤

再来了解OAuth2

Oauth2是一个关于授权的官方标准,核心思路是通过各种认证手段(需要用户自己选择实现)进行认证用户身份,并颁发token,使得第三方应用可以使用该令牌在限定时间和限定范围内访问指定资源

再来了解SpringSecurity Oauth2

Spring Security OAuth2建立在Spring Security的基础之上,实现了OAuth2的规范 概念部分铺垫完成了,现在我们讲下OAuth2的四种授权模式

四种授权模式

为什么提供四种呢?四种授权模式代表OAuth授权第三方的不同互信程度

授权码模式(最安全,也最常用)

通过授权码获取token

场景

当用户使用B网站的账号(如微信)授权登录A网站

步骤

  • 跳转到B网站授权页面,输入B网站用户名和密码
  • B网站认证服务验证通过,回调网站A提供的回调地址,并带上认证code
  • A网站收到回调之后,提取code,再调用B网站获取token的接口,并带上A网站在B网站上面注册的client_id和client_secert

    别说话,看图

    SpringSecurity OAuth2 四种授权模式(理论 实战) - 图1SpringSecurity OAuth2 四种授权模式(理论 实战) - 图2

    简化模式(有中间人攻击的风险)

    简化模式在授权码模式的基础上减去了获取授权码的步骤,B网站认证通过之后,直接返回token 回调给A网站,所以又称为(授权码)“隐藏式”(implicit)

    步骤

  • 跳转到B网站授权页面,输入B网站用户名和密码

  • B网站认证服务验证通过,回调网站A提供的回调地址,并带上token信息

    看图

    SpringSecurity OAuth2 四种授权模式(理论 实战) - 图3

    密码模式(除非没得选才用,毕竟涉及到用户隐私)

    A网站要求用户直接输入用户名和密码,A网站自己拿着用户的密码和账号去B网站进行认证,直接获取token

    步骤

  • A网站要求用户输入账号和密码

  • A网站携带账号和密码去B网站认证
  • B网站认证通过,直接返回token给A网站

    看图

    SpringSecurity OAuth2 四种授权模式(理论 实战) - 图4

    客户端模式

    客户端模式其实和用户就没关系了,其实也可以说它不属于Oauth了,因为是用网站A自己的身份信息去B网站认证获取token,A网站上的所有用户去访问B网站的资源,都是用的A网站自己的客户端信息。

    场景

    这个一般用于微服务之间进行认证,如服务A想访问服务B,那么服务B需要校验服务A有没有权限访问自己,所以就需要校验服务B的token,这时就可以采用客户端模式,后面讲资源服务去认证中check_toekn的时候,用的就是这种方式。

    看图

    SpringSecurity OAuth2 四种授权模式(理论 实战) - 图5
    现在四种授权模式讲完了,大家是不是有个疑问:前面三种授权方式,拿到token之后,是怎么和用户进行绑定的呢?接下来看下实战过程中,是怎么用SpringSecurity Oauth2 实现四种授权方式。

    准备好电脑,开干

    创建认证服务

    创建ClientDetailsServiceImpl

    根据clientID获取客户端信息,security会去调用多次,所以一般加上缓存 ```java package com.lglbc.oauth2.config.details.client;

import com.lglbc.oauth2.entity.OauthClient; import com.lglbc.oauth2.enums.PasswordEncoderTypeEnum; import com.lglbc.oauth2.service.OauthClientService; import lombok.RequiredArgsConstructor; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.stereotype.Service;

import java.util.Objects;

/**

  • @author: 乐哥聊编程(全平台同号) */ @Service @RequiredArgsConstructor public class ClientDetailsServiceImpl implements ClientDetailsService { private final OauthClientService oauthClientService;

    @Override public ClientDetails loadClientByClientId(String clientId) {

    1. // 获取client信息
    2. OauthClient oauthClient = oauthClientService.lambdaQuery().eq(OauthClient::getClientId, clientId).one();
    3. if (Objects.nonNull(oauthClient)) {
    4. BaseClientDetails clientDetails = new BaseClientDetails(
    5. oauthClient.getClientId(),
    6. oauthClient.getResourceIds(),
    7. oauthClient.getScope(),
    8. oauthClient.getAuthorizedGrantTypes(),
    9. oauthClient.getAuthorities(),
    10. oauthClient.getWebServerRedirectUri());
    11. clientDetails.setClientSecret(PasswordEncoderTypeEnum.NOOP.getPrefix() + oauthClient.getClientSecret());
    12. clientDetails.setAccessTokenValiditySeconds(oauthClient.getAccessTokenValidity());
    13. clientDetails.setRefreshTokenValiditySeconds(oauthClient.getRefreshTokenValidity());
    14. return clientDetails;
    15. } else {
    16. throw new NoSuchClientException("没找到客户端信息");
    17. }

    } } ```

创建自定义的userDetails(自定义用户自己的信息)

  1. package com.lglbc.oauth2.config.details.user;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Builder;
  4. import lombok.Data;
  5. import lombok.NoArgsConstructor;
  6. import org.springframework.security.core.GrantedAuthority;
  7. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  8. import org.springframework.security.core.userdetails.UserDetails;
  9. import java.util.Collection;
  10. /**
  11. * @author: 乐哥聊编程(全平台同号)
  12. */
  13. @Data
  14. @Builder
  15. @AllArgsConstructor
  16. @NoArgsConstructor
  17. public class SysUserDetails implements UserDetails {
  18. /**
  19. * 扩展字段
  20. */
  21. private Long userId;
  22. /**
  23. * 默认字段
  24. */
  25. private String username;
  26. private String password;
  27. private Boolean enabled;
  28. private Collection<SimpleGrantedAuthority> authorities;
  29. @Override
  30. public Collection<? extends GrantedAuthority> getAuthorities() {
  31. return this.authorities;
  32. }
  33. @Override
  34. public String getPassword() {
  35. return this.password;
  36. }
  37. @Override
  38. public String getUsername() {
  39. return this.username;
  40. }
  41. @Override
  42. public boolean isAccountNonExpired() {
  43. return true;
  44. }
  45. @Override
  46. public boolean isAccountNonLocked() {
  47. return true;
  48. }
  49. @Override
  50. public boolean isCredentialsNonExpired() {
  51. return true;
  52. }
  53. @Override
  54. public boolean isEnabled() {
  55. return this.enabled;
  56. }
  57. }

创建根据用户账号获取信息的detailService

  1. package com.lglbc.oauth2.config.details.user;
  2. import com.lglbc.oauth2.entity.Role;
  3. import com.lglbc.oauth2.entity.User;
  4. import com.lglbc.oauth2.enums.PasswordEncoderTypeEnum;
  5. import com.lglbc.oauth2.mapper.UserRoleMapper;
  6. import com.lglbc.oauth2.service.RoleService;
  7. import com.lglbc.oauth2.service.UserService;
  8. import com.xiaoleilu.hutool.collection.CollectionUtil;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.security.core.GrantedAuthority;
  11. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  12. import org.springframework.security.core.userdetails.UserDetails;
  13. import org.springframework.security.core.userdetails.UserDetailsService;
  14. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  15. import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
  16. import org.springframework.stereotype.Service;
  17. import java.util.*;
  18. import java.util.stream.Collectors;
  19. @Service
  20. public class SysUserDetailServiceImpl implements UserDetailsService {
  21. @Autowired
  22. private UserService userServiceImpl;
  23. @Autowired
  24. private RoleService roleServiceImpl;
  25. @Autowired
  26. private UserRoleMapper userRoleMapper;
  27. @Override
  28. public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
  29. Map<String, Object> map = new HashMap<>();
  30. map.put("username", name);
  31. User user = userServiceImpl.lambdaQuery().eq(User::getUsername, name).one();
  32. if (user == null) {
  33. throw new UsernameNotFoundException("此用户不存在");
  34. }
  35. Collection<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();
  36. List<String> roleIds = userRoleMapper.getRoleIdByUserId(user.getId());
  37. if (CollectionUtil.isNotEmpty(roleIds)){
  38. List<Role> roles = roleServiceImpl.lambdaQuery().in(Role::getId, roleIds).list();
  39. List<String> roleCodes = roles.stream().map(Role::getCode).collect(Collectors.toList());
  40. for (String roleCOde : roleCodes) {
  41. SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleCOde);
  42. grantedAuthorities.add(grantedAuthority);
  43. }
  44. }
  45. return new SysUserDetails(user.getId(), user.getUsername(),PasswordEncoderTypeEnum.BCRYPT.getPrefix() + user.getPassword(),true, grantedAuthorities);
  46. }
  47. }

创建认证服务配置

  1. package com.lglbc.oauth2.config;
  2. import com.lglbc.oauth2.config.details.client.ClientDetailsServiceImpl;
  3. import com.lglbc.oauth2.config.details.user.SysUserDetails;
  4. import com.xiaoleilu.hutool.collection.CollectionUtil;
  5. import lombok.RequiredArgsConstructor;
  6. import lombok.SneakyThrows;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.core.io.ClassPathResource;
  10. import org.springframework.data.redis.connection.RedisConnectionFactory;
  11. import org.springframework.security.authentication.AuthenticationManager;
  12. import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
  13. import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
  14. import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
  15. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
  16. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
  17. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
  18. import org.springframework.security.oauth2.provider.CompositeTokenGranter;
  19. import org.springframework.security.oauth2.provider.TokenGranter;
  20. import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
  21. import org.springframework.security.oauth2.provider.token.TokenEnhancer;
  22. import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
  23. import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
  24. import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
  25. import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
  26. import java.security.KeyPair;
  27. import java.util.*;
  28. /**
  29. * @author: 乐哥聊编程(全平台同号)
  30. */
  31. @Configuration
  32. @EnableAuthorizationServer
  33. @RequiredArgsConstructor
  34. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  35. private final AuthenticationManager authenticationManager;
  36. private final ClientDetailsServiceImpl clientDetailsService;
  37. // 该对象用来将令牌信息存储到Redis中
  38. private final RedisConnectionFactory redisConnectionFactory;
  39. /**
  40. * OAuth2客户端
  41. */
  42. @Override
  43. @SneakyThrows
  44. public void configure(ClientDetailsServiceConfigurer clients) {
  45. clients.withClientDetails(clientDetailsService);
  46. }
  47. /**
  48. * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
  49. */
  50. @Override
  51. public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
  52. // Token增强
  53. TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
  54. List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
  55. tokenEnhancers.add(tokenEnhancer());
  56. tokenEnhancers.add(jwtAccessTokenConverter());
  57. tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
  58. // 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
  59. List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));
  60. CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
  61. endpoints
  62. .authenticationManager(authenticationManager)
  63. .accessTokenConverter(jwtAccessTokenConverter())
  64. .tokenEnhancer(tokenEnhancerChain)
  65. .tokenGranter(compositeTokenGranter)
  66. .tokenServices(tokenServices(endpoints))
  67. ;
  68. }
  69. @Override
  70. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  71. security.checkTokenAccess("permitAll()");
  72. }
  73. public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
  74. TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
  75. List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
  76. tokenEnhancers.add(tokenEnhancer());
  77. tokenEnhancers.add(jwtAccessTokenConverter());
  78. tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
  79. DefaultTokenServices tokenServices = new DefaultTokenServices();
  80. tokenServices.setTokenStore(endpoints.getTokenStore());
  81. tokenServices.setSupportRefreshToken(true);
  82. tokenServices.setReuseRefreshToken(true);
  83. tokenServices.setClientDetailsService(clientDetailsService);
  84. tokenServices.setTokenEnhancer(tokenEnhancerChain);
  85. return tokenServices;
  86. }
  87. /**
  88. * JWT内容增强
  89. */
  90. @Bean
  91. public TokenEnhancer tokenEnhancer() {
  92. return (accessToken, authentication) -> {
  93. Map<String, Object> additionalInfo = CollectionUtil.newHashMap();
  94. if (Objects.nonNull(authentication.getUserAuthentication())) {
  95. Object principal = authentication.getUserAuthentication().getPrincipal();
  96. if (principal instanceof SysUserDetails) {
  97. SysUserDetails sysUserDetails = (SysUserDetails) principal;
  98. additionalInfo.put("userId", sysUserDetails.getUserId());
  99. additionalInfo.put("username", sysUserDetails.getUsername());
  100. ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
  101. }
  102. }
  103. return accessToken;
  104. };
  105. }
  106. /**
  107. * 使用非对称加密算法对token签名
  108. */
  109. @Bean
  110. public JwtAccessTokenConverter jwtAccessTokenConverter() {
  111. JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  112. converter.setKeyPair(keyPair());
  113. return converter;
  114. }
  115. /**
  116. * 密钥库中获取密钥对(公钥+私钥)
  117. */
  118. @Bean
  119. public KeyPair keyPair() {
  120. KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
  121. KeyPair keyPair = factory.getKeyPair("jwt", "123456".toCharArray());
  122. return keyPair;
  123. }
  124. }

创建安全配置

  1. package com.lglbc.oauth2.config;
  2. import com.lglbc.oauth2.common.result.R;
  3. import com.lglbc.oauth2.common.result.ResultCode;
  4. import com.xiaoleilu.hutool.json.JSONUtil;
  5. import lombok.RequiredArgsConstructor;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.http.HttpHeaders;
  10. import org.springframework.http.HttpStatus;
  11. import org.springframework.http.MediaType;
  12. import org.springframework.security.authentication.AuthenticationManager;
  13. import org.springframework.security.authentication.InsufficientAuthenticationException;
  14. import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
  15. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  16. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  17. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  18. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  19. import org.springframework.security.core.userdetails.UserDetailsService;
  20. import org.springframework.security.crypto.factory.PasswordEncoderFactories;
  21. import org.springframework.security.crypto.password.PasswordEncoder;
  22. import org.springframework.security.web.AuthenticationEntryPoint;
  23. /**
  24. * @author: 乐哥聊编程(全平台同号)
  25. */
  26. @Configuration
  27. @EnableWebSecurity
  28. @Slf4j
  29. @RequiredArgsConstructor
  30. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  31. private final UserDetailsService sysUserDetailsService;
  32. private final String realName = ".";
  33. @Override
  34. protected void configure(HttpSecurity http) throws Exception {
  35. http
  36. .authorizeRequests().antMatchers("/oauth/**", "/test/**", "/login").permitAll()
  37. .anyRequest().authenticated()
  38. .and()
  39. .httpBasic().and()
  40. .csrf().disable()
  41. .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
  42. }
  43. /**
  44. * 认证管理对象
  45. *
  46. * @return
  47. * @throws Exception
  48. */
  49. @Bean
  50. public AuthenticationManager authenticationManagerBean() throws Exception {
  51. return super.authenticationManagerBean();
  52. }
  53. /**
  54. * 添加自定义认证器
  55. *
  56. * @param auth
  57. */
  58. @Override
  59. public void configure(AuthenticationManagerBuilder auth) throws Exception {
  60. auth.authenticationProvider(daoAuthenticationProvider());
  61. }
  62. /**
  63. * 设置默认的用户名密码认证授权提供者
  64. *
  65. * @return
  66. */
  67. @Bean
  68. public DaoAuthenticationProvider daoAuthenticationProvider() {
  69. DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
  70. provider.setUserDetailsService(sysUserDetailsService);
  71. provider.setPasswordEncoder(passwordEncoder());
  72. provider.setHideUserNotFoundExceptions(false); // 是否隐藏用户不存在异常,默认:true-隐藏;false-抛出异常;
  73. return provider;
  74. }
  75. @Bean
  76. public PasswordEncoder passwordEncoder() {
  77. return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  78. }
  79. /**
  80. * 自定义认证异常响应数据
  81. */
  82. @Bean
  83. public AuthenticationEntryPoint authenticationEntryPoint() {
  84. return (request, response, e) -> {
  85. if (e.getMessage().equals("User must be authenticated with Spring Security before authorization can be completed.")) {
  86. response.addHeader("WWW-Authenticate", "Basic realm= " + realName);
  87. response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
  88. } else {
  89. response.setStatus(HttpStatus.OK.value());
  90. response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
  91. response.setHeader("Access-Control-Allow-Origin", "*");
  92. response.setHeader("Cache-Control", "no-cache");
  93. R result = R.failed(ResultCode.CLIENT_AUTHENTICATION_FAILED);
  94. response.getWriter().print(JSONUtil.toJsonStr(result));
  95. response.getWriter().flush();
  96. }
  97. };
  98. }
  99. }

重写token enpoint

  1. package com.lglbc.oauth2;
  2. import com.lglbc.oauth2.common.result.R;
  3. import com.nimbusds.jose.jwk.JWKSet;
  4. import com.nimbusds.jose.jwk.RSAKey;
  5. import lombok.AllArgsConstructor;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.security.oauth2.common.OAuth2AccessToken;
  8. import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
  9. import org.springframework.web.HttpRequestMethodNotSupportedException;
  10. import org.springframework.web.bind.annotation.*;
  11. import java.security.KeyPair;
  12. import java.security.Principal;
  13. import java.security.interfaces.RSAPublicKey;
  14. import java.util.Map;
  15. /**
  16. * @author: 乐哥聊编程(全平台同号)
  17. */
  18. @RestController
  19. @RequestMapping("/oauth")
  20. @AllArgsConstructor
  21. @Slf4j
  22. public class AuthController {
  23. private KeyPair keyPair;
  24. private final TokenEndpoint tokenEndpoint;
  25. @PostMapping("/token")
  26. public Object postAccessToken(
  27. Principal principal,
  28. @RequestParam Map<String, String> parameters
  29. ) throws HttpRequestMethodNotSupportedException {
  30. OAuth2AccessToken accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
  31. return R.ok(accessToken);
  32. }
  33. @GetMapping("/public-key")
  34. public Map<String, Object> getPublicKey() {
  35. RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
  36. RSAKey key = new RSAKey.Builder(publicKey).build();
  37. return new JWKSet(key).toJSONObject();
  38. }
  39. }

演示四种授权模式

授权码模式

获取授权码

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图6

  • 输入用户名和密码

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图7
在这里插入图片描述

  • 点击授权,获取授权码

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图8

获取token

  1. curl --location --request POST --X POST 'http://localhost:8080/oauth/token?code=k9jcxv&grant_type=authorization_code' \
  2. --header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
  3. --header 'Authorization: Basic YWRtaW46YW1z'

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图9

刷新token

  1. curl --location --request POST --X POST 'http://localhost:8080/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhbXMiLCJzY29wZSI6WyJ3cml0ZSJdLCJhdGkiOiI4NjZmYWY2MC05MTE1LTQ0NGMtOWNkNi05MjRmYTRlZjQ4YjUiLCJleHAiOjE2NTQ1MzExMzcsInVzZXJJZCI6NCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiIyNDg1M2NiNi02ZGM1LTRlMDQtYjAxOC0wMGE0MGMxNGMyNjAiLCJjbGllbnRfaWQiOiJhZG1pbiIsInVzZXJuYW1lIjoiYW1zIn0.BX87xbC1SEVgXsQ6qdI1_RLVOY1ffqoKDvd-Fnu5FfJor6CR-3NtUjMBIQig_jAnJwptG8ZIs1q715GAQTtgoRVUMsnXBv8RKmlDvxjclUYIWrRXy1d9Rj84WC4niDU8rOzazr4L1-chkwod3rAEDAZSiBcd4XZ1S2cfG7WBSH1TcivNtY0pk_ePS08ItN_2wYm57-7_JD8XCRecpn6nF6ax20ysimqPftAKIiVzvhhN4O_TsHd3-lB7ZYTsT-SlzjBu43gm5eGFWhsOBK7NgxObwoeXaoARrzW3GaUw8DsLTKsDofyIIFGnKee9sooAwejPrsxtVRJlfj8gIc5EdA&grant_type=refresh_token' \
  2. --header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
  3. --header 'Authorization: Basic YWRtaW46YW1z'

简单模式

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图10

  • 输入用户名和密码(用户信息,不是客户端)

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图11

  • 点击授权,直接获取token

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图12

用户名密码模式

  1. curl --location --request POST --X POST 'http://localhost:8080/oauth/token?grant_type=password&username=ams&password=ams' \
  2. --header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
  3. --header 'Authorization: Basic YWRtaW46YW1z'

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图13

客户端模式

  1. curl --location --request POST --X POST 'http://127.0.0.1:8080/oauth/token?grant_type=client_credentials' \
  2. --header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
  3. --header 'Authorization: Basic YWRtaW46YW1z'

SpringSecurity OAuth2 四种授权模式(理论 实战) - 图14

创建资源服务

资源服务配置

  1. package com.lglbc.oauth2.config;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import com.lglbc.oauth2.common.KeyPairUtil;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.core.io.ClassPathResource;
  7. import org.springframework.core.io.Resource;
  8. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9. import org.springframework.security.config.http.SessionCreationPolicy;
  10. import org.springframework.security.config.web.server.ServerHttpSecurity;
  11. import org.springframework.security.crypto.codec.Base64;
  12. import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
  13. import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
  14. import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
  15. import org.springframework.security.oauth2.provider.token.TokenStore;
  16. import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
  17. import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
  18. import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
  19. import org.springframework.security.web.server.SecurityWebFilterChain;
  20. import org.springframework.web.client.RestTemplate;
  21. import java.io.BufferedReader;
  22. import java.io.IOException;
  23. import java.io.InputStreamReader;
  24. import java.security.KeyPair;
  25. import java.security.interfaces.RSAPublicKey;
  26. import java.util.Map;
  27. import java.util.stream.Collectors;
  28. @Configuration
  29. public class Oauth2ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
  30. private static final String CHECK_TOKEN_URL = "http://localhost:8080/oauth/check_token";
  31. // 也可以使用认证服务去校验
  32. // @Override
  33. // public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
  34. // RemoteTokenServices tokenService = new RemoteTokenServices();
  35. // tokenService.setCheckTokenEndpointUrl(CHECK_TOKEN_URL);
  36. // tokenService.setClientId("resource");
  37. // tokenService.setClientSecret("resource");
  38. // resources.tokenServices(tokenService);
  39. // }
  40. @Bean
  41. public TokenStore tokenStore() {
  42. return new JwtTokenStore(jwtAccessTokenConverter());
  43. }
  44. @Bean
  45. public JwtAccessTokenConverter jwtAccessTokenConverter() {
  46. JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  47. //设置用于解码的非对称加密的公钥
  48. converter.setVerifierKey(KeyPairUtil.getPubKey());
  49. return converter;
  50. }
  51. @Override
  52. public void configure(HttpSecurity http) throws Exception {
  53. http.authorizeRequests()
  54. // 任何请求都必须具有all这个域的授权
  55. .antMatchers("/test/read").access("#oauth2.hasScope('read') || #oauth2.hasScope('write')")
  56. .antMatchers("/test/write").access("#oauth2.hasScope('write')")
  57. .and()
  58. .csrf().disable()
  59. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  60. }
  61. }

创建安全配置,添加拦截

  1. package com.lglbc.oauth2.config;
  2. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  3. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  4. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  5. @EnableWebSecurity
  6. public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  7. @Override
  8. protected void configure(HttpSecurity http) throws Exception {
  9. http.authorizeRequests().antMatchers("/test/**").authenticated();
  10. // 禁用CSRF
  11. http.csrf().disable();
  12. }
  13. }

演示资源服务

定义两个接口

  • /read:需要有admin角色 并且有对目前服务的读权限
    1. @RequestMapping("/read")
    2. @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    3. public String read(){
    4. System.out.println("read");
    5. return "read";
    6. }
  • /write:需要有admin角色 并且有对目前服务的读写权限
    1. @RequestMapping("/write")
    2. // @Secured({"ROLE_ADMIN"})
    3. @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    4. public String write() throws ParseException {
    5. return UserContextUtil.getUserId()+""+UserContextUtil.getUserName();
    6. }

常见问题

角色权限注解问题

  • 不加ROLE_前缀,只能使用此注解

@PreAuthorize(“hasAuthority(‘ADMIN2’)”)

  • 加ROLE_前缀,可以使用如下注解:

@PreAuthorize(“hasRole(‘ADMIN’)”) //允许
@PreAuthorize(“hasRole(‘ROLE_ADMIN’)”) //允许
@PreAuthorize(“hasAuthority(‘ROLE_ADMIN’)”) //允许

客户端密钥问题

  • 如果密钥没有加密,在构建clientDetail的时候一定要加上{noop}前缀
  • 否则加上 {bcrypt}前缀

持续整理…

获取用户信息

源码部分SpringSecurity OAuth2 四种授权模式(理论 实战) - 图15
由于在资源服务中,我们没有重写userDetailService,所以我们是获取不到用户的信息(我们也没必要写),但是我们可以自己解析token,拿到用户信息

  1. package com.lglbc.oauth2.config;
  2. import com.lglbc.oauth2.common.UserContextUtil;
  3. import com.nimbusds.jose.JWSObject;
  4. import org.apache.commons.lang3.StringUtils;
  5. import org.springframework.web.servlet.HandlerInterceptor;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.servlet.http.HttpServletResponse;
  8. import java.util.Map;
  9. public class UserInterceptor implements HandlerInterceptor {
  10. @Override
  11. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  12. String authorization = request.getHeader("Authorization");
  13. if (StringUtils.isBlank(authorization)) {
  14. return true;
  15. }
  16. String token = StringUtils.substringAfter(authorization, "Bearer ");
  17. if (StringUtils.isBlank(token)) {
  18. return true;
  19. }
  20. try {
  21. Map<String, Object> stringObjectMap = JWSObject.parse(token).getPayload().toJSONObject();
  22. UserContextUtil.setUser(stringObjectMap);
  23. } catch (Exception e) {
  24. }
  25. return true;
  26. }
  27. @Override
  28. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  29. UserContextUtil.removeUser();
  30. }
  31. }