SpringSecurity(1)过滤器链、自定义认证页面、退出登录和加密认证

1、权限管理的概念

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

理几乎出现在任何系统里面,前提是需要有用户和密码认证的系统。

在权限管理的概念中,有两个非常重要的名词:

  • 认证:通过用户名和密码成功登陆系统后,让系统得到当前用户的角色身份。

    登录和认证的区别:一般简单的登录是只要验证用户的用户名和密码是否正确,但是认证还包括查询出用户的角色和权限信息。

  • 授权:系统根据当前用户的角色,给其授予对应可以操作的权限资源。

权限管理中三个重要的对象:

  • 用户:主要包含用户名,密码和当前用户的角色信息,可实现认证操作。

  • 角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。

  • 权限:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。

注:这三个对象中,用户与角色是多对多的关系,角色与权限是多对多的关系,用户与权限没有直接关系,

二者是通过角色来建立关联关系的。

2、Spring Security入门

初始化的工程使用的框架是Spring SpringMVC和MyBatis,数据库是mysql。

数据库脚本:

DROP TABLE IF EXISTS `sys_permission`;

CREATE TABLE `sys_permission` (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `permission_NAME` varchar(30) DEFAULT NULL COMMENT '菜单名称',
  `permission_url` varchar(100) DEFAULT NULL COMMENT '菜单地址',
  `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父菜单id',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role` (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称',
  `ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `sys_role_permission`;

CREATE TABLE `sys_role_permission` (
  `RID` int(11) NOT NULL COMMENT '角色编号',
  `PID` int(11) NOT NULL COMMENT '权限编号',
  PRIMARY KEY (`RID`,`PID`),
  KEY `FK_Reference_12` (`PID`),
  CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
  CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `sys_user`;

CREATE TABLE `sys_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) NOT NULL COMMENT '用户名称',
  `password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
  `status` int(1) DEFAULT '1' COMMENT '1开启0关闭',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `sys_user_role`;

CREATE TABLE `sys_user_role` (
  `UID` int(11) NOT NULL COMMENT '用户编号',
  `RID` int(11) NOT NULL COMMENT '角色编号',
  PRIMARY KEY (`UID`,`RID`),
  KEY `FK_Reference_10` (`RID`),
  CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
  CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Spring Security是spring采用AOP思想,基于servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的授权功能,是一款非常优秀的权限管理框架。

2.1 jar包导入

Spring Security主要jar包功能介绍

  • spring-security-core.jar:核心包,任何Spring Security功能都需要此包。

  • spring-security-web.jar:web工程必备,包含过滤器和相关的Web安全基础结构代码。

  • spring-security-confifig.jar:用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。

  • spring-security-taglibs.jar:Spring Security提供的动态标签库,jsp页面可以用。

如果是maven项目,因为具有依赖的传递性,所以只需要导入下面的两个依赖即可:

<!--SpringSecurity相关的包-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>

2.2 web.xml配置过滤器链

Spring Security是基于过滤器链来实现认证和授权的,所以需要在web.xml中配置:


<!--上面省略了很多其他的配置,比如SpringMvc的前置Servlet-->

<!--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>

2.3 编写SpringSecurity的配置文件

Spring Security需要配置很多的东西,比如用户,权限,拦截规则等,所以编写一个单独的配置文件spring-security.xml,来进行配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:security="http://www.springframework.org/schema/security"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
			    http://www.springframework.org/schema/beans/spring-beans.xsd
			    http://www.springframework.org/schema/context
			    http://www.springframework.org/schema/context/spring-context.xsd
			    http://www.springframework.org/schema/aop
			    http://www.springframework.org/schema/aop/spring-aop.xsd
			    http://www.springframework.org/schema/tx
			    http://www.springframework.org/schema/tx/spring-tx.xsd
			    http://www.springframework.org/schema/mvc
			    http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/security
			    http://www.springframework.org/schema/security/spring-security.xsd">

    <!--配置springSecurity-->
    <!--
        auto-config="true"  表示自动加载springsecurity的配置文件
        use-expressions="true" 表示使用spring的el表达式来配置springsecurity
    -->
    <security:http auto-config="true" use-expressions="true">
        
        <!--拦截资源-->
        <!--
            pattern="/**" 表示拦截所有资源
            access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
    </security:http>

    <!--设置Spring Security认证用户信息的来源-->
    <!--
        springsecurity默认的认证必须是加密的,加上{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>

2.4 在Spring的主配置文件applicationContext.xml中引入spring-security.xml

因为项目启动的时候,只会去加载web.xml,而我们在web.xml中配置了SpringMvc的前置servlet和Spring的ContextLoaderListener,里面指定了相关的配置文件路径,所以如果想要项目启动可以加载spring-security.xml,按理也是需要在web.xml中进行配置的,我们在applicationContext.xml中引入也是达到一样的效果。

<!--引入SpringSecurity配置文件,才会被Spring容器加载-->
<import resource="classpath:spring-security.xml"/>

这里可能有人存在一个疑问:到底应该在把spring-security的相关配置交给Spring管理还是SpringMVC管理?

Spring的IOC容器是父容器,通过Http的方式是不能被访问的,SpringMVC的容器是子容器,Http访问的就是子容器中的Controller,子容器可以访问父容器的Bean,而父容器不能访问子容器。这也就是我们经常在Controller中注入Service的原因。而我们的Spring Secutiry的配置,肯定是不需要让外界知道的,所以我们应该把他交给Spring的IOC容器管理。

2.5 启动项目测试

2.5.1 为啥到了不是我们写的登录页面

项目启动需要进行tomcat的配置,相关的配置请自行百度;

项目启动完成,会自动访问:http://localhost:8080/index.jsp,但是我们看到给我们弹出的页面是:

在这里插入图片描述

为什么会这样?

因为我们配置Spring Security的拦截所有的路径:

<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>

因为当前还没登录,所以就会跳转到login登录页面,但是这个登录页面,以及这个跳转操作,都不是我们实现的,而是Spring Security内部帮我们实现的。如果是早期的版本,这个登录页面是没样式的,很丑。

2.5.2 CSRF

F12查看这个页面的原代码,会发现有这样一行:

<input name="_csrf" type="hidden" value="5f4a7ee0-1494-483b-af32-98670facbebf">

这一行很重要,注意!这可是你想使用自定义页面时,排查问题的一条重要线索!后面会讲到。

2.5.3 AccessDeniedException

此时查看后端控制台,会发现一个这样的异常:

org.springframework.security.access.AccessDeniedException: Access is denied

这是因为项目启动会直接访问index.jsp页面,该请求会拦截了,因为需要ROLE_USER才能访问,所以Spring Security 抛出了这个异常。

2.5.4 15个过滤器

仔细找的话,你还能发现控制台里面的日志里面显示加载了下面这15个过滤器:

'SecurityContextPersistenceFilter'
'WebAsyncManagerIntegrationFilter'
'HeaderWriterFilter'
'CsrfFilter'
'LogoutFilter'
'UsernamePasswordAuthenticationFilter'
'DefaultLoginPageGeneratingFilter'
'DefaultLogoutPageGeneratingFilter'
'BasicAuthenticationFilter'
'RequestCacheAwareFilter'
'SecurityContextHolderAwareRequestFilter'
'AnonymousAuthenticationFilter'
'SessionManagementFilter'
'ExceptionTranslationFilter'
'FilterSecurityInterceptor'

这些就是之前说的Spring Security的过滤器链。

2.5.5 使用user用户登录

如果想要正常的访问到首页,我们需要使用user登录,因为user用户拥有ROLE_USER角色。

3、Spring Security的过滤器链

3.1 Spring Security常用过滤器介绍

过滤器是一种典型的AOP思想,关于什么是过滤器,就不赘述了,谁还不知道凡是web工程都能用过滤器?

接下来咱们就一起看看Spring Security中这些过滤器都是干啥用的,源码我就不贴出来了,有名字,大家可以自

己在idea中Double Shift去。我也会在后续的学习过程中穿插详细解释。

  • 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请求验证是否包含系统生成的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中。

    spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

  • org.springframework.security.web.session.SessionManagementFilter

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

  • org.springframework.security.web.access.ExceptionTranslationFilter

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

  • org.springframework.security.web.access.intercept.FilterSecurityInterceptor

    获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

那么,是不是spring security一共就这么多过滤器呢?答案是否定的!随着spring-security.xml配置的添加,还会出现新的过滤器。

那么,是不是spring security每次都会加载这些过滤器呢?答案也是否定的!随着spring-security.xml配置的修改,有些过滤器可能会被去掉。

3.2 Spring Security过滤器链加载原理

通过前面十五个过滤器功能的介绍,对于SpringSecurity简单入门中的疑惑是不是在心中已经有了答案了呀?

但新的问题来了!我们并没有在web.xml中配置这些过滤器啊?它们都是怎么被加载出来的?

友情提示:前方高能预警,吃饭喝水打瞌睡的请睁大眼睛,专注心神!

3.2.1 DelegatingFilterProxy

我们在web.xml中配置了一个名称为springSecurityFilterChain的过滤器DelegatingFilterProxy,接下我直接对

DelegatingFilterProxy源码里重要代码进行说明,其中删减掉了一些不重要的代码,大家注意我写的注释就行了!

package org.springframework.web.filter;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

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;

    //注:doFilter才是过滤器的入口,直接从这看!
    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?");
                    }
					//第一步:doFilter中最重要的一步,初始化上面私有过滤器属性delegate
                    delegateToUse = this.initDelegate(wac);
                }

                this.delegate = delegateToUse;
            }
        }
		//第三步:执行FilterChainProxy过滤器
        this.invokeDelegate(delegateToUse, request, response, filterChain);
    }
	//第二步:直接看最终加载的过滤器到底是谁
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        String targetBeanName = this.getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        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);
    }

    protected void destroyDelegate(Filter delegate) {
        if (this.isTargetFilterLifecycle()) {
            delegate.destroy();
        }
    }
}

第二步debug结果如下:

在这里插入图片描述

由此可知,DelegatingFilterProxy通过springSecurityFilterChain这个名称,得到了一个FilterChainProxy过滤器,最终在第三步执行了这个过滤器。

3.2.2 FilterChainProxy

注意代码注释!注意代码注释!注意代码注释!

package org.springframework.security.web;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.web.filter.GenericFilterBean;

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;

	//咿!?可以通过一个叫SecurityFilterChain的对象实例化出一个FilterChainProxy对象 
    //这FilterChainProxy又是何方神圣?会不会是真正的过滤器链对象呢?先留着这个疑问!
    public FilterChainProxy(SecurityFilterChain chain) {
        this(Arrays.asList(chain));
    }

    //又是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);
                //第一步:具体操作调用下面的doFilterInternal方法了
                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<Filter> 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);
        }
    }

    //第三步:封装过滤器链到SecurityFilterChain中!
    private List<Filter> 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中了。

3.2.3 SecurityFilterChain

最后看SecurityFilterChain,这是个接口,实现类也只有一个,这才是web.xml中配置的过滤器链对象!

public interface SecurityFilterChain {
    boolean matches(HttpServletRequest var1);

    List<Filter> getFilters();
}

实现类:DefaultSecurityFilterChain

package org.springframework.security.web;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.util.matcher.RequestMatcher;

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 + "]";
    }
}

总结:通过此章节,我们对SpringSecurity工作原理有了一定的认识。但理论千万条,功能第一条,探寻底层,是为了更好的使用框架。

那么,言归正传!到底如何使用自己的页面来实现SpringSecurity的认证操作呢?要完成此功能,首先要有一套自己的页面!

4、SpringSecurity使用自定义认证页面

4.1 spring_security.xml修改如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:security="http://www.springframework.org/schema/security"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
			    http://www.springframework.org/schema/beans/spring-beans.xsd
			    http://www.springframework.org/schema/context
			    http://www.springframework.org/schema/context/spring-context.xsd
			    http://www.springframework.org/schema/aop
			    http://www.springframework.org/schema/aop/spring-aop.xsd
			    http://www.springframework.org/schema/tx
			    http://www.springframework.org/schema/tx/spring-tx.xsd
			    http://www.springframework.org/schema/mvc
			    http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/security
			    http://www.springframework.org/schema/security/spring-security.xsd">

    <!--释放静态资源
        注意:SpringMvc中也有释放静态资源,但是和这里的不同
        SpringMVC释放静态资源是为了不让前端控制器去拦截静态资源
        而这里的释放静态资源是为了不让Spring Security拦截静态资源
    -->
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/plugins/**" security="none"/>
    <security:http pattern="/failer.jsp" security="none"/>

    <!--配置springSecurity-->
    <!--
    auto-config="true"  表示自动加载springsecurity的配置文件
    use-expressions="true" 表示使用spring的el表达式来配置springsecurity
    -->
    <security:http auto-config="true" use-expressions="true">
        <!--让认证页面可以匿名访问
            表示login.jsp页面不登陆就可以访问,但是依然会经过后续的过滤器链,因为接下来的操作都是Spring Security帮我们处理的,所以必须要经过
        -->
        <security:intercept-url pattern="/login.jsp" access="permitAll()"/>
        <!--拦截资源-->
        <!--
        pattern="/**" 表示拦截所有资源
        access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
        <!--配置认证信息
            login-page:配置登录页面
            login-processing-url:配置执行登录操作的请求路径,login是默认的,我们可以直接把自己的登录改成login就可以
            default-target-url:登录成功默认跳转的地址
            authentication-failure-url:登录(认证)失败跳转路径
            username-parameter:form表单登录时,input的name属性的值<input name="username"/>,username是默认的,可以自定义
            password-parameter:form表单登录时,input的name属性的值<input name="password"/>,password是默认的,可以自定义
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"
                             username-parameter="username"
                             password-parameter="password" />
        <!--配置退出登录信息
            logout-url:配置登出操作的请求路径
            logout-success-url:登出成功后请求的路径,这里设置成登出后跳转到登录页面
        -->
        <security:logout logout-url="/logout"
                         logout-success-url="/login.jsp"/>

    </security:http>

    <!--设置Spring Security认证用户信息的来源-->
    <!--
        springsecurity默认的认证必须是加密的,加上{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>

4.2 修改login.jsp

上面的配置了登录页面是我们自己的login.jsp,所以接下来访问的登录页面就不会是Spring Security自己的了,而是要访问我们自己的登录页面login.jsp。不过我们需要把登录的请求改成/login(这是Spring Security默认的,他会帮我们处理这个请求),并且提交方式必须是post。

<div class="login-box-body">
    <p class="login-box-msg">登录系统</p>
    <form action="${pageContext.request.contextPath}/login" method="post">
        <div class="form-group has-feedback">
            <input type="text" name="username" class="form-control"
                   placeholder="用户名"> <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="row">
            <div class="col-xs-8">
                <div class="checkbox icheck">
                    <label><input type="checkbox" name="remember-me" value="true"> 记住 下次自动登录</label>
                </div>
            </div>
            <!-- /.col -->
            <div class="col-xs-4">
                <button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
            </div>
            <!-- /.col -->
        </div>
    </form>

4.3 为什么会是403

上述修改完成,启动项目,会到我们自己的登录页面:

在这里插入图片描述

使用user/user登录,结果却是403

在这里插入图片描述

这是为什么呢?

还记得我们前面查看他默认的登录页面的源码的时候,有这么一行代码:

<input name="_csrf" type="hidden" value="5f4a7ee0-1494-483b-af32-98670facbebf">

我们的login.jsp页面里面根本没加上这个的,所以就出现403。因为Spring Security会检查post请求是否携带了csrf的token,如果没有,就403。这是为了防止csrf攻击。

4.4 解决403

可以参考这篇博客,对于前后端不分离和前后端分离的,这里都讲到了解决方案:https://www.jianshu.com/p/969784677864

既然上面出现了403,那么应该如何解决呢?

有两种办法:一:关掉csrf检查,二:在login.jsp页面加上csrf

4.4.1 关掉csrf检查

修改spring-security.xml文件,在security:http 中加上:

<!--去掉csrf拦截的过滤器,直接去掉的这种方式不建议使用,因为可能存在CSRF攻击-->
<security:csrf disabled="true"/>

再次启动项目,登录就是正常的了,但是请注意,我们的系统是存在csrf攻击的。

4.4.2 jsp页面添加<security:csrfInput/>

上面的方式虽然可以达到我们的登录成功的目的,但是是不安全的,存在csrf攻击。那么如何去安全的解决这个问题?

如果项目是前后端不分离的,就跟我这个示例代码一样,那么只需要在jsp页面中添加下面的代码:

在这里插入图片描述

<%--引入security的标签,security这个的取值是任意的--%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
    
<form action="${pageContext.request.contextPath}/login" method="post">
    <%--csrf--%>
    <security:csrfInput/>

                                     

如果是前后端分离的项目,请参考:https://www.jianshu.com/p/969784677864

4.5 SpringSecurity的csrf防护机制

CSRF(Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。

4.5.1 SpringSecurity中CsrfFilter过滤器说明

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.security.web.csrf;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

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;
    }

    //通过这里可以看出SpringSecurity的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都要被验证携带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<String> allowedMethods;

        private DefaultRequiresCsrfMatcher() {
            this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
        }

        public boolean matches(HttpServletRequest request) {
            return !this.allowedMethods.contains(request.getMethod());
        }
    }
}

注:HttpSessionCsrfTokenRepository对象负责生成token并放入session域中。

通过源码分析,我们明白了,自己的认证页面,请求方式为POST(“GET”, “HEAD”, “TRACE”, “OPTIONS” 除了这四种请求之外的,都需要加上token),但却没有携带token,所以才出现了403权限不足的异常。那么如何处理这个问题呢?

方式一:直接禁用csrf,不推荐。

方式二:在认证页面携带token请求。

4.6 退出登录

修改header.jsp的注销按钮:

在这里插入图片描述

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

上面的示例中,我们一直是使用在内存中配置的用户进行认证和授权,实际开发中,肯定是应该查询数据库的用户、角色和权限表来实现的。所以我们需要对使用数据库完成认证的流程先进行分析:

5.1 认证流程分析

5.1.1 UsernamePasswordAuthenticationFilter

先看主要负责认证的过滤器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";//从这里看出默认的用户名是username
    private String passwordParameter = "password";//从这里看出默认的密码是password
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        //可以知道默认的登录请求是 /login
        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);
        }
    }
}

5.1.2 AuthenticationManager的实现类ProviderManager

由上面源码得知,真正认证操作在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<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 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;
        }
    }
}

5.1.3 AbstractUserDetailsAuthenticationProvider

从上面的这句代码:

//provider是某种认证方式对象, authentication 是我们前面传过来的UsernamePasswordAuthenticationToken对象
result = provider.authenticate(authentication);

点进去,看到的是一个接口:

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws AuthenticationException;

    boolean supports(Class<?> var1);
}

这个接口有很多的实现类,针对不同的登录方式对应不同的实现类,我们本次使用的用户名密码登录,使用的实现类是:AbstractUserDetailsAuthenticationProvider(等会可以通过debug的方式验证)

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 {
                //这个方法是执行真正的认证操作的,在本类中,是个抽象方法,所以我们需要看它的实现类
                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;
}

5.1.4 DaoAuthenticationProvider

DaoAuthenticationProvider是AbstractUserDetailsAuthenticationProvider的唯一子类,源码如下:

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 final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            //重点来了!主要就在这里了! 
    //可别忘了,咱们为什么要翻源码,是想用自己数据库中的数据实现认证操作啊! 
    //UserDetails就是SpringSecurity自己的用户对象。 
    //this.getUserDetailsService()其实就是得到UserDetailsService的一个实现类 		 
    //loadUserByUsername里面就是真正的认证逻辑 
    //也就是说我们可以直接编写一个UserDetailsService的实现类,告诉SpringSecurity就可以了! 	  
    //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);
    }
}

5.1.5 AbstractUserDetailsAuthenticationProvider中authenticate返回值

按理说到此已经知道自定义认证方法的怎么写了,但咱们把返回的流程也大概走一遍,上面不是说到返回了一个

UserDetails对象对象吗?跟着它,就又回到了AbstractUserDetailsAuthenticationProvider对象中authenticate方法的最后一行了。

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
			//。。。。这里省略了很多行
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        //。。。。这里省略了很多行
      
		//最后一行返回值,调用了createSuccessAuthentication方法,此方法就在下面!
        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

	//咿!?怎么又封装了一次UsernamePasswordAuthenticationToken,开局不是已经封装过了吗?
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        //那就从构造方法点进去看看,这才干啥了。//认证成功后,调用的是这个带有三个参数的
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }

5.1.6 UsernamePasswordAuthenticationToken

来到UsernamePasswordAuthenticationToken对象发现里面有两个构造方法

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 510L;
    private final Object principal;
    private Object credentials;

    //认证成功前,调用的是这个带有两个参数的。
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    //认证成功后,调用的是这个带有三个参数的
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        //看看父类干了什么!
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
 }

5.1.7 AbstractAuthenticationToken

再点进去super(authorities)看看

public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
    private final Collection<GrantedAuthority> authorities;
    private Object details;
    private boolean authenticated = false;

    public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
        //这是两个参数那个分支!
        if (authorities == null) {
            this.authorities = AuthorityUtils.NO_AUTHORITIES;
        } else {
            //三个参数的,看这里!
            Iterator var2 = authorities.iterator();
			//原来是多个了添加权限信息的步骤
            GrantedAuthority a;
            do {
                if (!var2.hasNext()) {
                    ArrayList<GrantedAuthority> temp = new ArrayList(authorities.size());
                    temp.addAll(authorities);
                    this.authorities = Collections.unmodifiableList(temp);
                    return;
                }

                a = (GrantedAuthority)var2.next();
            } while(a != null);
			//若没有权限信息,是会抛出异常的!
            throw new IllegalArgumentException("Authorities collection cannot contain any null elements");
        }
    }
}

由此,咱们需要牢记自定义认证业务逻辑返回的UserDetails对象中一定要放置权限信息啊!

现在可以结束源码分析了吧?先不要着急!

咱们回到最初的地方UsernamePasswordAuthenticationFilter,你看好看了,这可是个过滤器,咱们分析这么

久,都没提到doFilter方法,你不觉得心里不踏实?可是这里面也没有doFilter呀?那就从父类找!

5.1.8 AbstractAuthenticationProcessingFilter

点开AbstractAuthenticationProcessingFilter,删掉不必要的代码!

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    //...
    
    public void afterPropertiesSet() {
        Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
    }

    //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);
                //失败走unsuccessfulAuthentication
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
                //失败走unsuccessfulAuthentication
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

            //成功走successfulAuthentication
            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);
        }

        //认证成功,将认证信息存储到SecurityContext中!
        SecurityContextHolder.getContext().setAuthentication(authResult);
        
        //登录成功调用rememberMeServices
        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);
        }

        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }

   
}

可见AbstractAuthenticationProcessingFilter这个过滤器对于认证成功与否,做了两个分支,成功执行

successfulAuthentication,失败执行unsuccessfulAuthentication。

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

5.2 初步实现认证功能

5.2.1 让我们自己的UserService接口继承UserDetailsService

毕竟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);
}

5.2.2 在UserServiceImpl编写loadUserByUsername业务

/**
     * Spring Security进行用户认证的方法
     * @param username                      页面传过来的用户名
     * @return  UserDetails                 UserDetails是Spring Security自己的用户对象,如果返回null就表示认证失败
     * @throws UsernameNotFoundException
     */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    try {
        //根据用户名从数据库中查询到用户信息
        SysUser sysUser = userDao.findByName(username);
        if(sysUser == null) {
            //若用户名不对,直接返回null,表示认证失败。
            return null;
        }
        //把sysUser封装成Spring Security可以认识的UserDetails对象
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        //得到当前用户的所有角色的集合
        List<SysRole> roles = sysUser.getRoles();
        //遍历角色(权限),存入到authorities中
        for (SysRole role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
        }

        /**
             * 最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
             * 浏览器输入的密码已经被Spring Security保存了,这里封装之后,会进行比对,如果是相同的就是认证通过,否则认证失败
             */
        UserDetails userDetails = new User(username,"{noop}"+sysUser.getPassword(),authorities);
        return userDetails;
    }catch (Exception e){
        e.printStackTrace();
        return null;
    }

}

5.2.3 spring-security.xml中配置用户的数据来源为数据库

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:security="http://www.springframework.org/schema/security"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
			    http://www.springframework.org/schema/beans/spring-beans.xsd
			    http://www.springframework.org/schema/context
			    http://www.springframework.org/schema/context/spring-context.xsd
			    http://www.springframework.org/schema/aop
			    http://www.springframework.org/schema/aop/spring-aop.xsd
			    http://www.springframework.org/schema/tx
			    http://www.springframework.org/schema/tx/spring-tx.xsd
			    http://www.springframework.org/schema/mvc
			    http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/security
			    http://www.springframework.org/schema/security/spring-security.xsd">

    <!--释放静态资源
        注意:SpringMvc中也有释放静态资源,但是和这里的不同
        SpringMVC释放静态资源是为了不让前端控制器去拦截静态资源
        而这里的释放静态资源是为了不让Spring Security拦截静态资源
    -->
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/plugins/**" security="none"/>
    <security:http pattern="/failer.jsp" security="none"/>

    <!--配置springSecurity-->
    <!--
    auto-config="true"  表示自动加载springsecurity的配置文件
    use-expressions="true" 表示使用spring的el表达式来配置springsecurity
    -->
    <security:http auto-config="true" use-expressions="true">
        <!--让认证页面可以匿名访问
            表示login.jsp页面不登陆就可以访问,但是依然会经过后续的过滤器链,因为接下来的操作都是Spring Security帮我们处理的,所以必须要经过
        -->
        <security:intercept-url pattern="/login.jsp" access="permitAll()"/>
        <!--拦截资源-->
        <!--
        pattern="/**" 表示拦截所有资源
        access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
        <!--配置认证信息
            login-page:配置登录页面
            login-processing-url:配置执行登录操作的请求路径,login是默认的,我们可以直接把自己的登录改成login就可以
            default-target-url:登录成功默认跳转的地址
            authentication-failure-url:登录(认证)失败跳转路径
            username-parameter:form表单登录时,input的name属性的值<input name="username"/>,username是默认的,可以自定义
            password-parameter:form表单登录时,input的name属性的值<input name="password"/>,password是默认的,可以自定义
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"
                             username-parameter="username"
                             password-parameter="password" />
        <!--配置退出登录信息
            logout-url:配置登出操作的请求路径
            logout-success-url:登出成功后请求的路径,这里设置成登出后跳转到登录页面
        -->
        <security:logout logout-url="/logout"
                         logout-success-url="/login.jsp"/>

        <!--去掉csrf拦截的过滤器,直接去掉的这种方式不建议使用,因为可能存在CSRF攻击-->
        <!--<security:csrf disabled="true"/>-->

    </security:http>

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

</beans>

主要修改的是<security:authentication-manager>,其他不变。

5.3 加密认证

目前我们的数据库中,保存的密码是明文的,这样显然是不安全的。所以应该把用户的密码加密之后才能保存到数据库,同时在认证的时候,也应该是验证这个加密之后的密码。

为此Spring Security给我们提供了一种加密方式:BCryptPasswordEncoder,它的特点是:同样的密码,比如123,每次加密得到的密文是不同的。对于密码的验证,它提供了一个matches方法,该方法有两个参数,参数一是密码的明文,参数二是加密之后的密码。方法返回一个boolean值。

它和MD5加密不同,MD5加密123得到的密文是相同的。因此现在的用法是加盐加密。而BCryptPasswordEncoder也是采用了加盐加密方式,但是它是动态加盐,每次加的盐都不同,所以每次的密文不同。

package org.springframework.security.crypto.bcrypt;

import java.security.SecureRandom;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.password.PasswordEncoder;

public class BCryptPasswordEncoder implements PasswordEncoder {
    private Pattern BCRYPT_PATTERN;
    private final Log logger;
    private final int strength;
    private final SecureRandom random;

    public BCryptPasswordEncoder() {
        this(-1);
    }

    public BCryptPasswordEncoder(int strength) {
        this(strength, (SecureRandom)null);
    }

    public BCryptPasswordEncoder(int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
        this.logger = LogFactory.getLog(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.strength = strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }

    //密码加密的方法,参数是明文密码,返回值是加密之后的密码
    public String encode(CharSequence rawPassword) {
        String salt;
        if (this.strength > 0) {
            if (this.random != null) {
                salt = BCrypt.gensalt(this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.strength);
            }
        } else {
            salt = BCrypt.gensalt();
        }

        return BCrypt.hashpw(rawPassword.toString(), salt);
    }

    //验证密码的方法,参数一是明文密码,参数二是加密之后的密码,返回值是是否验证成功
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }
}

5.3.1 在IOC容器中提供加密对象

修改spring-security.xml文件

<!--把加密对象放入的IOC容器中-->
    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

    <!--设置Spring Security认证用户信息的来源-->
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userServiceImpl">
            <!--如果BCryptPasswordEncoder的id属性是passwordEncoder,那么这句可以不加,不过BCryptPasswordEncoder的配置要在这个的前面,但是建议都加上-->
            <security:password-encoder ref="passwordEncoder"/>
        </security:authentication-provider>
    </security:authentication-manager>

5.3.2 修改上面的loadUserByUsername方法

修改认证方法loadUserByUsername ,去掉{noop}

UserDetails userDetails = new User(username,sysUser.getPassword(),authorities);
return userDetails;

5.3.3 修改添加用户的方法

添加用户的时候,应该把密码加密:

@Autowired
private BCryptPasswordEncoder passwordEncoder;

@RequestMapping("/save")
public String save(SysUser user){
    //加密密码
    String encodePassword = passwordEncoder.encode(user.getPassword());
    user.setPassword(encodePassword);
    userService.save(user);
    return "redirect:findAll";
}

5.3.4 测试

改完之后,重启项目,输入xiaoming/123456 你会发现登录失败了。这是为啥呢?

因为现在是开启的加密认证,Spring Security会认为你的数据库密码是加密的,但是现在我们存的是明文,所以导致了认证失败。

为了测试,我们需要手动把123456加密,然后手动修改数据库:

public class PasswordTest {
    public static void main(String[] args) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode("123456");
        System.out.println(encode);
    }
}

然后手动修改数据库用户的密码

再次登录的话,应该就是正常的了。
sword");
return false;
}
}
}


### 5.3.1 在IOC容器中提供加密对象

修改spring-security.xml文件

```xml
<!--把加密对象放入的IOC容器中-->
    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

    <!--设置Spring Security认证用户信息的来源-->
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userServiceImpl">
            <!--如果BCryptPasswordEncoder的id属性是passwordEncoder,那么这句可以不加,不过BCryptPasswordEncoder的配置要在这个的前面,但是建议都加上-->
            <security:password-encoder ref="passwordEncoder"/>
        </security:authentication-provider>
    </security:authentication-manager>

5.3.2 修改上面的loadUserByUsername方法

修改认证方法loadUserByUsername ,去掉{noop}

UserDetails userDetails = new User(username,sysUser.getPassword(),authorities);
return userDetails;

5.3.3 修改添加用户的方法

添加用户的时候,应该把密码加密:

@Autowired
private BCryptPasswordEncoder passwordEncoder;

@RequestMapping("/save")
public String save(SysUser user){
    //加密密码
    String encodePassword = passwordEncoder.encode(user.getPassword());
    user.setPassword(encodePassword);
    userService.save(user);
    return "redirect:findAll";
}

5.3.4 测试

改完之后,重启项目,输入xiaoming/123456 你会发现登录失败了。这是为啥呢?

因为现在是开启的加密认证,Spring Security会认为你的数据库密码是加密的,但是现在我们存的是明文,所以导致了认证失败。

为了测试,我们需要手动把123456加密,然后手动修改数据库:

public class PasswordTest {
    public static void main(String[] args) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode("123456");
        System.out.println(encode);
    }
}

然后手动修改数据库用户的密码

再次登录的话,应该就是正常的了。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值