【java】【SpringSecurity】SpringSecurity入门

前言

1. 初识SpringSecurity

权限管理

一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。

两个核心概念:

认证:通过用户名和密码成功登陆系统后,让系统得到当前用户的角色身份。
授权:系统根据当前用户的角色,给其授予对应可以操作的权限资源。

三个对象

用户:主要包含用户名,密码和当前用户的角色信息,可实现认证操作。
角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。
权限:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。
用户与角色是多对多的关系,角色与权限是多对多的关系,用户与权限没有直接关系
二者是通过角色来建立关联关系的。

初识SpringSecurity

SpringSecurity是一款非常优秀的权限管理框架。

  • 采用AOP思想,基于servlet过滤器实现的安全框架。
  • 提供了完善的认证机制和方法级的授权功能。

核心jar包

  • pring-security-core.jar:核心包,任何Spring Security功能都需要此包。
  • spring-security-web.jar:web工程必备,包含过滤器和相关的Web安全基础结构代码。
  • spring-security-config.jar:用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。
  • spring-security-taglibs.jar:Spring Security提供的动态标签库,jsp页面可以用。

pom依赖

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

配置web.xml

<display-name>Archetype Created Web Application</display-name>
	<!--Spring Security过滤器链,过滤器名称必须叫springSecurityFilterChain-->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <!-- 过滤器拦截所有请求 -->
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

配置SpringSecurity.xml

<?xml version="1.0" encoding="UTF-8"?>
...
xmlns:security="http://www.springframework.org/schema/security"
...
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
	<!--配置静态资源目录不经过过滤器,示例只写一个-->
	<security:http pattern="/css/**" security="none"/>
	...
	<!--设置可以用spring的el表达式配置Spring Security并自动生成对应配置组件(过滤器)-->
    <security:http auto-config="true" use-expressions="true">
    	<!--指定login.jsp页面可以匿名访问-->
    	<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
     	<!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色
			/* 是拦截所有的文件夹,不包含子文件夹
			/** 是拦截所有的文件夹及里面的子文件夹
		-->
    	<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
    	<!--指定自定义的认证页面-->
    	<security:form-login login-page="/login.jsp"
    						 login-processing-url="/login"
    						 default-target-url="index.jsp"
    						 authentication-farlure-url="failer.jsp"
    	/>
    	<security:from-login logout-url="/logout"
    						 logout-seccese-url="/login.jsp"
		/>
		<!--禁用csrf防护机制,不推荐-->
		<!-- <security:csrf disable="true"/> -->
    </security:http>
    
    <!--设置Spring Security认证用户信息的来源 {noop}表示不加密认证
		这部分配置可以在数据库查询,为了简便直接在这里写死
	-->
    <security:authentication-manager>
    	<security:authentication-provider>
    		<security:user-service>
			    <security:user name="user" password="{noop}user"
			    authorities="ROLE_USER" />
		    	<security:user name="admin" password="{noop}admin"
		    	authorities="ROLE_ADMIN" />
		    </security:user-service>
    	</security:authentication-provider>
    </security:authentication-manager>
</beans>

登录界面

<%@taglib prefix="security" "uri='http://www.springframework.org/ security /tags"%>


<form action="${pageContext.request.contextPath}/login" method="post">
	<!--在认证from表单内携带token,需要在文件头添加SpringSecurity标签库-->
	<!--<security:csrfInput/>-->
	<div class="form-group has-feedback">
		<input type="text" name="username" class="form-control" laceholder="用户名">
		<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
		</div>
		<div class="form-group has-feedback">
			<input type="password" name="password" class="form-control" placeholder="密码">
			<span class="glyphicon glyphicon-lock form-control-feedback"></span>
		</div> 
		<div class="col-xs-4">
			<button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
		</div>
	<!-- /.col -->
	</div>
</form>

常用过滤器

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。

WebAsyncManagerIntegrationFilter

此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager

HeaderWriterFilter

向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制(仅限于JSP页面)

CsrfFilter

csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息, 如果不包含,则报错。起到防止csrf攻击的效果。

LogoutFilter

匹配URL为/logout的请求,实现用户退出,清除认证信息。

UsernamePasswordAuthenticationFilter

认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。

DefaultLoginPageGeneratingFilter

如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。

DefaultLogoutPageGeneratingFilter

由此过滤器可以生产一个默认的退出登录页面

BasicAuthenticationFilter

此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。

RequestCacheAwareFilter

通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest

SecurityContextHolderAwareRequestFilter

针对ServletRequest进行了一次包装,使得request具有更加丰富的API

AnonymousAuthenticationFilter

当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。 spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
当用户以游客身份登录的时候,也就是可以通过设置某些接口可以匿名访问

SessionManagementFilter

SecurityContextRepository限制同一用户开启多个会话的数量

ExceptionTranslationFilter

异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常

FilterSecurityInterceptor

获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权 限
该过滤器限制哪些资源可以访问,哪些不能够访问。

过滤器链加载原理

DelegatingFilterProxy

<filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

上面是web.xml中配置的名为springSecurityFilterChain的过滤器DelegatingFilterProxy,下面对DelegatingFilterProxy源码里重要代码进行说明

public class DelegatingFilterProxy extends GenericFilterBean {
    @Nullable
    private String contextAttribute;
    @Nullable
    private WebApplicationContext webApplicationContext;
    @Nullable
    private String targetBeanName;
    private boolean targetFilterLifecycle;
    @Nullable
    private volatile Filter delegate;// 真正被加载的过滤器
    private final Object delegateMonitor;
	...
	// 这个方法是过滤器的入口
    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 = this.findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                    }
					// 1. 初始化过滤器delegate
                    delegateToUse = this.initDelegate(wac);
                }

                this.delegate = delegateToUse;
            }
        }
		// 3. 执行FilterChainProxy,即delegateToUse过滤链接
        this.invokeDelegate(delegateToUse, request, response, filterChain);
    }
	// 2. DelegatingFilterProxy通过springSecurityFilterChain这个名称,得到了一个FilterChainProxy过滤器,最终在第三步执行了这个过滤器。
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    	// 断点得知targetBeanName 值 "springSecurityFilterChain"
        String targetBeanName = this.getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        // 断点得知delegate就是FilterChainProxy过滤器,
        Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
        if (this.isTargetFilterLifecycle()) {
            delegate.init(this.getFilterConfig());
        }
        return delegate;
    }

    protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        delegate.doFilter(request, response, filterChain);
    }
}

FilterChainProxy

上面源码的第3步执行了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<SecurityFilterChain> filterChains;
    private FilterChainProxy.FilterChainValidator filterChainValidator;
    private HttpFirewall firewall;

    public FilterChainProxy() {
        this.filterChainValidator = new FilterChainProxy.NullFilterChainValidator();
        this.firewall = new StrictHttpFirewall();
    }
	// 先用SecurityFilterChain 对象初始化了FilterChainProxy
	// 那么SecurityFilterChain 是什么,看后面代码
    public FilterChainProxy(SecurityFilterChain chain) {
        this(Arrays.asList(chain));
    }
	// SecurityFilterChain数组也能初始化了FilterChainProxy
	// SecurityFilterChain到底是什么呢?
    public FilterChainProxy(List<SecurityFilterChain> 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 {
        	// 1. 具体操作调用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);
        // 2. 封装过滤器链,多个在这里被封装进数组List集合,点进去getFilters方法看第3步
        // 断点到这一步会发现十五个过滤器都在filters数组里
        List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
        if (filters != null && filters.size() != 0) {
            FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
            // 4. 加载过滤器链
            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<Filter> getFilters(HttpServletRequest request) {
        Iterator var2 = this.filterChains.iterator();
		// 3. 将过滤器链封装到SecurityFilterChain中,然后返回SecurityFilterChain.getFilters()
        SecurityFilterChain chain;
        do {
            if (!var2.hasNext()) {
                return null;
            }

            chain = (SecurityFilterChain)var2.next();
        } while(!chain.matches(request));
		
        return chain.getFilters();
    }

SecurityFilterChain

上面源码看出过滤器都被封装进SecurityFilterChain中了
接下来看SecurityFilterChain,这是个接口,实现类也只有一个,这才是web.xml中配置的过滤器链对象!

// SecurityFilterChain接口
public interface SecurityFilterChain {
    boolean matches(HttpServletRequest var1);

    List<Filter> getFilters();
}
// 实现类
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
    private static final Log logger = LogFactory.getLog(DefaultSecurityFilterChain.class);
    private final RequestMatcher requestMatcher;
    private final List<Filter> filters;

    public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) {
        this(requestMatcher, Arrays.asList(filters));
    }

    public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
        logger.info("Creating filter chain: " + requestMatcher + ", " + filters);
        this.requestMatcher = requestMatcher;
        this.filters = new ArrayList(filters);
    }

    public RequestMatcher getRequestMatcher() {
        return this.requestMatcher;
    }

    public List<Filter> getFilters() {
        return this.filters;
    }

    public boolean matches(HttpServletRequest request) {
        return this.requestMatcher.matches(request);
    }

    public String toString() {
        return "[ " + this.requestMatcher + ", " + this.filters + "]";
    }
}

CsrfFilter

SpringSecurity的csrf防护机制,CSRF(Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。
SpringSecurity中CsrfFilter过滤器说明

public final class CsrfFilter extends OncePerRequestFilter {
    public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
    private static final String SHOULD_NOT_FILTER = "SHOULD_NOT_FILTER" + CsrfFilter.class.getName();
    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;
    }

    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        return Boolean.TRUE.equals(request.getAttribute(SHOULD_NOT_FILTER));
    }
	// 过滤器入口,从这个方法看出scrf机制吧请求方式分成两类来处理
    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 {
        	// 第二类:其他请求都要求携带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);
            }
        }
    }
}

认证页面,请求方式为POST,所以会出现403
解决方案:方式一:直接禁用csrf,不推荐。方式二:在认证页面携带token请求

禁用csrf

见上文中web.xml中被注释的配置

<!--禁用csrf防护机制,不推荐-->
<security:csrf disable="true"/>
在认证页面携带token请求

见登录界面login.jsp中,先在文件头添加SpringSecurity标签库,然后在from表单第一行添加<security:csrfInput/>
HttpSessionCsrfTokenRepository对象负责生成token并放入session域中

SpringSecurity使用数据库数据完成认证

认证流程

UsernamePasswordAuthenticationFilter是主要负责认证的过滤器

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对象的authenticate方法实现认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
}

发送的登录请求,请求地址需要是 /login,请求方法 post, 获取并调用AuthenticationManager对象的方法实现认证

AuthenticationManager

然后看AuthenticationManager的实现类

// 接口
public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

实现类ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    private static final Log logger = LogFactory.getLog(ProviderManager.class);
    private AuthenticationEventPublisher eventPublisher;
    private List<AuthenticationProvider> providers;
    protected MessageSourceAccessor messages;
    private AuthenticationManager parent;
    private boolean eraseCredentialsAfterAuthentication;
    
	// AuthenticationProvider: SpringSecurity针对每一种认证(微信、qq、账号密码等),都封装了一个AuthenticationProvider对象
    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, (AuthenticationManager)null);
    }
    public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
        ...
    }
	// 执行authenticate方法实现认证的
    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 (InternalAuthenticationServiceException | AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (AuthenticationException var14) {
                    lastException = var14;
                }
            }
        ...
    }
}

AuthenticationProvider

抽象类AbstractUserDetailsAuthenticationProvider,其实现类DaoAuthenticationProvider
AbstractUserDetailsAuthenticationProvider.authenticate中调用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;
    
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
        	// UserDetails: SpringSecurity自己的用户对象
			// this.getUserDetailsService(): 得到UserDetailsService的一个实现类
			// loadUserByUsername(username):里面就是真正的认证逻辑
			// 我们需要编写一个UserDetailService的实现类,在loadUserByUsername方法中返回一个UserDetails对象
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            // 若返回null,异常,认证失败
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
            	// 得到userDetails对象,正常返回
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }

        return super.createSuccessAuthentication(principal, authentication, user);
    }

    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
        }

    }

    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }

    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = null;
    }

    protected PasswordEncoder getPasswordEncoder() {
        return this.passwordEncoder;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }

    public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
        this.userDetailsPasswordService = userDetailsPasswordService;
    }
}

这个UserDetails对象返回到AbstractUserDetailsAuthenticationProviderauthenticate

AbstractUserDetailsAuthenticationProvider

public abstract class AbstractUserDetailsAuthenticationProvider implements
		...
		// 最后一行返回,调用了createSuccessAuthentication方法,看下面
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
	protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		// 又封装了一次UsernamePasswordAuthenticationToken,刚开始封装过一次,再点进去看看这个构造方法
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());

		return result;
	}
}

UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken有两个构造方法

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
	// 认证前,两个参数
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	// 认证成功后,三个参数
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		// 看看父类AbstractAuthenticationToken做了什么
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}
}

AbstractAuthenticationToken

super(null)对应上述两个参数情况,super(authorities)对应上述三个参数情况

public abstract class AbstractAuthenticationToken implements Authentication,
	public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
		// 上述两个参数时
		if (authorities == null) {
			this.authorities = AuthorityUtils.NO_AUTHORITIES;
			return;
		}
		// 上述三个参数时,多了添加权限信息的步骤
		for (GrantedAuthority a : authorities) {
			if (a == null) {
				// 没有权限信息抛异常,咱们需要牢记自定义认证业务逻辑返回的UserDetails对象中一定要放置权限信息
				throw new IllegalArgumentException(
						"Authorities collection cannot contain any null elements");
			}
		}
		ArrayList<GrantedAuthority> temp = new ArrayList<>(
				authorities.size());
		temp.addAll(authorities);
		this.authorities = Collections.unmodifiableList(temp);
	}
}

咱们回到最初的地方UsernamePasswordAuthenticationFilter,你看好看了,这可是个过滤器,咱们分析这么久,都没提到doFilter方法,你不觉得心里不踏实?

可是这里面也没有doFilter呀?那就从父类AbstractAuthenticationProcessingFilter中找,晕QAQ没完了。。。

AbstractAuthenticationProcessingFilter

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    // doFilter!!!
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }

                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                // 失败
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
            	// 失败
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
			// 认证成功
            this.successfulAuthentication(request, response, chain, authResult);
        }
    }

	// 认证成功走successfulAuthentication
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }
        // SecurityContextHolder.getContext()得到SecurityContext
		// 将认证信息存储到SecurityContext中
        SecurityContextHolder.getContext().setAuthentication(authResult);
        // 登陆成功,调用rememberMeServices.loginSuccess
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }
	// 认证失败走unsuccessfulAuthentication
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication request failed: " + failed.toString(), failed);
            this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
            this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
        }
        // 登陆失败,调用rememberMeServices.loginFail
        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }
}

在successfulAuthentication内部,将认证信息存储到了SecurityContext中。并调用了loginSuccess方法,这就是 常见的“**记住我”功能!**此功能具体应用,咱们后续再研究!

初步实现认证功能

根据以上的源码分析,接下来完成SpringSecurity使用数据库数据完成认证的实现

创建UserDetailsService

// 接口
public interface UserService extends UserDetailsService {
    public void save(SysUser user);
    public List<SysUser> findAll();
    public Map<String, Object> 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(sysUser==null){
        //若用户名不对,直接返回null,表示认证失败。
        return null;
    }
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    List<SysRole> roles = sysUser.getRoles();
    for (SysRole role : roles) {
    	authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
    }
    //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
    return new User(sysUser.getUsername(), "{noop}"+sysUser.getPassword(), authorities);
}

UserDetailsService接口的实现类


@Service
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    @Autowired
    private RoleService roleService;
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Override
    public void save(SysUser user) {
        user.setPassword(user.getPassword());
        userDao.save(user);
	}
}

SpringSecurity.xml指定认证使用的业务对象

修改之前写死的认证用户信息的来源

<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
	<security:authentication-provider user-service-ref="userServiceImpl">
	</security:authentication-provider>
</security:authentication-manager>

加密认证

在IOC容器中提供加密对象

SpringSecurity.xml

<!--加密对象-->
<bean id="passwordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--设置Spring Security认证用户信息的来源-->
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userServiceImpl">
        <!--指定认证使用的加密对象-->
        <security:password-encoder ref="passwordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>

修改认证方法

去掉{noop},该方法可以让我们的密码不加密

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = userDao.findByName(username);
    if(sysUser==null){
        //若用户名不对,直接返回null,表示认证失败。
        return null;
    }
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    List<SysRole> roles = sysUser.getRoles();
    for (SysRole role : roles) {
    	authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
    }
    //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
    return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
}

修改实现类

@Service
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    @Autowired
    private RoleService roleService;
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Override
    public void save(SysUser user) {
        //对密码进行加密,然后再入库
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        userDao.save(user);
	}
}

学习视频

【黑马程序员】手把手教你精通新版SpringSecurity

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值