目标:初步掌握Spring Security的认证功能实现。
案例介绍
- 项目的地址是:项目初始化
- 项目的数据库脚本在src/main/resources目的下的security.sql。
- 开发工具使用的是IntelliJ IDEA。
- JDK版本是1.8。
- MySQL的版本是5.7。
- 后端的技术是SSM(SpringMVC、Spring、Mybatis)。
- 前端的技术是AdminLTE2。访问地址:http://localhost:8080/login.jsp。
初识权限管理
权限管理概念
- 权限管理,一般是指根据系统设置的安全规则或安全策略,用户可以访问而且只能访问自己被授予的权限。权限管理几乎出现在任何系统里面,前提是需要用户认证的系统。
在权限管理的概念中,有两个非常重要的名词:认证:通过用户名和密码(当然也可以是其它方式,比如邮箱、身份证等)成功登录系统后,让系统得到当前用户的角色身份。
授权:系统根据当前用户的角色,给其授予对应可以操作的权限资源。
完全权限管理需要三个对象
- 用户:主要包含用户名、密码等,可以实现认证操作。一般而言,在系统中给用户分配角色。
- 角色:角色是权限的集合。一般而言,在系统中给角色分配权限。
- 权限:权限也可以称为资源,包括地址、权限名称等。
一般而言,用户可以分配多个角色,角色可以分配多个权限。所以,在权限设计表的时候,一般设计5张表,分别为用户表、角色表、权限表、用户角色表、角色权限表。业内有时也会将这5张表称为经典的RBAC权限设计模型。
初识Spring Security
Spring Security概念
- Spring Security是Spring采用AOP思想,基于Servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的授权功能。是一个非常优秀的权限管理框架。
Spring Security简单入门
创建web工程并导入相应的jar包
org.springframework.security
spring-security-config
5.1.5.RELEASE
org.springframework.security
spring-security-core
5.1.5.RELEASE
org.springframework.security
spring-security-taglibs
5.1.5.RELEASE
org.springframework.security
spring-security-web
5.1.5.RELEASE
配置web.xml
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
配置spring-security.xml
将spring-security.xml配置文件引入到applicationContext.xml中
运行项目
- 运行项目会出现下面的界面。
- 其源码如下:
- 控制台的日志:
INFO web.DefaultSecurityFilterChain - Creating filter chain: any request, [org.springframework.security.web.context.SecurityContextPersistenceFilter@17455fed, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@97f4fa3, org.springframework.security.web.header.HeaderWriterFilter@1384edda, org.springframework.security.web.csrf.CsrfFilter@544ac02d, org.springframework.security.web.authentication.logout.LogoutFilter@694d477e, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@1abc0fa3, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2e62db13, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2c2b3d5a, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2fdef916, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@55e53a59, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6d67c9b5, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@18d3e8ae, org.springframework.security.web.session.SessionManagementFilter@409db4eb, org.springframework.security.web.access.ExceptionTranslationFilter@3cd67953, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6faf351d]
- 最后,我们在此登录页面输入用户名为user,密码为user,即可登录。
Spring Security常用的过滤器链
Spring Security常用过滤器介绍
- org.springframework.security.web.context.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用个,为后续Filter建立所需要的上下文。SecurityContext中存储了当前用户的认证和权限信息。
- org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
此过滤器用语集成SecurityContext到Spring异步执行机制中的WebAsyncManager。
- org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制。
- org.springframework.security.web.csrf.CsrfFilter
csrf又称为跨域请求伪造,SpringSecurity会对所有POST、PUT、DELETE请求验证是否包含系统生成的csrf的token信息,如果不包含,就报错。起到防止csrf攻击的效果。
- org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
- org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
- org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中执行认证页面,则由该过滤器生成一个默认认证页面。
- org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
此过滤器产生的一个默认的退出登录的页面。
- org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部带有Authentication,且以Basic开头的头信息。
- org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest。
- org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API。
- org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中,SpringSecurity为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名身份。
- org.springframework.security.web.session.SessionManagementFilter
SecurityContextRepository限制同一用户开启多个会话的数量。
- org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个SpringSecurityFilterChain的后方,用来转换整个链路中出现的异常。
- org.springframework.security.web.access.intercept.FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
Spring Security过滤器链加载原理
DelegatingFilterProxy
- 我们在web.xml中配置了一个名称为springSecurityFilterChain的过滤器DelegatingFilterProxy,接下来我们看其中的重要源码即可。
public class DelegatingFilterProxy extends GenericFilterBean {
@Nullable
private String contextAttribute;
@Nullable
private WebApplicationContext webApplicationContext;
@Nullable
private String targetBeanName;
private boolean targetFilterLifecycle = false;
@Nullable
private volatile Filter delegate; //注意:这个过滤器才是真正加载的过滤器
private final Object delegateMonitor = new Object();
//注意:doFilter是过滤器的入口,直接从这边看。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
//第一步:doFilter中最重要的一步,初始化上面私有过滤器属性delegate
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
//第三步:执行FilterChainProxy过滤器
invokeDelegate(delegateToUse, request, response, filterChain);
}
//第二步:直接看最终加载的过滤器是谁
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//debug得知targetBeanName为springSecurityFilterChain
String targetBeanName = getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
//debug得知Filter对象为FilterChainProxy
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
}
- 第二步debug的结果如下图所示:
- 由此可知,DelegatingFilterProxy通过springSecurityFilterChain这个名词,得到了一个FilterChainProxy过滤器,最终在第三步执行了该过滤器。
FilterChainProxy
public class FilterChainProxy extends GenericFilterBean {
private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
private List filterChains;
private FilterChainProxy.FilterChainValidator filterChainValidator;
private HttpFirewall firewall;
//可以通过一个叫SecurityFilterChain的对象实例化一个FilterChainProxy对象,可能SecurityFilterChain才是真正的过滤器对象。
public FilterChainProxy(SecurityFilterChain chain) {
this(Arrays.asList(chain));
}
//又是SecurityFilterChain对象。
public FilterChainProxy(List filterChains) {
this.filterChainValidator = new FilterChainProxy.NullFilterChainValidator();
this.firewall = new StrictHttpFirewall();
this.filterChains = filterChains;
}
//注意:直接从doFilter看
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);
this.doFilterInternal(request, response, chain);
} finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
} else {
//第一步:具体操作调用下面的doFilterInternal方法
this.doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
//第二步:封装要执行的过滤器链,这么多的过滤器链就在这里封装进去了。
List filters = this.getFilters((HttpServletRequest)fwRequest);
if (filters != null && filters.size() != 0) {
FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
//第四步:加载过滤器链
vfc.doFilter(fwRequest, fwResponse);
} else {
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);
}
}
private List getFilters(HttpServletRequest request) {
Iterator var2 = this.filterChains.iterator();
//封装过滤器链到SecurityFilterChain对象
SecurityFilterChain chain;
do {
if (!var2.hasNext()) {
return null;
}
chain = (SecurityFilterChain)var2.next();
} while(!chain.matches(request));
return chain.getFilters();
}
}
- 第二步debug的结果如下图所示:
SecurityFilterChain
- SecurityFilterChain是一个接口,实现类也只有一个,这才是web.xml配置的过滤器链对象。
//接口
public interface SecurityFilterChain {
boolean matches(HttpServletRequest var1);
List getFilters();
}
//实现类
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private static final Log logger = LogFactory.getLog(DefaultSecurityFilterChain.class);
private final RequestMatcher requestMatcher;
private final List filters;
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) {
this(requestMatcher, Arrays.asList(filters));
}
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List filters) {
logger.info("Creating filter chain: " + requestMatcher + ", " + filters);
this.requestMatcher = requestMatcher;
this.filters = new ArrayList(filters);
}
public RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
public List getFilters() {
return this.filters;
}
public boolean matches(HttpServletRequest request) {
return this.requestMatcher.matches(request);
}
public String toString() {
return "[ " + this.requestMatcher + ", " + this.filters + "]";
}
}
SpringSecurity使用自定义认证页面
在SpringSecurity的主配置文件中指定认证页面配置信息
- 修改认证页面的请求地址:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
数据 - AdminLTE2定制版 | Log in
- 再次启动项目就可以看到自定义的登录页面了。
- 但是当我们输入用户名为user,密码为user的时候,却会出现如下的页面:
- 403在Spring Security中是权限不足?为什么?Spring Security内置的认证页面源代码中有_csrf隐藏input,问题就在这里,而且后台日志是这样的。
Spring Security的csrf防护机制
Spring Security中CsrfFilter过滤器的说明
public final class CsrfFilter extends OncePerRequestFilter {
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
private final Log logger = LogFactory.getLog(this.getClass());
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher;
private AccessDeniedHandler accessDeniedHandler;
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
this.accessDeniedHandler = new AccessDeniedHandlerImpl();
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
}
//从这里可以看出Spring Security的csrf机制把请求方式分为两类
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//第一类:GET、HEAD、TRACE、OPTIONS四类请求可以直接通过
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
//第二类:除去上面的四种方式,包括POST、DELETE、PUT等都需要携带token才能通过
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
filterChain.doFilter(request, response);
}
}
}
public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
this.accessDeniedHandler = accessDeniedHandler;
}
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet allowedMethods;
private DefaultRequiresCsrfMatcher() {
this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
}
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
}
-
通过源码,我们知道,我们自己的登录页面的请求方式是POST,但是却没有携带token,所以才会出现403权限不足的异常,那么如何处理?
- ①直接禁用csrf,不推荐。
- ②在认证页面携带token请求。
禁用csrf防护机制
- 在Spring Security的主配置文件中添加禁用csrf防护的机制。
在认证页面携带token请求
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%-- 添加标签库 --%>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
数据 - AdminLTE2定制版 | Log in
注销
- 需要将header.jsp中的注销功能,改为form表单提交,并且提交的方式是POST提交,而且在表单携带token请求。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
数据
数据后台管理
Spring Security使用数据库完成认证
认证流程分析
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter是用来负责认证的过滤器。
package org.springframework.security.web.authentication;
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
//视图认证的方法
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//必须为POST请求,否则会抛出异常
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//将填写的用户名和密码封装到UsernamePasswordAuthenticationToken对象中
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//调用AuthenticationManager对应进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.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 this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
AuthenticationManager
- 由上面的源码可知,真正的认证操作在AuthenticationManager里面。但是AuthenticationManager是接口,其子类是ProviderManager。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher;
private List providers;
protected MessageSourceAccessor messages;
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication;
//注意AuthenticationProvider,Spring Security针对每一种认证,什么QQ登录,微信登录都封装到一个AuthenticationProvider对象中
public ProviderManager(List providers) {
this(providers, (AuthenticationManager)null);
}
public ProviderManager(List providers, AuthenticationManager parent) {
this.eventPublisher = new ProviderManager.NullEventPublisher();
this.providers = Collections.emptyList();
this.messages = SpringSecurityMessageSource.getAccessor();
this.eraseCredentialsAfterAuthentication = true;
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
this.checkState();
}
public void afterPropertiesSet() throws Exception {
this.checkState();
}
private void checkState() {
if (this.parent == null && this.providers.isEmpty()) {
throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required");
}
}
//认证的方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();
//循环遍历所有的AuthenticationProvider,匹配当前认证类型
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
//找到对应的认证类型继续调用AuthenticationProvider对象完成认证业务
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (InternalAuthenticationServiceException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {
} catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
private void prepareException(AuthenticationException ex, Authentication auth) {
this.eventPublisher.publishAuthenticationFailure(ex, auth);
}
private void copyDetails(Authentication source, Authentication dest) {
if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;
token.setDetails(source.getDetails());
}
}
public List getProviders() {
return this.providers;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
this.eraseCredentialsAfterAuthentication = eraseSecretData;
}
public boolean isEraseCredentialsAfterAuthentication() {
return this.eraseCredentialsAfterAuthentication;
}
private static final class NullEventPublisher implements AuthenticationEventPublisher {
private NullEventPublisher() {
}
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
public void publishAuthenticationSuccess(Authentication authentication) {
}
}
}
AuthenticationProvider
- AuthenticationProvider是接口,其子类是AbstractUserDetailsAuthenticationProvider。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
protected final Log logger = LogFactory.getLog(this.getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false;
protected boolean hideUserNotFoundExceptions = true;
private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
public AbstractUserDetailsAuthenticationProvider() {
}
protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
public final void afterPropertiesSet() throws Exception {
Assert.notNull(this.userCache, "A user cache must be set");
Assert.notNull(this.messages, "A message source must be set");
this.doAfterPropertiesSet();
}
//认证的方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.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 {
//获取UserDetails对象,即SpringSecurity自己的用户对象
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
//这是个抽象方法,由子类实现
protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
}
- DaoAuthenticationProvider是AbstractUserDetailsAuthenticationProvider的子类,有对应retrieveUser方法的实现。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
////获取UserDetails对象,即SpringSecurity自己的用户对象
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//UserDetails对象,即SpringSecurity自己的用户对象
//loadUserByUsername是真正的认证逻辑,即我们可以直接编写一个UserDetailsService()的实现呢类,告诉SpringSecurity就可以了。
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
}
初步实现自己的认证功能
我们自己的UserService接口继承UserDetailService。
package com.weiwei.xu.service;
import com.weiwei.xu.domain.SysUser;
import org.springframework.security.core.userdetails.UserDetailsService;
import java.util.List;
import java.util.Map;
public interface UserService extends UserDetailsService {
public void save(SysUser user);
public List findAll();
public Map toAddRolePage(Integer id);
public void addRoleToUser(Integer userId, Integer[] ids);
}
编写loadUserByUsername的逻辑
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userDao.findByName(username);
if (null == sysUser) {
//如果用户名不对,直接返回null,表示认证失败
return null;
}
List authorities = new ArrayList<>();
List roles = sysUser.getRoles();
if (null != roles && roles.size() != 0) {
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
authorities.add(simpleGrantedAuthority);
});
}
//返回UserDetails对象,"{noop}"+密码表示不加密认证
UserDetails userDetails = new User(sysUser.getUsername(), "{noop}" + sysUser.getPassword(), authorities);
return userDetails;
}
}
修改SpringSecurity的主配置文件
加密认证
在IOC容器中添加加密对象
- 在SpringSecurity的主配置文件中添加加密对象。
修改认证方法
- 去掉nohup
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userDao.findByName(username);
if (null == sysUser) {
//如果用户名不对,直接返回null,表示认证失败
return null;
}
List authorities = new ArrayList<>();
List roles = sysUser.getRoles();
if (null != roles && roles.size() != 0) {
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
authorities.add(simpleGrantedAuthority);
});
}
//返回UserDetails对象,"{noop}"+密码表示不加密认证
UserDetails userDetails = new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
return userDetails;
}
修改添加用户的方法
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public void save(SysUser user) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
userDao.save(user);
}
手动修改数据库中用户对应的密码
- 将xiaoming账号对应的密码改为
$2a$10$ynlaufZM048G5jsp98seeuvkAXNCVD5RFEudlrW.xiNihU.2Tjm9W