spring security权限拦截
OAuth2的概念请参考深入理解OAuth2协议和使用场景。Servlet 过滤器可以动态地拦截Http请求和响应,在真正执行servlet逻辑之前自定义请求的处理。Servlet 过滤器Filter的接口定义如下:
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
/**
* 过滤请求
*/
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
在spring security 中,主要是通过FilterChainProxy(Servlet Filter过滤器)实现了Filter接口,对Http请求进行拦截和控制的,定义了一组过滤器(filterChains),实现了对认证和授权的各种拦截,FilterChainProxy源码如下:
public class FilterChainProxy extends GenericFilterBean {
/**
* 权限过滤器链
*/
private List<SecurityFilterChain> filterChains;
/**
* 请求拦截
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
// 匹配请求的过滤器列表
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
// 过滤器链过滤
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
}
spring security对权限拦截的大致流程如下:
的在spring security 中常见的Filter如下:
- SecurityContextPersistenceFilter,用于在认证前后设置SecurityContext值,认证通过以后,会把认证的用户信息保存保存到SecurityContext。
- UsernamePasswordAuthenticationFilter,用户名和密码验证过滤器,拦截/login请求,对用户进行身份验证。
- FilterSecurityInterceptor,拦截资源服务器的请求,进行资源授权。
spring security的认证逻辑,通过AuthenticationManager接口实现,源码如下:
public interface AuthenticationManager {
/**
* 身份认证
*/
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
ProviderManager为AuthenticationManager,定义了AuthenticationProvider列表,扩展了身份认证逻辑,AuthenticationProvider的接口定义如下:
public interface AuthenticationProvider {
/**
* 身份认证
*/
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
ProviderManager的部分源码如下:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 认证逻辑列表
private List<AuthenticationProvider> providers = Collections.emptyList();
/**
* 身份认证逻辑
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
// 遍历身份认证列表
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 身份认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
}
......
}
在 AuthenticationProvider的子类实现中丰富了身份认证逻辑,常见的实现类的如下:
- DaoAuthenticationProvider,用户信息认证,最常使用
- PreAuthenticatedAuthenticationProvider,身份预先认证逻辑
- RememberMeAuthenticationProvider,记住我身份认证
spring security OAuth2拦截流程
spring security OAuth2作为spring security的子项目,对于web安全的控制也是通过Servlet 过滤器调用链实现的,在spring security的基础上,新增了自己特有的过滤器和服务端点,实现OAuth2中定义的客户端,授权服务器,资源服务器,访问授权码等等概念的逻辑。例如,新增的服务端点:
- /oauth/authorize GET,获取客户端的授权码
- /oauth/authorize POST,获取隐式授权类型(Implicit)的获取访问令牌
- /oauth/token,用于获取除隐式类型以外的授权类型的访问令牌,以及刷新访问令牌
- /oauth/check_token,用于验证访问令牌
新增ClientCredentialsTokenEndpointFilter,拦截/oauth/token http请求,用户对客户端进行验证,源码如下:
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
public ClientCredentialsTokenEndpointFilter() {
// 拦截/oauth/token url
this("/oauth/token");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] { "POST" });
}
// 客户端信息
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
// If the request is already authenticated we can assume that this
// filter is not needed
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
}
if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
}
if (clientSecret == null) {
clientSecret = "";
}
clientId = clientId.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
// 验证客户端
return this.getAuthenticationManager().authenticate(authRequest);
}
}
以下用户密码类型授权方式,讲述下spring security OAuth2的身份认证,大致流程如下:
- 在客户端直接访问/oauth/token 发起获取访问授权码的请求。
- 请求会被ClientCredentialsTokenEndpointFilter拦截,在该过滤器中,会对客户端信息进行认证。
- 认证通过后会访问到在TokenEndpoint中定义的/oauth/token 逻辑,通过TokenGranter对用户身份进行认证,生成访问授权码。
TokenGranter生成器
在spring security OAuth2,定义了适用于OAuth2认证类型的TokenGranter子类,实现类如下:
- ClientCredentialsTokenGranter,客户端授权类型的TokenGranter
- ImplicitTokenGranter,简化授权类型的TokenGranter
- AuthorizationCodeTokenGranter,授权码授权类型的TokenGranter
- ResourceOwnerPasswordTokenGranter,密码授权类型的TokenGranter
- RefreshTokenGranter,刷新token
- CompositeTokenGranter,组合TokenGranter,根据类型生成访问授权码
以上,以密码授权类型(Resource Owner Password Credentials)进行用户身份认证。所以,TokenEndpoint会调用ResourceOwnerPasswordTokenGranter密码授权类型的TokenGranter。在该类中,首先对用户信息(username,password)进行认证,认证通过生成访问令牌access token,用户身份的认证逻辑基于spring security身份认证调用DaoAuthenticationProvider实现。ResourceOwnerPasswordTokenGranter的源码如下:
public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {
// 生成token的授权类型 password
private static final String GRANT_TYPE = "password";
/**
* 对用户信息username和password进行验证
* 生成OAuth2Authentication
*/
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
// 获取username和password信息
String username = parameters.get("username");
String password = parameters.get("password");
// Protect from downstream leaks of password
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
// 使用AuthenticationManager进行验证
userAuth = authenticationManager.authenticate(userAuth);
}
catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
}
catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
搭建认证服务器
在spring security OAuth2中,通过@EnableAuthorizationServer注解开启了授权服务器的默认配置,通过继承AuthorizationServerConfigurerAdapter实现了对于授权服务器的自定义。配置包括三方面的定义:
- ClientDetailsServiceConfigurer,定义了对OAuth2客户端信息的配置。
- AuthorizationServerEndpointsConfigurer,定义了对OAuth2端点的配置,例如对用户服务,访问授权的存储,以及转换的配置。
- AuthorizationServerSecurityConfigurer,定义了安全过滤器链FilterChainProxy的配置,例如设置允许所有人访问令牌,已验证的客户端才能请求check_token。
在实现认证服务器的过程中,使用了redis作为tokenStore;使用jwt对token进行转换;使用了jdbc模式存储客户端信息。源码如下:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
/**
* redis连接工厂
*/
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 用户服务
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 数据源
*/
@Autowired
private DataSource dataSource;
/**
* jwt
*/
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthorizationServerTokenServices tokenServices;
/**
* oauth2客户端服务配置
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置基于jdbc的ClientDetailsService,用于加载客户端信息
clients.withClientDetails(ClientDetailsService());
}
/**
* 使用jdbc方式,配置客户端服务
*/
@Bean
public ClientDetailsService ClientDetailsService() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
// 设置自定义加密规则,默认不加密
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
/**
* 授权服务端点配置
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
// 配置tokenStore,使用redis 存储token
.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
// 用户管理服务
.userDetailsService(userDetailsService)
// 配置令牌转化器
.accessTokenConverter(jwtAccessTokenConverter)
// 允许 GET、POST 请求获取 token,即访问端点:oauth/token
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.reuseRefreshTokens(true);
}
/**
* 授权服务器安全认证的相关配置,生成对应的安全过滤器链,主要是控制oauth/**端点的相关配置,保证授权服务器端点的安全访问
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
// 允许所有人访问令牌
.tokenKeyAccess("permitAll()")
// 已验证的客户端才能请求check_token
.checkTokenAccess("isAuthenticated()")
// 允许表单认证
.allowFormAuthenticationForClients();
}
/**
* jwt token 配置
*/
@Configuration
public static class JwtTokenConfig {
/**
* JwtAccessTokenConverter
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("kuqi-mall");
return converter;
}
}
}
WebSecurityConfig配置源码如下:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 用户服务
*/
@Autowired
private MallUserDetailsService userDetailsService;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/**
* 配置HTTP安全
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated();
}
/**
* 配置密码解码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
在db中,配置了客户端信息(client_id为meituan,client_secret为123456),配置了用户信息(username为admin,password为123456).通过客户端密码类型操作,获取到了access token,使用postman访问成功的截图如下:
不足与优化之处
以上,是基于spring security OAuth2的默认实现配置的。如果,项目中,要求通过电话号码和密码的方式获取access token该如何实现呢?又或者使用电话号码和验证码的方式呢?请关注后续的认证自定义章节。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13632.html