项目简介

pig项目是目前(2021-10-30)的gitee上排名第一的开源项目。Pig使用的也是Spring Security Oauth2这一套框架。所以,今天就来好好整理一下整体的一个写法。
https://gitee.com/log4j/pig.git

登录演示

image.png

  • 登录地址使用的是框架(Spring Security Oauth2)提供的token发布接口
  • 使用的是password授权类型

image.png

  • 短信登录这种方式是Pig团队自己定义的一种授权类型

    授权服务器配置

    本节带大家一起看一下,pig-auth模块(授权服务器的配置)
  1. 项目使用的是password授权方式,那么必定需要AuthenticationManager

    image.png

  2. Oauth2必须有客户端信息,具体存放位置是哪里?

  3. Token的存放方式?Redis?Jwt?
  4. 如何自定义授权方式
  5. SpringSecurity配置类的作用?怎么配置?

问题1

  1. 项目使用的是password授权方式,那么必定需要AuthenticationManager从哪里来

答:AuthenticationManager一定是有SpringSecurity配置类中@Bean注册到容器中。自然需要有SpringSecurity的相关配置

问题2

  1. Oauth2必须有客户端信息,具体存放位置是哪里?

答:作者重写了SpringSecurityOauth2提供的JdbcClientService。使用自己的表存储查询客户端信息

  1. @Configuration
  2. @RequiredArgsConstructor
  3. @EnableAuthorizationServer
  4. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  5. private final DataSource dataSource;
  6. private final UserDetailsService userDetailsService;
  7. private final AuthenticationManager authenticationManager;
  8. private final TokenStore redisTokenStore;
  9. @Override
  10. @SneakyThrows
  11. public void configure(ClientDetailsServiceConfigurer clients) {
  12. clients.withClientDetails(pigClientDetailsService());
  13. }
  14. @Bean
  15. public ClientDetailsService pigClientDetailsService() {
  16. PigClientDetailsService clientDetailsService = new PigClientDetailsService(dataSource);
  17. clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);
  18. clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);
  19. return clientDetailsService;
  20. }
  21. }

image.png

问题3

  1. Token的存放方式

答:作者将Token选择存放在Redis中

  1. /**
  2. * @author lengleng
  3. * @date 2019/2/1 认证服务器配置
  4. */
  5. @Configuration
  6. @RequiredArgsConstructor
  7. @EnableAuthorizationServer
  8. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  9. private final DataSource dataSource;
  10. private final UserDetailsService userDetailsService;
  11. private final AuthenticationManager authenticationManager;
  12. private final TokenStore redisTokenStore;
  13. /*
  14. 令牌增强器的作用就是调用oauth/token后返回的字段的增加
  15. {
  16. "access_token": "b9e968ff-5ce2-42b6-b726-6313e11f1239",
  17. "token_type": "bearer",
  18. "refresh_token": "dd5f65df-8792-47d3-a10e-2985481d7b2b",
  19. "expires_in": 43199,
  20. "scope": "server",
  21. "license": "made by pig" (这行就是添加的)
  22. }
  23. */
  24. @Bean
  25. public TokenEnhancer tokenEnhancer() {
  26. return (accessToken, authentication) -> {
  27. final Map<String, Object> additionalInfo = new HashMap<>(4);
  28. additionalInfo.put(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PROJECT_LICENSE);
  29. ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
  30. return accessToken;
  31. };
  32. }
  33. /*
  34. 1.这块原来不是这样的,最近正版又被改了。
  35. 2.自定义了TokenService
  36. 3.自定义了TokenStore
  37. 4.但是最终还是Redis存储
  38. 5.太厉害了,改这一套要看好多源码的。
  39. */
  40. @Bean
  41. public PigCustomTokenServices tokenServices() {
  42. PigCustomTokenServices tokenServices = new PigCustomTokenServices();
  43. tokenServices.setTokenStore(redisTokenStore);
  44. tokenServices.setSupportRefreshToken(true);
  45. tokenServices.setReuseRefreshToken(false);
  46. tokenServices.setClientDetailsService(pigClientDetailsService());
  47. tokenServices.setTokenEnhancer(tokenEnhancer());
  48. addUserDetailsService(tokenServices, userDetailsService);
  49. return tokenServices;
  50. }
  51. /*
  52. 这行我不太了解。
  53. 但是目的就是把 userDetailService放入 tokenService中
  54. */
  55. private void addUserDetailsService(PigCustomTokenServices tokenServices, UserDetailsService userDetailsService) {
  56. if (userDetailsService != null) {
  57. PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
  58. provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService));
  59. tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
  60. }
  61. }
  62. }

问题5

如何自定义授权方式

答:pig团队自定义了一种授权类型并配置到框架中

  1. /**
  2. * @author lengleng
  3. * @date 2019/2/1 认证服务器配置
  4. */
  5. @Configuration
  6. @RequiredArgsConstructor
  7. @EnableAuthorizationServer
  8. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  9. private final DataSource dataSource;
  10. private final UserDetailsService userDetailsService;
  11. private final AuthenticationManager authenticationManager;
  12. private final TokenStore redisTokenStore;
  13. @Override
  14. public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
  15. endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenServices(tokenServices())
  16. .tokenStore(redisTokenStore).tokenEnhancer(tokenEnhancer()).userDetailsService(userDetailsService)
  17. .authenticationManager(authenticationManager).reuseRefreshTokens(false)
  18. .pathMapping("/oauth/confirm_access", "/token/confirm_access")
  19. .exceptionTranslator(new PigWebResponseExceptionTranslator());
  20. setTokenGranter(endpoints);
  21. }
  22. private void setTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
  23. // 获取默认授权类型
  24. TokenGranter tokenGranter = endpoints.getTokenGranter();
  25. ArrayList<TokenGranter> tokenGranters = new ArrayList<>(Arrays.asList(tokenGranter));
  26. ResourceOwnerCustomeAppTokenGranter resourceOwnerCustomeAppTokenGranter = new ResourceOwnerCustomeAppTokenGranter(
  27. authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(),
  28. endpoints.getOAuth2RequestFactory());
  29. tokenGranters.add(resourceOwnerCustomeAppTokenGranter);
  30. CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranters);
  31. endpoints.tokenGranter(compositeTokenGranter);
  32. }
  33. }

问题5

  1. SpringSecurity配置类的作用?怎么配置?

答:这边SpringSecurity配置类的出现是为了配置:

  1. AuthenticationManager
  2. PasswordEncoder
  3. 放行 oauth2 相关端口
  4. 其他系统oauth2认证的登录页面和退出处理定义 ```java

@Primary @Order(90) @Configuration @AllArgsConstructor public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

  1. /*
  2. UserDetail在 common-security中定义并注册好了
  3. */
  4. private final UserDetailsService userDetailsService;
  5. @Override
  6. @SneakyThrows
  7. protected void configure(HttpSecurity http) {
  8. http.authenticationProvider(phoneAuthenticationProvider()).formLogin().loginPage("/token/login")
  9. //定义Oauth2登录页面
  10. .loginProcessingUrl("/token/form").failureHandler(authenticationFailureHandler()).and().logout()
  11. //退出登录处理
  12. .logoutSuccessHandler(logoutSuccessHandler()).deleteCookies("JSESSIONID").invalidateHttpSession(true)
  13. //放行 oauth接口 和 健康监控接口
  14. .and().authorizeRequests().antMatchers("/token/**", "/actuator/**", "/mobile/**").permitAll()
  15. .anyRequest().authenticated().and().csrf().disable();
  16. }
  17. /**
  18. * 不要直接使用@Bean注入 会导致默认的提供者无法注入(DaoAuthenticationProvider)
  19. * 自定义的一个认证提供者
  20. */
  21. private CustomAppAuthenticationProvider phoneAuthenticationProvider() {
  22. CustomAppAuthenticationProvider phoneAuthenticationProvider = new CustomAppAuthenticationProvider();
  23. phoneAuthenticationProvider.setUserDetailsService(userDetailsService);
  24. return phoneAuthenticationProvider;
  25. }
  26. @Override
  27. public void configure(WebSecurity web) {
  28. web.ignoring().antMatchers("/css/**");
  29. }
  30. @Bean
  31. @Override
  32. @SneakyThrows
  33. public AuthenticationManager authenticationManagerBean() {
  34. return super.authenticationManagerBean();
  35. }
  36. @Bean
  37. public AuthenticationFailureHandler authenticationFailureHandler() {
  38. return new FormAuthenticationFailureHandler();
  39. }
  40. /**
  41. * 支持SSO 退出
  42. * @return LogoutSuccessHandler
  43. */
  44. @Bean
  45. public LogoutSuccessHandler logoutSuccessHandler() {
  46. return new SsoLogoutSuccessHandler();
  47. }
  48. /**
  49. * https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-updated
  50. * Encoded password does not look like BCrypt
  51. * @return PasswordEncoder
  52. */
  53. @Bean
  54. public PasswordEncoder passwordEncoder() {
  55. return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  56. }

}

  1. <a name="ceIgE"></a>
  2. # 退出登录
  3. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1609516/1635566534289-43d6314a-8569-4415-b63e-33b71068869c.png#clientId=uf49bfa3c-a0c0-4&from=paste&height=390&id=u273a6a6a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=780&originWidth=850&originalType=binary&ratio=1&size=111765&status=done&style=none&taskId=u2728700d-4f0c-43ae-9465-5cb2492774e&width=425)
  4. - 退出登录调用的是自定义的一个接口
  5. - 成功后,会发送SpringEvent事件。然后Common中定义了处理逻辑(记日志)
  6. <a name="v55XX"></a>
  7. # Security-Common包解读
  8. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1609516/1635574049668-6fd09ced-63b0-4739-bc55-5618df3d995b.png#clientId=uf49bfa3c-a0c0-4&from=paste&height=436&id=u6db2e460&margin=%5Bobject%20Object%5D&name=image.png&originHeight=362&originWidth=343&originalType=binary&ratio=1&size=14302&status=done&style=none&taskId=u9a87d04c-0e62-4bd3-9dbc-bee75bd683a&width=413.49147033691406)
  9. - util:主要是token从header中解析和用户信息获取工具类
  10. - handler:主要定义了一些Spring Event处理器(登录成功、失败)
  11. - exception:自定义异常包
  12. - service:特定的UserDetailService
  13. <a name="dh0mr"></a>
  14. ## Service包下内容
  15. Pig项目用户登录使用password方式进行授权。用户登录时会使用SpringSecurity校验用户的账号和密码。校验逻辑通过自定义UserDetailService来实现。
  16. <a name="O5gC9"></a>
  17. ### PigClientDetailsService自定义OauthClientService
  18. 这个就是自定义的一个ClientService。
  19. <a name="AqHnF"></a>
  20. ### SpringSecurity用户实体类
  21. UserDetailService需要返回一个UserDetail对象。Pig项目自定义了自己的`PigUser`类(继承User类)来实现UserDetail的实现。
  22. ```java
  23. /**
  24. * @author lengleng
  25. * @date 2019/2/1 扩展用户信息
  26. */
  27. public class PigUser extends User {
  28. /**
  29. * 用户ID
  30. */
  31. @Getter
  32. private final Integer id;
  33. /**
  34. * 部门ID
  35. */
  36. @Getter
  37. private final Integer deptId;
  38. /**
  39. * Construct the <code>User</code> with the details required by
  40. * {@link DaoAuthenticationProvider}.
  41. * @param id 用户ID
  42. * @param deptId 部门ID
  43. * @param username the username presented to the
  44. * <code>DaoAuthenticationProvider</code>
  45. * @param password the password that should be presented to the
  46. * <code>DaoAuthenticationProvider</code>
  47. * @param enabled set to <code>true</code> if the user is enabled
  48. * @param accountNonExpired set to <code>true</code> if the account has not expired
  49. * @param credentialsNonExpired set to <code>true</code> if the credentials have not
  50. * expired
  51. * @param accountNonLocked set to <code>true</code> if the account is not locked
  52. * @param authorities the authorities that should be granted to the caller if they
  53. * presented the correct username and password and the user is enabled. Not null.
  54. * @throws IllegalArgumentException if a <code>null</code> value was passed either as
  55. * a parameter or as an element in the <code>GrantedAuthority</code> collection
  56. */
  57. public PigUser(Integer id, Integer deptId, String username, String password, boolean enabled,
  58. boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked,
  59. Collection<? extends GrantedAuthority> authorities) {
  60. super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
  61. this.id = id;
  62. this.deptId = deptId;
  63. }
  64. }

PigUserDetailsServiceImpl用户登录逻辑类

这个类完成具体的登录逻辑,具体流程是:

  1. 先从cache中拿去PigUser,若有直接返回
  2. 调用system服务,去查询用户信息
  3. 封装PigUser信息(之后SpringSecurityOauth2框架会自动的比较密码)

    PigCustomTokenServices令牌服务

    主要负责令牌的刷新创建。在之前还是没有这个类的。之前使用框架自带的DefaultTokenServices。

    grant包下内容

    grant包下的所有内容都是为了平台能够完成手机号+验证码的登录方式做的功能。但是目前这个功能我还没还有实际使用上。还不太清楚其中某些类的用法。

fegin包下内容

外部直接调用服务,框架会校验token,但是服务之间调用的话 token是不会自己带着的。所有这个包下就是解决这个问题的。

annotation包下内容

注解包下主要有两个注解。@Inner的作用是做切面标识使用。标识某个接口是否为内部调用接口。

  1. @Slf4j
  2. @Aspect
  3. @RequiredArgsConstructor
  4. public class PigSecurityInnerAspect implements Ordered {
  5. private final HttpServletRequest request;
  6. @SneakyThrows
  7. @Around("@within(inner) || @annotation(inner)")
  8. public Object around(ProceedingJoinPoint point, Inner inner) {
  9. // 实际注入的inner实体由表达式后一个注解决定,即是方法上的@Inner注解实体,若方法上无@Inner注解,则获取类上的
  10. if (inner == null) {
  11. Class<?> clazz = point.getTarget().getClass();
  12. inner = AnnotationUtils.findAnnotation(clazz, Inner.class);
  13. }
  14. String header = request.getHeader(SecurityConstants.FROM);
  15. //内部 true 且 但是来源不是内部
  16. if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
  17. log.warn("访问接口 {} 没有权限", point.getSignature().getName());
  18. throw new AccessDeniedException("Access is denied");
  19. }
  20. return point.proceed();
  21. }
  22. @Override
  23. public int getOrder() {
  24. return Ordered.HIGHEST_PRECEDENCE + 1;
  25. }
  26. }
  • AnnotationUtils找类上的注解
  • 默认标识接口,接口的调用者必须是通过fegin调用
  • 如果想通过postman直接调用,则使用@Inner(false);

@EnablePigResourceServer这个注解值得学习一下。一般写Common包。通过这个注解开启一些功能。

  1. @Documented
  2. @Inherited
  3. @EnableResourceServer
  4. @Target({ ElementType.TYPE })
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @EnableGlobalMethodSecurity(prePostEnabled = true)
  7. @Import({ PigResourceServerAutoConfiguration.class, PigSecurityBeanDefinitionRegistrar.class,
  8. PigResourceServerTokenRelayAutoConfiguration.class, PigFeignClientConfiguration.class })
  9. public @interface EnablePigResourceServer {
  10. }

Component包下内容

我们先从上个注解的@Import的类说起

PigResourceServerAutoConfiguration类

框架的ResourceServer的核心配置类。除了pig-auth外,其他的所有微服务都是一个资源服务器。

  1. @EnableConfigurationProperties(PermitAllUrlProperties.class)
  2. public class PigResourceServerAutoConfiguration {
  3. /*
  4. 定义权限校验Service
  5. */
  6. @Bean("pms")
  7. public PermissionService permissionService() {
  8. return new PermissionService();
  9. }
  10. /*
  11. 注册 无权限访问的处理类
  12. */
  13. @Bean
  14. public PigAccessDeniedHandler pigAccessDeniedHandler(ObjectMapper objectMapper) {
  15. return new PigAccessDeniedHandler(objectMapper);
  16. }
  17. /*
  18. 注册自己token提取器
  19. */
  20. @Bean
  21. public PigBearerTokenExtractor pigBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
  22. return new PigBearerTokenExtractor(urlProperties);
  23. }
  24. /*
  25. 没有token时的处理器
  26. */
  27. @Bean
  28. public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper) {
  29. return new ResourceAuthExceptionEntryPoint(objectMapper);
  30. }
  31. /*
  32. 注册自己的ResourceServerTokenService 资源服务器token解析器
  33. 这里之后就不照着 常规去再 配置 ResourceAdapter了。
  34. */
  35. @Bean
  36. @Primary
  37. public ResourceServerTokenServices resourceServerTokenServices(TokenStore tokenStore,
  38. UserDetailsService userDetailsService) {
  39. return new PigLocalResourceServerTokenServices(tokenStore, userDetailsService);
  40. }
  41. }

PermissionService类

权限校验

PigBearerTokenExtractor

需要过滤哪些加入白名单的接口,哪些接口是没token的。

PigLocalResourceServerTokenServices类

这个类直接不用调用 pig-auth去校验token了。直接使用TokenStore。从redis校验

  1. /**
  2. * @author lengleng
  3. * @date 2020/9/29
  4. */
  5. @RequiredArgsConstructor
  6. public class PigLocalResourceServerTokenServices implements ResourceServerTokenServices {
  7. private final TokenStore tokenStore;
  8. private final UserDetailsService userDetailsService;
  9. @Override
  10. public OAuth2Authentication loadAuthentication(String accessToken)
  11. throws AuthenticationException, InvalidTokenException {
  12. OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
  13. if (oAuth2Authentication == null) {
  14. return null;
  15. }
  16. OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
  17. if (!(oAuth2Authentication.getPrincipal() instanceof PigUser)) {
  18. return oAuth2Authentication;
  19. }
  20. // 根据 username 查询 spring cache 最新的值 并返回
  21. PigUser pigxUser = (PigUser) oAuth2Authentication.getPrincipal();
  22. UserDetails userDetails;
  23. try {
  24. userDetails = userDetailsService.loadUserByUsername(pigxUser.getUsername());
  25. }
  26. catch (UsernameNotFoundException notFoundException) {
  27. throw new UnauthorizedException(String.format("%s username not found", pigxUser.getUsername()),
  28. notFoundException);
  29. }
  30. Authentication userAuthentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A",
  31. userDetails.getAuthorities());
  32. OAuth2Authentication authentication = new OAuth2Authentication(oAuth2Request, userAuthentication);
  33. authentication.setAuthenticated(true);
  34. return authentication;
  35. }
  36. @Override
  37. public OAuth2AccessToken readAccessToken(String accessToken) {
  38. throw new UnsupportedOperationException("Not supported: read access token");
  39. }
  40. }

PigSecurityBeanDefinitionRegistrar类

  1. @Slf4j
  2. public class PigSecurityBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
  3. /**
  4. * 根据注解值动态注入资源服务器的相关属性
  5. * @param metadata 注解信息
  6. * @param registry 注册器
  7. */
  8. @Override
  9. public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
  10. if (registry.isBeanNameInUse(SecurityConstants.RESOURCE_SERVER_CONFIGURER)) {
  11. log.warn("本地存在资源服务器配置,覆盖默认配置:" + SecurityConstants.RESOURCE_SERVER_CONFIGURER);
  12. return;
  13. }
  14. GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
  15. beanDefinition.setBeanClass(PigResourceServerConfigurerAdapter.class);
  16. registry.registerBeanDefinition(SecurityConstants.RESOURCE_SERVER_CONFIGURER, beanDefinition);
  17. }
  18. }
  1. 这个写法告诉我们Common包和某些本地的bean 可能会冲突时。可以这样解决

PigResourceServerConfigurerAdapter资源服务器配置类

这里之前一般使用的RemoteTokenServices,现在改成本地校验token了。

  1. @Slf4j
  2. public class PigResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
  3. /**
  4. * 资源服务器认证,处理异常自定义逻辑
  5. */
  6. @Autowired
  7. protected ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
  8. /*
  9. 可以不用注入了
  10. */
  11. @Autowired
  12. protected RemoteTokenServices remoteTokenServices;
  13. /**
  14. * 无权访问时的处理逻辑
  15. */
  16. @Autowired
  17. private AccessDeniedHandler pigAccessDeniedHandler;
  18. @Autowired
  19. private PermitAllUrlProperties permitAllUrl;
  20. @Autowired
  21. private PigBearerTokenExtractor pigBearerTokenExtractor;
  22. @Autowired
  23. private ResourceServerTokenServices resourceServerTokenServices;
  24. /**
  25. * 默认的配置,对外暴露
  26. * @param httpSecurity
  27. */
  28. @Override
  29. @SneakyThrows
  30. public void configure(HttpSecurity httpSecurity) {
  31. // 允许使用iframe 嵌套,避免swagger-ui 不被加载的问题
  32. httpSecurity.headers().frameOptions().disable();
  33. ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
  34. .authorizeRequests();
  35. permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
  36. registry.anyRequest().authenticated().and().csrf().disable();
  37. }
  38. @Override
  39. public void configure(ResourceServerSecurityConfigurer resources) {
  40. resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint).tokenExtractor(pigBearerTokenExtractor)
  41. .accessDeniedHandler(pigAccessDeniedHandler).tokenServices(resourceServerTokenServices);
  42. }
  43. }