项目简介
pig项目是目前(2021-10-30)的gitee上排名第一的开源项目。Pig使用的也是Spring Security Oauth2这一套框架。所以,今天就来好好整理一下整体的一个写法。
https://gitee.com/log4j/pig.git
登录演示
- 登录地址使用的是框架(Spring Security Oauth2)提供的token发布接口
- 使用的是password授权类型
项目使用的是password授权方式,那么必定需要
AuthenticationManager
Oauth2必须有客户端信息,具体存放位置是哪里?
- Token的存放方式?Redis?Jwt?
- 如何自定义授权方式
- SpringSecurity配置类的作用?怎么配置?
问题1
- 项目使用的是password授权方式,那么必定需要
AuthenticationManager
从哪里来
答:AuthenticationManager一定是有SpringSecurity配置类中@Bean
注册到容器中。自然需要有SpringSecurity的相关配置
问题2
- Oauth2必须有客户端信息,具体存放位置是哪里?
答:作者重写了SpringSecurityOauth2提供的JdbcClientService。使用自己的表存储查询客户端信息
@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
private final UserDetailsService userDetailsService;
private final AuthenticationManager authenticationManager;
private final TokenStore redisTokenStore;
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
clients.withClientDetails(pigClientDetailsService());
}
@Bean
public ClientDetailsService pigClientDetailsService() {
PigClientDetailsService clientDetailsService = new PigClientDetailsService(dataSource);
clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);
clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);
return clientDetailsService;
}
}
问题3
- Token的存放方式
答:作者将Token选择存放在Redis中
/**
* @author lengleng
* @date 2019/2/1 认证服务器配置
*/
@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
private final UserDetailsService userDetailsService;
private final AuthenticationManager authenticationManager;
private final TokenStore redisTokenStore;
/*
令牌增强器的作用就是调用oauth/token后返回的字段的增加
{
"access_token": "b9e968ff-5ce2-42b6-b726-6313e11f1239",
"token_type": "bearer",
"refresh_token": "dd5f65df-8792-47d3-a10e-2985481d7b2b",
"expires_in": 43199,
"scope": "server",
"license": "made by pig" (这行就是添加的)
}
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
final Map<String, Object> additionalInfo = new HashMap<>(4);
additionalInfo.put(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PROJECT_LICENSE);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
/*
1.这块原来不是这样的,最近正版又被改了。
2.自定义了TokenService
3.自定义了TokenStore
4.但是最终还是Redis存储
5.太厉害了,改这一套要看好多源码的。
*/
@Bean
public PigCustomTokenServices tokenServices() {
PigCustomTokenServices tokenServices = new PigCustomTokenServices();
tokenServices.setTokenStore(redisTokenStore);
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(false);
tokenServices.setClientDetailsService(pigClientDetailsService());
tokenServices.setTokenEnhancer(tokenEnhancer());
addUserDetailsService(tokenServices, userDetailsService);
return tokenServices;
}
/*
这行我不太了解。
但是目的就是把 userDetailService放入 tokenService中
*/
private void addUserDetailsService(PigCustomTokenServices tokenServices, UserDetailsService userDetailsService) {
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService));
tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
}
}
}
问题5
如何自定义授权方式
答:pig团队自定义了一种授权类型并配置到框架中
/**
* @author lengleng
* @date 2019/2/1 认证服务器配置
*/
@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
private final UserDetailsService userDetailsService;
private final AuthenticationManager authenticationManager;
private final TokenStore redisTokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenServices(tokenServices())
.tokenStore(redisTokenStore).tokenEnhancer(tokenEnhancer()).userDetailsService(userDetailsService)
.authenticationManager(authenticationManager).reuseRefreshTokens(false)
.pathMapping("/oauth/confirm_access", "/token/confirm_access")
.exceptionTranslator(new PigWebResponseExceptionTranslator());
setTokenGranter(endpoints);
}
private void setTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
// 获取默认授权类型
TokenGranter tokenGranter = endpoints.getTokenGranter();
ArrayList<TokenGranter> tokenGranters = new ArrayList<>(Arrays.asList(tokenGranter));
ResourceOwnerCustomeAppTokenGranter resourceOwnerCustomeAppTokenGranter = new ResourceOwnerCustomeAppTokenGranter(
authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(),
endpoints.getOAuth2RequestFactory());
tokenGranters.add(resourceOwnerCustomeAppTokenGranter);
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranters);
endpoints.tokenGranter(compositeTokenGranter);
}
}
问题5
- SpringSecurity配置类的作用?怎么配置?
答:这边SpringSecurity配置类的出现是为了配置:
AuthenticationManager
PasswordEncoder
- 放行 oauth2 相关端口
- 其他系统oauth2认证的登录页面和退出处理定义 ```java
@Primary @Order(90) @Configuration @AllArgsConstructor public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/*
UserDetail在 common-security中定义并注册好了
*/
private final UserDetailsService userDetailsService;
@Override
@SneakyThrows
protected void configure(HttpSecurity http) {
http.authenticationProvider(phoneAuthenticationProvider()).formLogin().loginPage("/token/login")
//定义Oauth2登录页面
.loginProcessingUrl("/token/form").failureHandler(authenticationFailureHandler()).and().logout()
//退出登录处理
.logoutSuccessHandler(logoutSuccessHandler()).deleteCookies("JSESSIONID").invalidateHttpSession(true)
//放行 oauth接口 和 健康监控接口
.and().authorizeRequests().antMatchers("/token/**", "/actuator/**", "/mobile/**").permitAll()
.anyRequest().authenticated().and().csrf().disable();
}
/**
* 不要直接使用@Bean注入 会导致默认的提供者无法注入(DaoAuthenticationProvider)
* 自定义的一个认证提供者
*/
private CustomAppAuthenticationProvider phoneAuthenticationProvider() {
CustomAppAuthenticationProvider phoneAuthenticationProvider = new CustomAppAuthenticationProvider();
phoneAuthenticationProvider.setUserDetailsService(userDetailsService);
return phoneAuthenticationProvider;
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/css/**");
}
@Bean
@Override
@SneakyThrows
public AuthenticationManager authenticationManagerBean() {
return super.authenticationManagerBean();
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new FormAuthenticationFailureHandler();
}
/**
* 支持SSO 退出
* @return LogoutSuccessHandler
*/
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new SsoLogoutSuccessHandler();
}
/**
* https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-updated
* Encoded password does not look like BCrypt
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
<a name="ceIgE"></a>
# 退出登录
![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)
- 退出登录调用的是自定义的一个接口
- 成功后,会发送SpringEvent事件。然后Common中定义了处理逻辑(记日志)
<a name="v55XX"></a>
# Security-Common包解读
![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)
- util:主要是token从header中解析和用户信息获取工具类
- handler:主要定义了一些Spring Event处理器(登录成功、失败)
- exception:自定义异常包
- service:特定的UserDetailService
<a name="dh0mr"></a>
## Service包下内容
Pig项目用户登录使用password方式进行授权。用户登录时会使用SpringSecurity校验用户的账号和密码。校验逻辑通过自定义UserDetailService来实现。
<a name="O5gC9"></a>
### PigClientDetailsService自定义OauthClientService
这个就是自定义的一个ClientService。
<a name="AqHnF"></a>
### SpringSecurity用户实体类
UserDetailService需要返回一个UserDetail对象。Pig项目自定义了自己的`PigUser`类(继承User类)来实现UserDetail的实现。
```java
/**
* @author lengleng
* @date 2019/2/1 扩展用户信息
*/
public class PigUser extends User {
/**
* 用户ID
*/
@Getter
private final Integer id;
/**
* 部门ID
*/
@Getter
private final Integer deptId;
/**
* Construct the <code>User</code> with the details required by
* {@link DaoAuthenticationProvider}.
* @param id 用户ID
* @param deptId 部门ID
* @param username the username presented to the
* <code>DaoAuthenticationProvider</code>
* @param password the password that should be presented to the
* <code>DaoAuthenticationProvider</code>
* @param enabled set to <code>true</code> if the user is enabled
* @param accountNonExpired set to <code>true</code> if the account has not expired
* @param credentialsNonExpired set to <code>true</code> if the credentials have not
* expired
* @param accountNonLocked set to <code>true</code> if the account is not locked
* @param authorities the authorities that should be granted to the caller if they
* presented the correct username and password and the user is enabled. Not null.
* @throws IllegalArgumentException if a <code>null</code> value was passed either as
* a parameter or as an element in the <code>GrantedAuthority</code> collection
*/
public PigUser(Integer id, Integer deptId, String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.id = id;
this.deptId = deptId;
}
}
PigUserDetailsServiceImpl用户登录逻辑类
这个类完成具体的登录逻辑,具体流程是:
- 先从cache中拿去PigUser,若有直接返回
- 调用system服务,去查询用户信息
- 封装PigUser信息(之后SpringSecurityOauth2框架会自动的比较密码)
PigCustomTokenServices令牌服务
主要负责令牌的刷新创建。在之前还是没有这个类的。之前使用框架自带的DefaultTokenServices。grant包下内容
grant包下的所有内容都是为了平台能够完成手机号+验证码的登录方式做的功能。但是目前这个功能我还没还有实际使用上。还不太清楚其中某些类的用法。
fegin包下内容
外部直接调用服务,框架会校验token,但是服务之间调用的话 token是不会自己带着的。所有这个包下就是解决这个问题的。
annotation包下内容
注解包下主要有两个注解。@Inner的作用是做切面标识使用。标识某个接口是否为内部调用接口。
@Slf4j
@Aspect
@RequiredArgsConstructor
public class PigSecurityInnerAspect implements Ordered {
private final HttpServletRequest request;
@SneakyThrows
@Around("@within(inner) || @annotation(inner)")
public Object around(ProceedingJoinPoint point, Inner inner) {
// 实际注入的inner实体由表达式后一个注解决定,即是方法上的@Inner注解实体,若方法上无@Inner注解,则获取类上的
if (inner == null) {
Class<?> clazz = point.getTarget().getClass();
inner = AnnotationUtils.findAnnotation(clazz, Inner.class);
}
String header = request.getHeader(SecurityConstants.FROM);
//内部 true 且 但是来源不是内部
if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
log.warn("访问接口 {} 没有权限", point.getSignature().getName());
throw new AccessDeniedException("Access is denied");
}
return point.proceed();
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
- AnnotationUtils找类上的注解
- 默认标识接口,接口的调用者必须是通过fegin调用
- 如果想通过postman直接调用,则使用@Inner(false);
@EnablePigResourceServer这个注解值得学习一下。一般写Common包。通过这个注解开启一些功能。
@Documented
@Inherited
@EnableResourceServer
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Import({ PigResourceServerAutoConfiguration.class, PigSecurityBeanDefinitionRegistrar.class,
PigResourceServerTokenRelayAutoConfiguration.class, PigFeignClientConfiguration.class })
public @interface EnablePigResourceServer {
}
Component包下内容
PigResourceServerAutoConfiguration类
框架的ResourceServer的核心配置类。除了pig-auth外,其他的所有微服务都是一个资源服务器。
@EnableConfigurationProperties(PermitAllUrlProperties.class)
public class PigResourceServerAutoConfiguration {
/*
定义权限校验Service
*/
@Bean("pms")
public PermissionService permissionService() {
return new PermissionService();
}
/*
注册 无权限访问的处理类
*/
@Bean
public PigAccessDeniedHandler pigAccessDeniedHandler(ObjectMapper objectMapper) {
return new PigAccessDeniedHandler(objectMapper);
}
/*
注册自己token提取器
*/
@Bean
public PigBearerTokenExtractor pigBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
return new PigBearerTokenExtractor(urlProperties);
}
/*
没有token时的处理器
*/
@Bean
public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper) {
return new ResourceAuthExceptionEntryPoint(objectMapper);
}
/*
注册自己的ResourceServerTokenService 资源服务器token解析器
这里之后就不照着 常规去再 配置 ResourceAdapter了。
*/
@Bean
@Primary
public ResourceServerTokenServices resourceServerTokenServices(TokenStore tokenStore,
UserDetailsService userDetailsService) {
return new PigLocalResourceServerTokenServices(tokenStore, userDetailsService);
}
}
PermissionService类
PigBearerTokenExtractor
需要过滤哪些加入白名单的接口,哪些接口是没token的。
PigLocalResourceServerTokenServices类
这个类直接不用调用 pig-auth去校验token了。直接使用TokenStore。从redis校验
/**
* @author lengleng
* @date 2020/9/29
*/
@RequiredArgsConstructor
public class PigLocalResourceServerTokenServices implements ResourceServerTokenServices {
private final TokenStore tokenStore;
private final UserDetailsService userDetailsService;
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
if (oAuth2Authentication == null) {
return null;
}
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
if (!(oAuth2Authentication.getPrincipal() instanceof PigUser)) {
return oAuth2Authentication;
}
// 根据 username 查询 spring cache 最新的值 并返回
PigUser pigxUser = (PigUser) oAuth2Authentication.getPrincipal();
UserDetails userDetails;
try {
userDetails = userDetailsService.loadUserByUsername(pigxUser.getUsername());
}
catch (UsernameNotFoundException notFoundException) {
throw new UnauthorizedException(String.format("%s username not found", pigxUser.getUsername()),
notFoundException);
}
Authentication userAuthentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A",
userDetails.getAuthorities());
OAuth2Authentication authentication = new OAuth2Authentication(oAuth2Request, userAuthentication);
authentication.setAuthenticated(true);
return authentication;
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
}
PigSecurityBeanDefinitionRegistrar类
@Slf4j
public class PigSecurityBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* 根据注解值动态注入资源服务器的相关属性
* @param metadata 注解信息
* @param registry 注册器
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
if (registry.isBeanNameInUse(SecurityConstants.RESOURCE_SERVER_CONFIGURER)) {
log.warn("本地存在资源服务器配置,覆盖默认配置:" + SecurityConstants.RESOURCE_SERVER_CONFIGURER);
return;
}
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(PigResourceServerConfigurerAdapter.class);
registry.registerBeanDefinition(SecurityConstants.RESOURCE_SERVER_CONFIGURER, beanDefinition);
}
}
这个写法告诉我们Common包和某些本地的bean 可能会冲突时。可以这样解决
PigResourceServerConfigurerAdapter资源服务器配置类
这里之前一般使用的RemoteTokenServices,现在改成本地校验token了。
@Slf4j
public class PigResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
/**
* 资源服务器认证,处理异常自定义逻辑
*/
@Autowired
protected ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
/*
可以不用注入了
*/
@Autowired
protected RemoteTokenServices remoteTokenServices;
/**
* 无权访问时的处理逻辑
*/
@Autowired
private AccessDeniedHandler pigAccessDeniedHandler;
@Autowired
private PermitAllUrlProperties permitAllUrl;
@Autowired
private PigBearerTokenExtractor pigBearerTokenExtractor;
@Autowired
private ResourceServerTokenServices resourceServerTokenServices;
/**
* 默认的配置,对外暴露
* @param httpSecurity
*/
@Override
@SneakyThrows
public void configure(HttpSecurity httpSecurity) {
// 允许使用iframe 嵌套,避免swagger-ui 不被加载的问题
httpSecurity.headers().frameOptions().disable();
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
registry.anyRequest().authenticated().and().csrf().disable();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint).tokenExtractor(pigBearerTokenExtractor)
.accessDeniedHandler(pigAccessDeniedHandler).tokenServices(resourceServerTokenServices);
}
}