Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。
核心对象:
SecurityContextHolder 是 SecurityContext的存放容器,默认使用ThreadLocal 存储,意味SecurityContext在相同线程中的方法都可用。
SecurityContext主要是存储应用的principal信息,在Spring Security中用Authentication 来表示。
获取principal:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
在Spring Security中,可以看一下Authentication定义:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 通常是密码
*/
Object getCredentials();
/**
* Stores additional details about the authentication request. These might be an IP
* address, certificate serial number etc.
*/
Object getDetails();
/**
* 用来标识是否已认证,如果使用用户名和密码登录,通常是用户名
*/
Object getPrincipal();
/**
* 是否已认证
*/
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
在实际应用中,通常使用UsernamePasswordAuthenticationToken
:
public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {
}
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
}
一个常见的认证过程通常是这样的,创建一个UsernamePasswordAuthenticationToken,然后交给authenticationManager认证,
认证通过则通过SecurityContextHolder存放Authentication信息。
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword());
Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails 是Spring Security里的一个关键接口,用来表示一个principal。
public interface UserDetails extends Serializable {
/**
* 用户的授权信息,可以理解为角色
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 用户密码
*
* @return the password
*/
String getPassword();
/**
* 用户名
* */
String getUsername();
boolean isAccountNonExpired(); // 用户是否过期
boolean isAccountNonLocked(); // 是否锁定
boolean isCredentialsNonExpired(); // 用户密码是否过期
boolean isEnabled(); // 账号是否可用
}
UserDetails提供了认证所需的必要信息,在实际使用里,可以自己实现UserDetails,并增加额外的信息,比如email、mobile等信息。
在Authentication中的principal通常是用户名,我们可以通过UserDetailsService来通过principal获取UserDetails:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
小结
SecurityContextHolder
, 用来访问SecurityContext
.SecurityContext
, 用来存储Authentication
.Authentication
, 代表凭证.GrantedAuthority
, 代表权限.UserDetails
, 对用户信息进行封装.UserDetailsService
,对用户信息进行管理.
整个过程:
Authentication认证
1、用户进入登录页面,输入用户名和密码,security首先会进入UsernamePasswordAuthenticationFilter,调用
attemptAuthentication方法,将用户名和密码作为pricaipal和critial组合成UsernamePasswordAuthenticationToken实例
2、将令牌传递给AuthenticationManage实例进行验证,根据用户名查找到用户,在进行密码比对
3、校验成功后,会把查询到的user对象填充到authenticaton对象中,并将标志authenticated设为true
4、通过调用 SecurityContextHolder.getContext().setAuthentication(...) 建立安全上下文的实例,传递到返回的身份认证对象上
AbstractAuthenticationProcessingFilter 抽象类
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
调用requestsAuthentication()决定是否需要进行校验,如果需要验证,则会调用attemptAuthentication()进行校验,有三种结果:
1、验证成功,返回一个填充好的Authentication对象(通常带上authenticated=true),接着执行successfulAuthentication()
2、验证失败,抛出AuthenticationException,接着执行unsuccessfulAuthentication()
3、返回null,表示身份验证不完整。假设子类做了一些必要的工作(如重定向)来继续处理验证,方法将立即返回。假设后一个请求将被这种方法接收,其中返回的Authentication对象不为空。
AuthenticationException
是运行时异常,它通常由应用程序按通用方式处理,用户代码通常不用特意被捕获和处理这个异常。
UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子类)
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
attemptAuthentication ()
方法将 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 对象,用于 AuthenticationManager
的验证(即 this.getAuthenticationManager().authenticate(authRequest) )
默认情况下注入 Spring 容器的 AuthenticationManager
是 ProviderManager
。
ProviderManager(AuthenticationManager的实现类)
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
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;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
// 如果没有任何一个 Provider 验证成功,则使用父类型AuthenticationManager进行验证
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
// 擦除敏感信息
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
AuthenticationManager
的默认实现是ProviderManager
,它委托一组AuthenticationProvider
实例来实现认证。AuthenticationProvider
和AuthenticationManager
类似,都包含authenticate
,但它有一个额外的方法supports
,以允许查询调用方是否支持给定Authentication
类型:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
ProviderManager包含一组AuthenticationProvider
,执行authenticate时,遍历Providers,然后调用supports,如果支持,则执行遍历当前provider的authenticate方法,如果一个provider认证成功,则break。如果最后所有的 AuthenticationProviders 都没有成功验证 Authentication 对象,将抛出 AuthenticationException。
由 provider 来验证 authentication, 核心点方法是Authentication result = provider.authenticate(authentication);此处的 provider
是 AbstractUserDetailsAuthenticationProvider,它
是AuthenticationProvider的实现,看看它的 authenticate(authentication)
方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 必须是UsernamePasswordAuthenticationToken
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 获取用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 从缓存中获取
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// retrieveUser抽象方法,获取用户
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 预先检查 DefaultPreAuthenticationChecks,检查用户是否被lock或者账号是否可用
preAuthenticationChecks.check(user);
// 抽象方法,自定义检验
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 后置检查 DefaultPostAuthenticationChecks,检查isCredentialsNonExpired
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
三步验证工作
1. preAuthenticationChecks
2. additionalAuthenticationChecks(抽象方法,子类实现)
3. postAuthenticationChecks
AbstractUserDetailsAuthenticationProvider
内置了缓存机制,从缓存中获取不到的 UserDetails 信息的话,就调用retrieveUser()方法获取用户信息,然后和用户传来的信息进行对比来判断是否验证成功。retrieveUser()
方法在 DaoAuthenticationProvider
中实现, DaoAuthenticationProvider
是 AbstractUserDetailsAuthenticationProvider
的子类。这个类的核心是让开发者提供UserDetailsService来获取UserDetails以及 PasswordEncoder来检验密码是否有效:
private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
具体实现如下
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
可以看到此处的返回对象 userDetails
是由 UserDetailsService
的 loadUserByUsername(username)
来获取的。
再来看验证:
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
// 获取用户密码
String presentedPassword = authentication.getCredentials().toString();
// 比较passwordEncoder后的密码是否和userdetails的密码一致
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
小结:要自定义认证,使用DaoAuthenticationProvider,只需要为其提供PasswordEncoder和UserDetailsService就可以了。
时序图
补充:SecurityContextHolder
的工作原理
这是一个工具类,只提供一些静态方法,目的是用来保存应用程序中当前使用人的安全上下文。
缺省工作模式 MODE_THREADLOCAL
我们知道,一个应用同时可能有多个使用者,每个使用者对应不同的安全上下文,那么SecurityContextHolder
是怎么保存这些安全上下文的呢 ?缺省情况下,SecurityContextHolder
使用了ThreadLocal
机制来保存每个使用者的安全上下文。这意味着,只要针对某个使用者的逻辑执行都是在同一个线程中进行,即使不在各个方法之间以参数的形式传递其安全上下文,各个方法也能通过SecurityContextHolder
工具获取到该安全上下文。只要在处理完当前使用者的请求之后注意清除ThreadLocal
中的安全上下文,这种使用ThreadLocal
的方式是很安全的。当然在Spring Security
中,这些工作已经被Spring Security
自动处理,开发人员不用担心这一点。
SecurityContextHolder
源码
public class SecurityContextHolder {
// ~ Static fields/initializers
// =====================================================================================
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
// ~ Methods
// ========================================================================================================
/**
* Explicitly clears the context value from the current thread.
*/
public static void clearContext() {
strategy.clearContext();
}
/**
* Obtain the current <code>SecurityContext</code>.
*
* @return the security context (never <code>null</code>)
*/
public static SecurityContext getContext() {
return strategy.getContext();
}
/**
* Primarily for troubleshooting purposes, this method shows how many times the class
* has re-initialized its <code>SecurityContextHolderStrategy</code>.
*
* @return the count (should be one unless you've called
* {@link #setStrategyName(String)} to switch to an alternate strategy.
*/
public static int getInitializeCount() {
return initializeCount;
}
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
}
else {
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
initializeCount++;
}
/**
* Associates a new <code>SecurityContext</code> with the current thread of execution.
*
* @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
*/
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
/**
* Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
* a given JVM, as it will re-initialize the strategy and adversely affect any
* existing threads using the old strategy.
*
* @param strategyName the fully qualified class name of the strategy that should be
* used.
*/
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
/**
* Allows retrieval of the context strategy. See SEC-1188.
*
* @return the configured strategy for storing the security context.
*/
public static SecurityContextHolderStrategy getContextHolderStrategy() {
return strategy;
}
/**
* Delegates the creation of a new, empty context to the configured strategy.
*/
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
public String toString() {
return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount="
+ initializeCount + "]";
}
}
Spring security获取当前用户
前面介绍过用SecurityContextHolder.getContext().getAuthentication() .getPrincipal();获取当前用户,还有一种方法:
经过spring security认证后,spring security会把一个SecurityContextImpl对象存储到session中,此对象中有当前用户的各种资料
SecurityContextImpl securityContextImpl =
(SecurityContextImpl) request.getSession.getAttribute("SPRING_SECURITY_CONTEXT");
//登录名
System.out.println("Username:" + securityContextImpl.getAuthentication().getName());
//登录密码,未加密的
System.out.println("Credentials:" + securityContextImpl.getAuthentication().getCredentials());
SecurityContextImpl源码
/**
* Base implementation of {@link SecurityContext}.
* <p>
* Used by default by {@link SecurityContextHolder} strategies.
*
* @author Ben Alex
*/
public class SecurityContextImpl implements SecurityContext {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private Authentication authentication;
// ~ Methods
// ========================================================================================================
public boolean equals(Object obj) {
if (obj instanceof SecurityContextImpl) {
SecurityContextImpl test = (SecurityContextImpl) obj;
if ((this.getAuthentication() == null) && (test.getAuthentication() == null)) {
return true;
}
if ((this.getAuthentication() != null) && (test.getAuthentication() != null)
&& this.getAuthentication().equals(test.getAuthentication())) {
return true;
}
}
return false;
}
public Authentication getAuthentication() {
return authentication;
}
public int hashCode() {
if (this.authentication == null) {
return -1;
}
else {
return this.authentication.hashCode();
}
}
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString());
if (this.authentication == null) {
sb.append(": Null authentication");
}
else {
sb.append(": Authentication: ").append(this.authentication);
}
return sb.toString();
}
}
可以看出,主要就两个方法getAuthentication()和setAuthentication()
问题: 如果修改了登录用户的信息,怎么更新到security context中呢
目前想到的解决办法是重新认证:
//首先在WebSecurityConfig中注入AuthenticationManager
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//在Controller中注入
@Autowired
private AuthenticationManager authenticationManager;
//接下来就是在会修改用户信息的地方设置重新认证
//也可以通过SecurityContextHolder获取
SecurityContextImpl securityContext = (SecurityContextImpl) request.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(登录名,登录密码);
Authentication authentication = authenticationManager.authenticate(token);
//重新设置authentication
securityContext.setAuthentication(authentication);
因为我做的是前后端分离,返回的都是json格式的数据,所以在项目中还遇到的问题有:
spring security未登陆时,不跳转登陆页面改为返回JSON字符串
参考文章:
Spring Security源码分析一:Spring Security认证过程