一 . 前言
SpringSecurity 应该是最常见的认证框架了 , 处于Spring体系中使他能快速地上手 , 这一篇开始作为入门级开篇作 , 来浅浅地讲一下SpringSecurity 的整体结构.
关于Security 的使用 , 官方文档已经太详细了, 建议直接查看 官方文档 , 作为开篇 , 不深入源码 , 只做体系的介绍~~
后续会陆陆续续将笔记中的源码梳理出来 ,笔记很乱, 但愿等整理出体系!
二 . Spring Security 知识体系
Security中需要我们操作的成员大概可以分为以下几种 , 但是涉及的类远远不止他们
- Filter : 对请求拦截处理
- Provider : 对用户进行认证
- Token : 用户认证主体
2.1 Spring Security 主要包结构
- spring-security-remoting : 提供与 Spring Remoting 的集成
- spring-security-web : 包含过滤器和相关的网络安全基础设施代码。
- spring-security-config : 包含安全命名空间解析代码和 Java 配置代码 ?- 使用 Spring Security xml 命名空间进行配置或 Srping Security 的 Java Configuration 支持
- spring-security-ldap : 该模块提供 LDAP 身份验证和配置代码
- spring-security-oauth2-core : 包含支持 OAuth 2.0授权框架和 OpenID Connect Core 1.0的核心类和接口
- spring-security-oauth2-client : 包含 Spring Security 对 OAuth 2.0授权框架和 OpenID Connect Core 1.0的客户端支持
- spring-security-oauth2-jose : 包含 Spring Security 对 JOSE (Javascript 对象签名和加密)框架的支持JSON Web Token (JWT)JSON Web Signature (JWS)JSON Web Encryption (JWE)JSON Web Key (JWK)
- spring-security-oauth2-resource-server : 包含 Spring Security 对 OAuth 2.0资源服务器的支持。它通过 OAuth 2.0承载令牌来保护 api
- spring-security-acl : 此模块包含专门的域对象 ACL 实现。它用于对应用程序中的特定域对象实例应用安全性
- spring-security-cas : 该模块包含 Spring Security 的 CAS 客户端集成
- spring-security-openid : 此模块包含 OpenID web 身份验证支持。它用于根据外部 OpenID 服务器对用户进行身份验证。
- spring-security-test
- spring-secuity-taglibs
2.2 Spring Security 核心体系 Filter
SpringSecurity 中存在很多Filter , 抛开一些底层的 , 一般业务中的Filter主要是为了控制以何种方式进行认证 . 一般的体系结构里面 , 都会循环处理 , 例如 CAS 中 , 就是通过 HandlerManager 进行 for each 循环 , 而 SpirngSecurity 中 , 同样通过 SecurityFilterChain 进行循环.
SecurityFilterChain 在后期源码梳理的时候再详细介绍 , 这里先看张图 :
FilterChainProxy 使用 SecurityFilterChain 来确定应该为此请求调用哪个 Spring 安全过滤器 ,
FilterChainProxy 决定应该使用哪个 SecurityFilterChain。会调用第一个被匹配的SecurityFilterChain ,即匹配是有序的
- 如果请求/api/messages/ 的URL,它将首先匹配 SecurityFilterChain0的 /api/** 模式,因此只会调用 SecurityFilterChain0,即使它也匹配 SecurityFilterChainn。
- 如果请求/messages/ 的URL,它将不匹配 SecurityFilterChain0的 /api/** 模式,因此 FilterChainProxy 将继续尝试每个 SecurityFilterChain。假设没有其他的 SecurityFilterChai n实例匹配 SecurityFilterChainn 将被调用。 (即无匹配调用最后一个)
已知的 Filter 类
ChannelProcessingFilter WebAsyncManagerIntegrationFilter SecurityContextPersistenceFilter HeaderWriterFilter CorsFilter CsrfFilter LogoutFilter OAuth2AuthorizationRequestRedirectFilter Saml2WebSsoAuthenticationRequestFilter X509AuthenticationFilter AbstractPreAuthenticatedProcessingFilter CasAuthenticationFilter OAuth2LoginAuthenticationFilter Saml2WebSsoAuthenticationFilter UsernamePasswordAuthenticationFilter OpenIDAuthenticationFilter DefaultLoginPageGeneratingFilter DefaultLogoutPageGeneratingFilter ConcurrentSessionFilter DigestAuthenticationFilter BearerTokenAuthenticationFilter BasicAuthenticationFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter JaasApiIntegrationFilter RememberMeAuthenticationFilter AnonymousAuthenticationFilter OAuth2AuthorizationCodeGrantFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor SwitchUserFilter
2.3 Authentication 体系结构
认证体系是核心的处理体系 , 包含以下主要类 :
SecurityContextHolder : Spring Security 存储被验证者的详细信息的地方。
SecurityContext : 从 SecurityContextHolder 获得,并包含当前经过身份验证的用户的身份验证。。
Authentication : 可以作为 AuthenticationManager 的输入,以提供用户为身份验证或来自 SecurityContext 的当前用户提供的凭据。。
GrantedAuthority : 在身份验证上授予主体的权限(即角色、范围等)。
AuthenticationManager : 定义 Spring Security 的过滤器如何执行身份验证的 API。。
ProviderManager : AuthenticationManager 最常用的实现。。
Providationprovider : 由 ProviderManager 用于执行特定类型的身份验证。。
AuthenticationEntryPoint : 用于从客户机请求凭证(即重定向到登录页面,发送 www 认证响应等)。
AbstractAuthenticationProcessingFilter : 用作验证用户凭据的基本筛选器
AccessDecisionManager : 由
AbstractSecurityInterceptor 调用,负责做出最终的访问控制决策
后续我们会围绕以上类进行源码梳理:
三 . SpringSeurity 案例
以下是一个很简单的 Security 案例 : >>>>项目源码<<<<
3.1 SpringSecurity 依赖和配置
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler myAuthenctiationFailureHandler; @Bean public UserService CustomerUserService() { System.out.print("step1============"); return new UserService(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //BCryptPasswordEncoder().encode("123456")).roles("ADMIN"); auth.userDetailsService(CustomerUserService()).passwordEncoder(new BCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { //此方法中进行了请求授权,用来规定对哪些请求进行拦截 //其中:antMatchers--使用ant风格的路径匹配 //regexMatchers--使用正则表达式匹配 http.authorizeRequests() .antMatchers("/test/**").permitAll() .antMatchers("/before/**").permitAll() .antMatchers("/index").permitAll() .antMatchers("/").permitAll() .anyRequest().authenticated() //其它请求都需要校验才能访问 .and() .formLogin() .loginPage("/login") //定义登录的页面"/login",允许访问 .defaultSuccessUrl("/home") //登录成功后默认跳转到"list" .successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenctiationFailureHandler).permitAll().and() .logout() //默认的"/logout", 允许访问 .logoutSuccessUrl("/index") .permitAll(); http.addFilterBefore(new BeforeFilter(), UsernamePasswordAuthenticationFilter.class); } @Override public void configure(WebSecurity web) throws Exception { //解决静态资源被拦截的问题 web.ignoring().antMatchers("/**/*.js", "/lang/*.json", "/**/*.css", "/**/*.js", "/**/*.map", "/**/*.html", "/**/*.png"); } }
其中有几个主要的地方 :
@EnableWebSecurity 干了什么 ?
- 该配置将创建一个 servlet Filter,作为名为 springSecurityFilterChain 的 bean
- 创建一个具有用户用户名和随机生成地登录到控制台的密码的 UserDetailsService bean
- 为每个请求向 Servlet 容器注册名为 springSecurityFilterChain 的 bean 的 Filter
TODO
3.2 准备一个 UserDetailsService
public class UserService implements UserDetailsService { //...... @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Users user = userRepository.findByUsername(username); // .... return user; }
一个基本的 Demo 就完成了 , 案例能怎么简单 , 其实主要是因为我们复用了以下的类 :
- UsernamePasswordAuthenticationFilter
- DaoAuthenticationProvider
- UsernamePasswordAuthenticationToken
四 . 定制案例
我们把整个结构再定制一下 , 满足我们本身的功能 :
4.1 Step 1 : 定制一个 Filter
// 我们复用 UsernamePasswordAuthenticationFilter , 将其进行部分定制 public class DatabaseAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // 修改用户名为 account public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "account"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true; public DatabaseAuthenticationFilter() { super(new AntPathRequestMatcher("/database/login", "POST")); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } // username 从下方方法获取 String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); // 核心 : 这里替换了 DatabaseUserToken DatabaseUserToken authRequest = new DatabaseUserToken(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainPassword(HttpServletRequest request) { return request.getParameter(passwordParameter); } protected String obtainUsername(HttpServletRequest request) { return request.getParameter(usernameParameter); } protected void setDetails(HttpServletRequest request, DatabaseUserToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return usernameParameter; } public final String getPasswordParameter() { return passwordParameter; } }
4.2 Step 2 : 准备一个 Token
Token 是在Authentication 中传递的核心 , 它用于后续进行票据的认证
public class DatabaseUserToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; private String credentials; private String type; private Collection<? extends GrantedAuthority> authorities; public DatabaseUserToken(Object principal, String credentials) { super(null); this.principal = principal; this.credentials = credentials; this.type = "common"; setAuthenticated(false); } public DatabaseUserToken(Object principal, String credentials, String type) { super(null); this.principal = principal; this.credentials = credentials; this.type = StringUtils.isEmpty(type) ? "common" : type; setAuthenticated(false); } public DatabaseUserToken(Object principal, String credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } public DatabaseUserToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = null; super.setAuthenticated(true); // must use super, as we override } public String getType() { return type; } public void setType(String type) { this.type = type; } /** * @param isAuthenticated * @throws IllegalArgumentException */ public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { super.setAuthenticated(true); } @Override public Object getCredentials() { return credentials; } @Override public Object getPrincipal() { return principal; } }
4.3 Step 3 : 准备一个 Provider
这里通过 supports 方法判断 token 是否符合 ,从而发起认证过程 (PS : 和 CAS 简直一个思路)
public class DatabaseAuthenticationProvider implements AuthenticationProvider { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private UserInfoService userInfoService; @Autowired private AntSSOConfiguration antSSOConfiguration; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { logger.info("------> auth database <-------"); String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : String.valueOf(authentication.getPrincipal()); String password = (String) authentication.getCredentials(); if (StringUtils.isEmpty(password)) { throw new BadCredentialsException("密码不能为空"); } UserInfo user = userInfoService.searchUserInfo(new UserInfoSearchTO<String>(username)); logger.info("------> this is [{}] user :{}<-------", username, String.valueOf(user)); if (null == user) { logger.error("E----> error :{} --user not fount ", username); throw new BadCredentialsException("用户不存在"); } String encodePwd = ""; if (password.length() != 32) { encodePwd = PwdUtils.AESencode(password, AlgorithmConfig.getAlgorithmKey()); logger.info("------> {} encode password is :{} <-------", password, encodePwd); } if (!encodePwd.equals(user.getPassword())) { logger.error("E----> user check error"); throw new BadCredentialsException("用户名或密码不正确"); } else { logger.info("user check success"); } DatabaseUserToken result = new DatabaseUserToken( username, new BCryptPasswordEncoder().encode(password), listUserGrantedAuthorities(user.getUserid())); result.setDetails(authentication.getDetails()); logger.info("------> auth database result :{} <-------", JSONObject.toJSONString(result)); return result; } @Override public boolean supports(Class<?> authentication) { return (DatabaseUserToken.class.isAssignableFrom(authentication)); } private Set<GrantedAuthority> listUserGrantedAuthorities(String uid) { Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); if (StringUtils.isEmpty(uid)) { return authorities; } authorities.add(new SimpleGrantedAuthority("ROLE_USER")); return authorities; } }
4.4 隐藏环节
// 将 Provider 注入体系 auth.authenticationProvider(reflectionUtils.springClassLoad(item.getProvider())); // 将 Filter 注入体系 AbstractAuthenticationProcessingFilter filter = reflectionUtils.classLoadReflect(item.getFilter()); filter.setAuthenticationManager(authenticationManager); http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
以上的流程可以看到已经不需要 继承 UserService类了 . 重写这些足够我们去实现大部分业务逻辑 , 使用时在Provider 完成对应的认证方式即可
五 . 总结
Security 很好用, 在我的个人实践中 , 正在尝试将常见的协议进行整合 , 做成一个开源脚手架 , 个人感觉SpringSecuity 体系应该可以轻松地完成 .
开篇比较简单 , 正在构思怎样才能从实践的角度将他讲清楚 , 笔记也在陆陆续续整理 , 争取下个月将整套文章发出来 !
附录
HttpSecurity 常用方法
作者|AntBlack|掘金
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。