参考文章
spring security 极客学院
spring security 博客园
Spring security 基本流程
Java配置的方式
spring security 大佬博客
spring security CSDN
基于XML配置
/login
/websocket/**
/ureport/**
基于Java配置
package com.hand.sxy.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author spilledyear
* @date 2018/4/24 13:19
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("admin").roles("USER");
}
}
默认验证
当我们项目里添加spring security依赖它就已经起作用了,启动项目访问时,会出现弹出框。spring security默认采用basic模式认证。浏览器发送http报文请求一个受保护的资源,浏览器会弹出对话框让输入用户名和密码。并以用
户名:密码的形式base64加密,加入Http报文头部的Authorization(默认用户名为user,密码则是会在启动程序时后
台console里输出,每次都不一样)。后台获取Http报文头部相关认证信息,认证成功返回相应内容,失败则继续认证。
基本概念
- AuthenticationManager: 身份验证的主要策略设置接口。
- ProviderManager: AuthenticationManager接口的最常用实现类。
- AuthenticationProvider: 是一个接口,ProviderManager委托列表中AuthenticationProvider处理认证工作。
- Authentication: 认证用户信息主体。
- GrantedAuthority: 用户主体的权限。
- UserDetails: 【接口】,通过自定义实现封装用户的基本必要信息。
- UserDetailsService: 【接口】,自定义的实现类通过loadUserByUsername方法返回一个UserDetails对象。
- SecurityContextHolder: 提供访问SecurityContext。
- SecurityContext: 保存Authentication,和一些其它的信息。
ProviderManager把工作委托给AuthenticationProvider集合,对所有AuthenticationProvider进行循环,直到运行返回一个完整的Authentication,不符合条件或者不能认证当前Authentication,返回AuthenticationException异常或者null。
核心过滤器
// 每个日志前面自动加上这个
2018-05-16 13:43:56.700 DEBUG 14448 --- [nio-8081-exec-1] o.s.security.web.FilterChainProxy
: /error at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
: /error at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
: /error at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
: /error at position 4 of 12 in additional filter chain; firing Filter: 'LogoutFilter'
: /error at position 5 of 12 in additional filter chain; firing Filter: 'JwtAuthorizationTokenFilter'
: /error at position 6 of 12 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
: /error at position 7 of 12 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
: /error at position 8 of 12 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
: /error at position 9 of 12 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
o.s.s.w.a.AnonymousAuthenticationFilter : Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@1dcfc3ac: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS'
: /error at position 10 of 12 in additional filter chain; firing Filter: 'SessionManagementFilter'
: /error at position 11 of 12 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
: /error at position 12 of 12 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
以上内容是我格式化之后的日志,代码的顺序不变。可以看到一共有12个过滤器,其中 第五个 过滤器 JwtAuthorizationTokenFilter 是我自定义的,其余的全是Spring Security自带的。也就是说,Spring Security中默认有 11 个过滤器。
- SecurityContextPersistenceFilter 两个主要职责:请求来临时,创建SecurityContext安全上下文信息,请求结束时清空SecurityContextHolder。
- HeaderWriterFilter (文档中并未介绍,非核心过滤器) 用来给http响应添加一些Header,比如X-Frame-Options, X-XSS-Protection*,X-Content-Type-Options.
- CsrfFilter 在spring4这个版本中被默认开启的一个过滤器,用于防止csrf攻击,了解前后端分离的人一定不会对这个攻击方式感到陌生,前后端使用json交互需要注意的一个问题。
- LogoutFilter 顾名思义,处理注销的过滤器
- UsernamePasswordAuthenticationFilter 表单提交了username和password,被封装成token进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。
- RequestCacheAwareFilter (文档中并未介绍,非核心过滤器) 内部维护了一个RequestCache,用于缓存request请求
- SecurityContextHolderAwareRequestFilter 此过滤器对ServletRequest进行了一次包装,使得request具有更加丰富的API
- AnonymousAuthenticationFilter 匿名身份过滤器,这个过滤器个人认为很重要,需要将它与UsernamePasswordAuthenticationFilter 放在一起比较理解,spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
- SessionManagementFilter 和session相关的过滤器,内部维护了一个SessionAuthenticationStrategy,两者组合使用,常用来防止session-fixation protection attack,以及限制同一用户开启多个会话的数量
- ExceptionTranslationFilter 直译成异常翻译过滤器,还是比较形象的,这个过滤器本身不处理异常,而是将认证过程中出现的异常交给内部维护的一些类去处理,具体是那些类下面详细介绍
- FilterSecurityInterceptor 这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。其实这个真的非常非常重要,前面的东西都是和用户认证相关,而这个是控制哪些资源是受限的,这些受限的资源需要什么权限,需要什么角色。
其中加粗的过滤器可以被认为是Spring Security的核心过滤器。在日志中未发现 CsrfFilter ,是因为我在代码中把 csrf保护关闭了。
FilterSecurityInterceptor的工作流程可以理解如下:FilterSecurityInterceptor从SecurityContextHolder中获取Authentication对象,然后比对用户拥有的权限和资源所需的权限。前者可以通过Authentication对象直接获得,而后者则需要引入两个类:SecurityMetadataSource,AccessDecisionManager。
认证流程
AbstractAuthenticationProcessingFilter
用于拦截认证请求,它是基于浏览器和 HTTP 认证请求的处理器,可以理解为它就是 Spring Security 认证流程的入口。
整个认证流程如下:
① AbstractAuthenticationProcessingFilter
收集用于认证的用户身份信息(通常是用户名和密码),并基于这些信息构造一个 Authentication
请求对象,AbstractAuthenticationProcessingFilter
只是一个虚类,查看 Spring Security API 文档 可以看到 Spring Security 提供了几个实现类:
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
OpenIDAuthenticationFilter
UsernamePasswordAuthenticationFilter
最常使用的应该是 UsernamePasswordAuthenticationFilter
,其它类都应用于特定的场景。
② AbstractAuthenticationProcessingFilter
类将构造的 Authentication
请求对象呈现给 AuthenticationManager
,AbstractAuthenticationProcessingFilter
类有以下方法设置和获取 AuthenticationManager
:
protected AuthenticationManager getAuthenticationManager()
public void setAuthenticationManager(AuthenticationManager authenticationManager)
③ AuthenticationManager
只是一个接口,Spring Security 提供了一个默认实现 ProviderManager
。ProviderManager
在接收到 AbstractAuthenticationProcessingFilter
传递过来的 Authentication
请求对象后并不会执行认证处理,它持有一个 AuthenticationProvider
的列表,ProviderManager
委托列表中的 AuthenticationProvider
处理认证请求;
④ AuthenticationProvider
也只是接口,Spring Security 提供了很多此接口的实现,如 DaoAuthenticationProvider
、LdapAuthenticationProvider
、JaasAuthenticationProvider
等,现在暂时不关心这些具体实现。列表中的 AuthenticationProvider
会依次对 Authentication
请求对象进行认证处理,如果认证通过则返回一个完全填充的 Authentication
对象(后面会解释什么是“完全填充”),如果认证不通过则抛出一个异常(注意对抛出的异常有类型要求)或直接返回 null。如果列表中的所有 AuthenticationProvider
都返回 null,则 ProviderManager
会抛出 ProviderNotFoundException
异常;
⑤ 认证通过后 AuthenticationProvider
返回完全填充的 Authentication
对象给 ProviderManager
,ProviderManager
继续向上返回给 AbstractAuthenticationProcessingFilter
,AbstractAuthenticationProcessingFilter
会继续返回。
⑥ Spring Security 的“authentication mechanism”在接收到一个完全填充的 Authentication
对象返回后会认定认证请求有效,并将此 Authentication
对象放入 SecurityContextHolder
。
⑦ SecurityContextHolder
是 Spring Security 最基础的对象,用于存储应用程序当前安全上下文的详细信息,这些信息后续会被用于授权。
核心组件
这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。
SecurityContextHolder
SecurityContextHolder
用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。SecurityContextHolder
默认使用 ThreadLocal
策略来存储认证信息。看到ThreadLocal
也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而如果是Swing界面,Spring也提供了支持,SecurityContextHolder
的策略则需要被替换,鉴于我的初衷是基于web来介绍Spring Security,所以这里以及后续,非web的相关的内容都一笔带过。
获取当前用户的信息
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
getAuthentication()返回了认证信息,再次getPrincipal()返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。Authentication和UserDetails的介绍在下面的小节具体讲解,本节重要的内容是介绍SecurityContextHolder这个容器。
Authentication
先看看这个接口的源码长什么样:
package org.springframework.security.core;// <1>
public interface Authentication extends Principal, Serializable { // <1>
Collection extends GrantedAuthority> getAuthorities(); // <2>
Object getCredentials();// <2>
Object getDetails();// <2>
Object getPrincipal();// <2>
boolean isAuthenticated();// <2>
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于java.security
包中的。可以见得,Authentication在spring security中是最高级别的身份/认证的抽象。
由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。
上面有提到,authentication.getPrincipal()返回了一个Object,我们将Principal强转成了Spring Security中最常用的UserDetails,这在Spring Security中非常常见,接口返回Object,使用instanceof判断类型,强转成对应的具体实现类。接口详细解读如下:
- getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
- getCredentials(),密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
- getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
- getPrincipal(),敲黑板!!!最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。UserDetails接口将会在下面的小节重点介绍。
Spring Security是如何完成身份认证的?
1 用户名和密码被过滤器获取到,封装成Authentication
,通常情况下是UsernamePasswordAuthenticationToken
这个实现类。
2 AuthenticationManager
身份管理器负责验证这个Authentication
。
3 认证成功后,AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication
实例。
4 SecurityContextHolder
安全上下文容器将第3步填充了信息的Authentication
,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
这是一个抽象的认证流程,而整个过程中,如果不纠结于细节,其实只剩下一个AuthenticationManager
是我们没有接触过的了,这个身份管理器我们在后面的小节介绍。将上述的流程转换成代码,便是如下的流程:
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while(true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains: " +
SecurityContextHolder.getContext().getAuthentication());
}
}
class SampleAuthenticationManager implements AuthenticationManager {
static final List AUTHORITIES = new ArrayList();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
public Authentication authenticate(Authentication auth) throws AuthenticationException {
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
注意:上述这段代码只是为了让大家了解Spring Security的工作流程而写的,不是什么源码。在实际使用中,整个流程会变得更加的复杂,但是基本思想,和上述代码如出一辙。
AuthenticationManager
初次接触Spring Security的朋友相信会被AuthenticationManager
,ProviderManager
,AuthenticationProvider
…这么多相似的Spring认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager
内部会维护一个List
列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。这样一来四不四就好理解多了?熟悉shiro的朋友可以把AuthenticationProvider理解成Realm。在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。
只保留了关键认证部分的ProviderManager源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 维护一个AuthenticationProvider列表
private List providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 依次认证
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication信息,则直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//移除密码
((CredentialsContainer) result).eraseCredentials();
}
//发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
...
//执行到此,说明没有认证成功,包装异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
ProviderManager
中的ListProviderManager
会抛出一个ProviderNotFoundException异常。
到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。下面来介绍下AuthenticationProvider接口的具体实现。
DaoAuthenticationProvider
AuthenticationProvider最最最常用的一个实现便是DaoAuthenticationProvider。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。由于本文是一个Overview,姑且只给出其UML类图:
按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
如果你已经被这些概念搞得晕头转向了,不妨这么理解DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
UserDetails与UserDetailsService
上面不断提到了UserDetails这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
public interface UserDetails extends Serializable {
Collection extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混,关于他们的问题在文档的FAQ和issues中屡见不鲜。记住一点即可,敲黑板!!!UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。
架构概览图
为了更加形象的理解上述我介绍的这些核心类,网上找到的一张图
一些Spring Security的过滤器还未囊括在架构概览中,如将表单信息包装成UsernamePasswordAuthenticationToken的过滤器,考虑到这些虽然也是架构的一部分,但是真正重写他们的可能性较小,所以打算放到后面的章节讲解。
案例
用户登陆时会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而且AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个UserDetails放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。
访问资源(即授权管理)访问url时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不足页面。
配置
有关于配置文件,在文章的开头已经给出,这里就不过多介绍了,下面对一些重点内容介绍一下
// 表示/resources/**、/lib/**、/timeout、/verifiCode 不需要过滤
form-login 表示表单认证方式,其实spring security默认采用的时 http-basic 认证方式(弹窗),当我们同时定义了 http-basic 和 form-login 元素时,form-login 将具有更高的优先级。即在需要认证的时候 Spring Security 将引导我们到登录页面,而不是弹出一个窗口。使用form-login认证时,当我们什么属性都不指定的时候 Spring Security 会为我们生成一个默认的登录页面。如果不想使用默认的登录页面,我们可以指定自己的登录页面。
authentication-success-handler-ref="successHandler" 表示验证成功时会调用 CustomAuthenticationSuccessHandler的onAuthenticationSuccess方法,authentication-failure-handler-ref="loginFailureHandler" 表示验证失败时会调用LoginFailureHandler的onAuthenticationFailure方法。
流程
在界面点击登录的时候,被AuthenticationProcessingFilter拦截,依次调用AuthenticationManager中的ProviderManager列表进行验证,ProviderManager的authenticate方法内会调用UserDetailsService的loadUserByUsername方法从数据库或其它地方获取用户信息,customUserDetailsService 即是一个UserDetailsService接口的一个实现,可以看看它的loadUserByUsername方法
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.selectByUserName(username);
if (user == null) {
throw new UsernameNotFoundException("User not found:" + username);
}
checkUserException(user);
Collection authorities = new ArrayList();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
for(String role:user.getRoleCode()){
authorities.add(new SimpleGrantedAuthority(role));
}
UserDetails userDetails = new CustomUserDetails(user.getUserId(), user.getUserName(),
user.getPasswordEncrypted(), true, true, true, true, authorities,user.getEmployeeId(),user.getEmployeeCode());
return userDetails;
}
private void checkUserException(User user) {
UserException ue = null;
if (User.STATUS_LOCK.equalsIgnoreCase(user.getStatus())) {
ue = new UserException(UserException.ERROR_USER_LOCKED, null);
} else if (User.STATUS_EXPR.equalsIgnoreCase(user.getStatus())) {
ue = new UserException(UserException.ERROR_USER_EXPIRED, null);
} else if (user.getStartActiveDate() != null
&& user.getStartActiveDate().getTime() > System.currentTimeMillis()) {
ue = new UserException(UserException.ERROR_USER_NOT_ACTIVE, null);
} else if (user.getEndActiveDate() != null && user.getEndActiveDate().getTime() < System.currentTimeMillis()) {
ue = new UserException(UserException.ERROR_USER_EXPIRED, null);
}
if (ue != null) {
throw new RuntimeException(ue);
}
}
}
表示用户密码的加密方式,这里使用的是一个自定义Been,其源码如下:
public class PasswordManager implements PasswordEncoder, InitializingBean, SystemConfigListener {
public static final String PASSWORD_COMPLEXITY_NO_LIMIT = "NO_LIMIT";
public static final String PASSWORD_COMPLEXITY_DIGITS_AND_LETTERS = "DIGITS_AND_LETTERS";
public static final String PASSWORD_COMPLEXITY_DIGITS_AND_CASE_LETTERS = "DIGITS_AND_CASE_LETTERS";
private PasswordEncoder delegate;
private String siteWideSecret = "my-secret-key";
private String defaultPassword = "123456";
/**
* 密码失效时间 默认0 不失效
*/
private Integer passwordInvalidTime = 0;
/**
* 密码长度
*/
private Integer passwordMinLength = 8;
/**
* 密码复杂度
*/
private String passwordComplexity = "no_limit";
@Override
public void afterPropertiesSet() throws Exception {
delegate = new StandardPasswordEncoder(siteWideSecret);
}
@Override
public String encode(CharSequence rawPassword) {
return delegate.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (StringUtil.isEmpty(encodedPassword)) {
return false;
}
return delegate.matches(rawPassword, encodedPassword);
}
@Override
public List getAcceptedProfiles() {
return Arrays.asList("DEFAULT_PASSWORD", "PASSWORD_INVALID_TIME", "PASSWORD_MIN_LENGTH", "PASSWORD_COMPLEXITY");
}
@Override
public void updateProfile(String profileName, String profileValue) {
if ("PASSWORD_INVALID_TIME".equalsIgnoreCase(profileName)) {
this.passwordInvalidTime = Integer.parseInt(profileValue);
} else if ("PASSWORD_MIN_LENGTH".equalsIgnoreCase(profileName)) {
this.passwordMinLength = Integer.parseInt(profileValue);
} else if ("PASSWORD_COMPLEXITY".equalsIgnoreCase(profileName)) {
this.passwordComplexity = profileValue;
} else if ("DEFAULT_PASSWORD".equalsIgnoreCase(profileName)) {
this.defaultPassword = profileValue;
}
}
}
有关于这一块的内容,我有一篇专门的文章来介绍 密码安全
还需要注意的一点,这里有个登录前置拦截器:
spring security中已经默认有一套拦截器链,但有时并不能完全满足项目上的需求。默认的拦截器如下:
定义 custom-filter 时需要我们通过 ref 属性指定其对应关联的是哪个 Filter,此外还需要通过 position、before 或者 after 指定该 Filter 放置的位置。从上图中可以知道,FORM_LOGIN_FILTER 对应的就是 UsernamePasswordAuthenticationFilter,before="FORM_LOGIN_FILTER" 就表示将定义的 Filter 放在 FORM_LOGIN_FILTER 之前,也就是将captchaVerifierFilter(验证码拦截器)放在UsernamePasswordAuthenticationFilter之前。有关于 CaptchaVerifierFilter 的源码,也可以简单看看,其实就是判断验证码是否正确,不正确就带上错误信息跳转到登录界面,这里不再深入。
public class CaptchaVerifierFilter extends OncePerRequestFilter {
@Autowired
private ICaptchaManager captchaManager;
@Autowired
private CaptchaConfig captchaConfig;
private RequestMatcher loginRequestMatcher;
private String captchaField = "captcha";
private String loginUrl = "/login";
public CaptchaVerifierFilter() {
setFilterProcessesUrl(this.loginUrl);
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
if (captchaConfig.isEnableCaptcha(WebUtils.getCookie(httpServletRequest, CaptchaConfig.LOGIN_KEY))
&& requiresValidateCaptcha(httpServletRequest, httpServletResponse)) {
Cookie cookie = WebUtils.getCookie(httpServletRequest, captchaManager.getCaptchaKeyName());
String captchaCode = httpServletRequest.getParameter(getCaptchaField());
if (cookie == null || StringUtils.isEmpty(captchaCode)
|| !captchaManager.checkCaptcha(cookie.getValue(), captchaCode)) {
httpServletRequest.setAttribute("error", true);
httpServletRequest.setAttribute("code", "CAPTCHA_INVALID");
httpServletRequest.setAttribute("exception",
new UserException(UserException.LOGIN_VERIFICATION_CODE_ERROR, null));
httpServletRequest.getRequestDispatcher(loginUrl).forward(httpServletRequest, httpServletResponse);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
public String getCaptchaField() {
return captchaField;
}
public void setCaptchaField(String captchaField) {
this.captchaField = captchaField;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.loginRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
}
protected boolean requiresValidateCaptcha(HttpServletRequest request, HttpServletResponse response) {
return loginRequestMatcher.matches(request) && "POST".equalsIgnoreCase(request.getMethod());
}
}
下面梳理一下流程:用户点击登录按钮-->xxx拦截-->校验验证码-->xxx处理-->调用customUserDetailsService的loadUserByUsername方法获取UserDetails并存起来-->身份认证-->根据认证结果执行不同的程序(认证成功对应authentication-success-handler-ref,认证失败对应authentication-failure-handler-ref)。
onAuthenticationSuccess
源码如下
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler implements SystemConfigListener{
@Autowired
private ApplicationContext applicationContext;
private RequestCache requestCache = new HttpSessionRequestCache();
private Logger logger = LoggerFactory.getLogger(getClass());
private Map listeners;
public static final String DEFAULT_TARGET_URL = "DEFAULT_TARGET_URL";
private final String loginOauthUrl = "/login?oauth";
private final String loginUrl = "/login";
private final String indexUrl = "/index";
private final String refererStr = "Referer";
private final String loginCasUrl = "/login/cas";
private final String functionCodeStr = "functionCode";
{
setDefaultTargetUrl("/");
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if(listeners == null) {
listeners = applicationContext
.getBeansOfType(IAuthenticationSuccessListener.class);
}
String referer = request.getHeader(refererStr);
if(referer != null && referer.endsWith(loginOauthUrl)){
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
List list = new ArrayList<>();
list.addAll(listeners.values());
Collections.sort(list);
IAuthenticationSuccessListener successListener = null;
try {
for (IAuthenticationSuccessListener listener : list) {
successListener = listener;
successListener.onAuthenticationSuccess(request, response, authentication);
}
HttpSession session = request.getSession(false);
session.setAttribute(User.LOGIN_CHANGE_INDEX,"CHANGE");
} catch (Exception e) {
logger.error("authentication success, but error occurred in " + successListener, e);
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
request.setAttribute("error", true);
request.setAttribute("exception", e);
request.getRequestDispatcher("/login").forward(request, response);
return;
}
String requestURI = request.getRequestURI();
boolean isCas = requestURI.endsWith(loginCasUrl);
if(isCas) {
//拿到登录以前的url
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
/* String defaultTarget = getDefaultTargetUrl();
if (!targetUrl.contains(functionCodeStr) && ! indexUrl.equalsIgnoreCase(defaultTarget)) {
targetUrl = getDefaultTargetUrl()+"?targetUrl="+targetUrl;
}*/
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
return;
}
}
handle(request, response, authentication);
}
@Override
public List getAcceptedProfiles() {
return Arrays.asList(DEFAULT_TARGET_URL);
}
@Override
public void updateProfile(String profileName, String profileValue) {
if(StringUtil.isNotEmpty(profileValue)) {
setDefaultTargetUrl(profileValue);
}
}
}
注意这个 private Map
得到的就是的IAuthenticationSuccessListener接口的实现类
主要有:GenerateTokenAuthenticationSuccessListener、DefaultAuthenticationSuccessListener、AuthenticationSuccessActivityListener、UserLoginInfoCollection。然后遍历listeners,调用各个listener的onAuthenticationSuccess方法。
GenerateTokenAuthenticationSuccessListener:保存 token 授权信息到redis中
DefaultAuthenticationSuccessListener:设置系统首页的一些配置
AuthenticationSuccessActivityListener:activity代办事项
UserLoginInfoCollection:用户登录记录
LoginFailureHandler
验证失败时的处理比较简单,主要就是返回错误码,然后跳转到登录界面。
public class LoginFailureHandler implements AuthenticationFailureHandler {
private static final Logger log = LoggerFactory.getLogger(LoginFailureHandler.class);
@Autowired
private CaptchaConfig captchaConfig;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (log.isDebugEnabled()) {
log.debug("login failed");
}
if (captchaConfig.getWrongTimes() > 0) {
captchaConfig.updateLoginFailureInfo(WebUtils.getCookie(request, CaptchaConfig.LOGIN_KEY));
}
request.setAttribute("error", true);
request.setAttribute("code", "LOGIN_NOT_MATCH");
request.setAttribute("exception", exception);
request.getRequestDispatcher("/login").forward(request, response);
}
}
大体的逻辑就是这样,但是中间还有太多太多细节,想要完全理解清除需要很大的精力,其实如果知道这个流程,就基本上可以根据项目上的需求去修改代码了。
基于投票的AccessDecisionManager实现
AccessDecisionManager
- 提供了一个基于AccessDecisionVoter 接口和投票集合的授权机制。
- supports:这个逻辑操作实际上包含两个方法,它们允许AccessDecisionManager 的实现
类判断是否支持当前的请求。 - decide:基于请求的上下文和安全配置,允许AccessDecisionManager 去核实访问是否被
允许以及请求是否能够被接受。decide 方法实际上没有返回值,通过抛出异常来表明对
请求访问的拒绝。
AbstractAccessDecisionManager implements AccessDecisionManager。AbstractAccessDecisionManager 实现类如下:
spring security文档
以下截图是文档的一部分
There are three concrete AccessDecisionManager s provided with Spring Security that tally the votes. The ConsensusBased implementation will grant or deny access based on the consensus of non-abstain votes. Properties are provided to control behavior in the event of an equality of votes or if all votes are abstain. The AffirmativeBased implementation will grant access if one or more ACCESS_GRANTED votes were received (i.e. a deny vote will be ignored, provided there was at least one grant vote). Like the ConsensusBased implementation, there is a parameter that controls the behavior if all voters abstain. The UnanimousBased provider expects unanimous ACCESS_GRANTED votes in order to grant access, ignoring abstains. It will deny access if there is any ACCESS_DENIED vote. Like the other implementations, there is a parameter that controls the behaviour if all voters abstain.
It is possible to implement a custom AccessDecisionManager that tallies votes differently. For example, votes from a particular AccessDecisionVoter might receive additional weighting, whilst a deny vote from a particular voter may have a veto effect.
在Hap的标准登录中,使用的是UnanimousBased投票机制,意识就是说需要所有的投票器都认证通过才算验证通过。
追溯到UnanimousBased源码,查看对应的UML类图:
UnanimousBased类中只有一个方法,其实现如下
public void decide(Authentication authentication, Object object, Collection attributes) throws AccessDeniedException {
int grant = 0;
int abstain = 0;
List singleAttributeList = new ArrayList(1);
singleAttributeList.add(null);
for (ConfigAttribute attribute : attributes) {
singleAttributeList.set(0, attribute);
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, singleAttributeList);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
grant++;
break;
case AccessDecisionVoter.ACCESS_DENIED:
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied",
"Access is denied"));
default:
abstain++;
break;
}
}
}
// To get this far, there were no deny votes
if (grant > 0) {
return;
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
其实就是在循环遍历配置文件中的那几个 实现类的 vote 方法。
再通过IDEA中的DEBUG功能,发现是在 org.springframework.security.access.intercept.AbstractSecurityInterceptor 的 beforeInvocation 方法中调用了 UnanimousBased 的 decide 方法;
org.springframework.security.web.access.intercept.FilterSecurityInterceptor 中调用了beforeInvocation方法