最近看到一个有趣的开源项目pig,主要的技术点在认证授权中心,spring security oauth,zuul网关实现,Elastic-Job定时任务,趁着刚刚入门微服务,赶快写个博客分析一下。此篇文章主要用于个人备忘。如果有不对,请批评。?
由于每个模块篇幅较长,且部分内容和前文有重叠,干货和图片较少,阅读时使用旁边的导航功能体验较佳。?
想要解锁更多新姿势?请访问https://blog.tengshe789.tech/
说明
本篇文章是对基于spring boot
1.5的pig 1
版本做的分析,不是收费的pigx 2
版本。
开源项目地址
冷冷官方地址
体验地址
pigx.pig4cloud.com/#/wel/index
项目启动顺序
请确保启动顺序(要先启动认证中心,再启动网关)
- eureka
- config
- auth
- gateway
- upms
认证中心
老规矩,自上到下看代码,先从接口层看起
请求rest接口
@RestController
@RequestMapping("/authentication")
public class AuthenticationController {
@Autowired
@Qualifier("consumerTokenServices")
private ConsumerTokenServices consumerTokenServices;
/**
* 认证页面
* @return ModelAndView
*/
@GetMapping("/require")
public ModelAndView require() {
return new ModelAndView("ftl/login");
}
/**
* 用户信息校验
* @param authentication 信息
* @return 用户信息
*/
@RequestMapping("/user")
public Object user(Authentication authentication) {
return authentication.getPrincipal();
}
/**
* 清除Redis中 accesstoken refreshtoken
*
* @param accesstoken accesstoken
* @return true/false
*/
@PostMapping("/removeToken")
@CacheEvict(value = SecurityConstants.TOKEN_USER_DETAIL, key = "#accesstoken")
public R<Boolean> removeToken(String accesstoken) {
return new R<>( consumerTokenServices.revokeToken(accesstoken));
}
}
复制代码
接口层有三个接口路径,第一个应该没用,剩下两个是校验用户信息的/user
和清除Redis中 accesstoken 与refreshtoken的/removeToken
框架配置
框架配置
下面这段代码时配置各种spring security
配置,包括登陆界面url是"/authentication/require"
啦。如果不使用默认的弹出框而使用自己的页面,表单的action是"/authentication/form"
啦。使用自己定义的过滤规则啦。禁用csrf
啦(自行搜索csrf,jwt验证不需要防跨域,但是需要使用xss过滤)。使用手机登陆配置啦。
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER - 1)
@Configuration
@EnableWebSecurity
public class PigSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private FilterIgnorePropertiesConfig filterIgnorePropertiesConfig;
@Autowired
private MobileSecurityConfigurer mobileSecurityConfigurer;
@Override
public void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry =
http.formLogin().loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests();
filterIgnorePropertiesConfig.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
registry.anyRequest().authenticated()
.and()
.csrf().disable();
http.apply(mobileSecurityConfigurer);
}
}
复制代码
校验用户信息
读配置类和接口层,我们知道了,总的逻辑大概就是用户登陆了以后,使用spring security框架的认证来获取权限。
我们一步一步看,边猜想边来。接口处有"ftl/login"
,这大概就是使用freemarker模板,login信息携带的token
会传到用户信息校验url"/user"
上,可作者直接使用Authentication
返回一个getPrincipal()
,就没了,根本没看见自定义的代码,这是怎么回事呢?
原来,作者使用spring security
框架,使用框架来实现校验信息。
打卡config
包下的PigAuthorizationConfig
,我们来一探究竟。
使用spring security 实现 授权服务器
注明,阅读此处模块需要OAUTH基础,blog.tengshe789.tech/2018/12/02/…
这里简单提一下,spring security oauth
里有两个概念,授权服务器和资源服务器。
授权服务器是根据授权许可给访问的客户端发放access token
令牌的,提供认证、授权服务;
资源服务器需要验证这个access token
,客户端才能访问对应服务。
客户详细信息服务配置
ClientDetailsServiceConfigurer
(AuthorizationServerConfigurer
的一个回调配置项) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),Spring Security OAuth2
的配置方法是编写@Configuration
类继承AuthorizationServerConfigurerAdapter
,然后重写void configure(ClientDetailsServiceConfigurer clients)
方法
下面代码主要逻辑是,使用spring security
框架封装的简单sql连接器,查询客户端的详细信息?
@Override
public void configure(` clients) throws Exception {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);
clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);
clients.withClientDetails(clientDetailsService);
}
复制代码
相关的sql语句如下,由于耦合度较大,我将sql声明语句改了一改,方面阅读:
/**
* 默认的查询语句
*/
String DEFAULT_FIND_STATEMENT = "select " + "client_id, client_secret, resource_ids, scope, "
+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
+ "refresh_token_validity, additional_information, autoapprove"
+ " from sys_oauth_client_details" + " order by client_id";
/**
* 按条件client_id 查询
*/
String DEFAULT_SELECT_STATEMENT = "select " +"client_id, client_secret, resource_ids, scope, "
+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
+ "refresh_token_validity, additional_information, autoapprove"
+ " from sys_oauth_client_details" + " where client_id = ?";
复制代码
相关数据库信息如下:
授权服务器端点配置器
endpoints
参数是什么?所有获取令牌的请求都将会在Spring MVC controller endpoints
中进行处理
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
//token增强配置
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
endpoints
.tokenStore(redisTokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.reuseRefreshTokens(false)
.userDetailsService(userDetailsService);
}
复制代码
token增强器(自定义token信息中携带的信息)
有时候需要额外的信息加到token返回中,这部分也可以自定义,此时我们可以自定义一个TokenEnhancer
,来自定义生成token携带的信息。TokenEnhancer
接口提供一个 enhance(OAuth2AccessToken var1, OAuth2Authentication var2)
方法,用于对token信息的添加,信息来源于OAuth2Authentication
。
作者将生成的accessToken
中,加上了自己的名字,加上了userId
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
final Map<String, Object> additionalInfo = new HashMap<>(2);
additionalInfo.put("license", SecurityConstants.PIG_LICENSE);
UserDetailsImpl user = (UserDetailsImpl) authentication.getUserAuthentication().getPrincipal();
if (user != null) {
additionalInfo.put("userId", user.getUserId());
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
复制代码
JWT转换器(自定义token信息中添加的信息)
JWT中,需要在token中携带额外的信息,这样可以在服务之间共享部分用户信息,spring security默认在JWT的token中加入了user_name,如果我们需要额外的信息,需要自定义这部分内容。
JwtAccessTokenConverter
是使用JWT
替换默认的Token的转换器,而token令牌默认是有签名的,且资源服务器需要验证这个签名。此处的加密及验签包括两种方式:
-
对称加密
-
非对称加密(公钥密钥)
对称加密需要授权服务器和资源服务器存储同一key值,而非对称加密可使用密钥加密,暴露公钥给资源服务器验签
public class PigJwtAccessTokenConverter extends JwtAccessTokenConverter {
@Override
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
Map<String, Object> representation = (Map<String, Object>) super.convertAccessToken(token, authentication);
representation.put("license", SecurityConstants.PIG_LICENSE);
return representation;
}
@Override
public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
return super.extractAccessToken(value, map);
}
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
return super.extractAuthentication(map);
}
}
复制代码
redis与token
使用鉴权的endpoint
将加上自己名字的token
放入redis
,redis连接器用的srping data redis
框架
/**
* tokenstore 定制化处理
*
* @return TokenStore
* 1. 如果使用的 redis-cluster 模式请使用 PigRedisTokenStore
* PigRedisTokenStore tokenStore = new PigRedisTokenStore();
* tokenStore.setRedisTemplate(redisTemplate);
*/
@Bean
public TokenStore redisTokenStore() {
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
tokenStore.setPrefix(SecurityConstants.PIG_PREFIX);
return tokenStore;
}
复制代码
授权服务器安全配置器
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()");
}
复制代码
自定义实现的手机号 认证服务
接口层
先看接口层,这里和pig-upms-service
联动,给了三个路径,用户使用手机号码登陆可通过三个路径发送请求
@FeignClient(name = "pig-upms-service", fallback = UserServiceFallbackImpl.class)
public interface UserService {
/**
* 通过用户名查询用户、角色信息
*
* @param username 用户名
* @return UserVo
*/
@GetMapping("/user/findUserByUsername/{username}")
UserVO findUserByUsername(@PathVariable("username") String username);
/**
* 通过手机号查询用户、角色信息
*
* @param mobile 手机号
* @return UserVo
*/
@GetMapping("/user/findUserByMobile/{mobile}")
UserVO findUserByMobile(@PathVariable("mobile") String mobile);
/**
* 根据OpenId查询用户信息
* @param openId openId
* @return UserVo
*/
@GetMapping("/user/findUserByOpenId/{openId}")
UserVO findUserByOpenId(@PathVariable("openId") String openId);
}
复制代码
配置类
重写SecurityConfigurerAdapter
的方法,通过http请求,找出有关手机号的token,用token找出相关用户的信息,已Authentication
方式保存。拿到信息后,使用过滤器验证
@Component
public class MobileSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler mobileLoginSuccessHandler;
@Autowired
private UserService userService;
@Override
public void configure(HttpSecurity http) throws Exception {
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
mobileAuthenticationFilter.setAuthenticationSuccessHandler(mobileLoginSuccessHandler);
MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider();
mobileAuthenticationProvider.setUserService(userService);
http.authenticationProvider(mobileAuthenticationProvider)
.addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
复制代码
手机号登录校验逻辑MobileAuthenticationProvider
在spring security
中,AuthenticationManage
管理一系列的AuthenticationProvider
, 而每一个Provider
都会通UserDetailsService
和UserDetail
来返回一个 以MobileAuthenticationToken
实现的带用户以及权限的Authentication
此处逻辑是,通过UserService
查找已有用户的手机号码,生成对应的UserDetails
,使用UserDetails生成手机验证Authentication
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;
UserVO userVo = userService.findUserByMobile((String) mobileAuthenticationToken.getPrincipal());
if (userVo == null) {
throw new UsernameNotFoundException("手机号不存在:" + mobileAuthenticationToken.getPrincipal());
}
UserDetailsImpl userDetails = buildUserDeatils(userVo);
MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationToken.setDetails(mobileAuthenticationToken.getDetails());
return authenticationToken;
}
private UserDetailsImpl buildUserDeatils(UserVO userVo) {
return new UserDetailsImpl(userVo);
}
@Override
public boolean supports(Class<?> authentication) {
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
复制代码
手机号登录令牌类MobileAuthenticationToken
MobileAuthenticationToken
继承AbstractAuthenticationToken
实现Authentication
所以当在页面中输入手机之后首先会进入到MobileAuthenticationToken
验证(Authentication), 然后生成的Authentication
会被交由我上面说的AuthenticationManager
来进行管理
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
public MobileAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public MobileAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
复制代码
手机号登录验证filter
判断http请求是否是post,不是则返回错误。
根据request请求拿到moblie信息,使用moblie信息返回手机号码登陆成功的oauth token。
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(mobile);
setDetails(request, mobileAuthenticationToken);
return this.getAuthenticationManager().authenticate(mobileAuthenticationToken);
}
复制代码
手机登陆成功的处理器MobileLoginSuccessHandler
这个处理器可以返回手机号登录成功的oauth token
,但是要将oauth token
传输出去必须配合上面的手机号登录验证filter
逻辑都在注释中
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith(BASIC_)) {
throw new