转载

SpringSecurity(一)

目标:初步掌握Spring Security的认证功能实现。

案例介绍

  • 项目的地址是: 项目初始化
  • 项目的数据库脚本在src/main/resources目的下的security.sql。
  • 开发工具使用的是IntelliJ IDEA。
  • JDK版本是1.8。
  • MySQL的版本是5.7。
  • 后端的技术是SSM(SpringMVC、Spring、Mybatis)。
  • 前端的技术是AdminLTE2。访问地址: http://localhost :8080/login.jsp。

SpringSecurity(一)

初识权限管理

权限管理概念

  • 权限管理,一般是指根据系统设置的安全规则或安全策略,用户可以访问而且只能访问自己被授予的权限。权限管理几乎出现在任何系统里面,前提是需要用户认证的系统。

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

认证:通过用户名和密码(当然也可以是其它方式,比如邮箱、身份证等)成功登录系统后,让系统得到当前用户的角色身份。

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

完全权限管理需要三个对象

  • 用户:主要包含用户名、密码等,可以实现认证操作。一般而言,在系统中给用户分配角色。
  • 角色:角色是权限的集合。一般而言,在系统中给角色分配权限。
  • 权限:权限也可以称为资源,包括地址、权限名称等。

一般而言,用户可以分配多个角色,角色可以分配多个权限。所以,在权限设计表的时候,一般设计5张表,分别为用户表、角色表、权限表、用户角色表、角色权限表。业内有时也会将这5张表称为经典的RBAC权限设计模型。

初识Spring Security

Spring Security概念

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

Spring Security简单入门

创建web工程并导入相应的jar包

<!-- 
    spring-security-config是用来解析XML配置文件
 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<!--
    spring-security-core是Spring Security的核心jar包,任何Spring Security都需要用此jar包
 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<!-- 
    spring-security-taglibs是Spring Security提供的动态标签库,JSP页面中可以使用。
 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<!--
    spring-security-web是web工程的必备jar包,包含过滤器和相关的web安全基础结构代码
 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>

配置web.xml

<!-- 配置Spring Security的核心过滤器链 -->
<!--  
    filter-name必须是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>

配置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">

    <!--
        配置Spring Security
        auto-config="true"表示自定加载spring-security.xml配置文件
        use-expressions="true"表示使用spring的el表达式来配置spring security
    -->
    <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认证用户信息的来源-->
    <!--
        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>

将spring-security.xml配置文件引入到applicationContext.xml中

<!--引入SpringSecurity主配置文件-->
<import resource="classpath:spring-security.xml"/>

运行项目

  • 运行项目会出现下面的界面。

SpringSecurity(一)

  • 其源码如下:

SpringSecurity(一)

  • 控制台的日志:
INFO  web.DefaultSecurityFilterChain  - Creating filter chain: any request, [org.springframework.security.web.context.SecurityContextPersistenceFilter@17455fed, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@97f4fa3, org.springframework.security.web.header.HeaderWriterFilter@1384edda, org.springframework.security.web.csrf.CsrfFilter@544ac02d, org.springframework.security.web.authentication.logout.LogoutFilter@694d477e, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@1abc0fa3, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2e62db13, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2c2b3d5a, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2fdef916, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@55e53a59, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6d67c9b5, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@18d3e8ae, org.springframework.security.web.session.SessionManagementFilter@409db4eb, org.springframework.security.web.access.ExceptionTranslationFilter@3cd67953, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6faf351d]
  • 最后,我们在此登录页面输入用户名为user,密码为user,即可登录。

SpringSecurity(一)

Spring Security常用的过滤器链

Spring Security常用过滤器介绍

  • org.springframework.security.web.context.SecurityContextPersistenceFilter

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

  • org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

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

  • org.springframework.security.web.header.HeaderWriterFilter

向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制。

  • org.springframework.security.web.csrf.CsrfFilter

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

  • org.springframework.security.web.authentication.logout.LogoutFilter

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

  • org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

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

  • org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

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

  • org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

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

  • org.springframework.security.web.authentication.www.BasicAuthenticationFilter

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

  • org.springframework.security.web.savedrequest.RequestCacheAwareFilter

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

  • org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

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

  • org.springframework.security.web.authentication.AnonymousAuthenticationFilter

当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中,SpringSecurity为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名身份。

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

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

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

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

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

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

Spring Security过滤器链加载原理

DelegatingFilterProxy

  • 我们在web.xml中配置了一个名称为springSecurityFilterChain的过滤器DelegatingFilterProxy,接下来我们看其中的重要源码即可。
public class DelegatingFilterProxy extends GenericFilterBean {
    @Nullable
    private String contextAttribute;
    @Nullable
    private WebApplicationContext webApplicationContext;
    @Nullable
    private String targetBeanName;
    private boolean targetFilterLifecycle = false;
    @Nullable
    private volatile Filter delegate; //注意:这个过滤器才是真正加载的过滤器
    private final Object delegateMonitor = new Object();

    //注意:doFilter是过滤器的入口,直接从这边看。
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: " +
                                "no ContextLoaderListener or DispatcherServlet registered?");
                    }
                    //第一步:doFilter中最重要的一步,初始化上面私有过滤器属性delegate
                    delegateToUse = initDelegate(wac);
                }
                this.delegate = delegateToUse;
            }
        }
        //第三步:执行FilterChainProxy过滤器
        invokeDelegate(delegateToUse, request, response, filterChain);
    }
    //第二步:直接看最终加载的过滤器是谁
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        //debug得知targetBeanName为springSecurityFilterChain
        String targetBeanName = getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        //debug得知Filter对象为FilterChainProxy
        Filter delegate = wac.getBean(targetBeanName, Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }

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

        delegate.doFilter(request, response, filterChain);
    }

}
  • 第二步debug的结果如下图所示:

SpringSecurity(一)

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

FilterChainProxy

public class FilterChainProxy extends GenericFilterBean {
    private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
    private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
    private List<SecurityFilterChain> filterChains;
    private FilterChainProxy.FilterChainValidator filterChainValidator;
    private HttpFirewall firewall;

    //可以通过一个叫SecurityFilterChain的对象实例化一个FilterChainProxy对象,可能SecurityFilterChain才是真正的过滤器对象。
    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);
                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);
        }
    }

    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的结果如下图所示:

SpringSecurity(一)

SecurityFilterChain

  • SecurityFilterChain是一个接口,实现类也只有一个,这才是web.xml配置的过滤器链对象。
//接口
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 + "]";
    }
}

SpringSecurity使用自定义认证页面

在SpringSecurity的主配置文件中指定认证页面配置信息

<?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过滤器的静态资源-->
    <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"/>
    <security:http pattern="/favicon.ico" security="none"/>

    <!--
        配置Spring Security
        auto-config="true"表示自定加载spring-security.xml配置文件
        use-expressions="true"表示使用spring的el表达式来配置spring security
    -->
    <security:http auto-config="true" use-expressions="true">

        <!--指定login.jsp页面可以被匿名访问-->
        <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 处理登录的处理器的地址
            default-target-url 登录成功跳转的地址
            authentication-failure-url 登录失败跳转的地址

            默认的用户名是username,密码是password,当然也可以使用username-parameter和password-parameter修改。
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"/>
        <!--
            指定退出登录后跳转的页面
            logout-url 处理退出登录的处理器地址
            logout-success-url 退出登录成功跳转的地址
        -->
        <security:logout logout-url="/logout" logout-success-url="/login.jsp"/>
    </security:http>

    <!--设置Spring Security认证用户信息的来源-->
    <!--
        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>
  • 修改认证页面的请求地址:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>数据 - AdminLTE2定制版 | Log in</title>

<meta
    content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
    name="viewport">

<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css">
</head>

<body class="hold-transition login-page">
    <div class="login-box">
        <div class="login-logo">
            <a href="#"><b></b>后台管理系统</a>
        </div>
        <!-- /.login-logo -->
        <div class="login-box-body">
            <p class="login-box-msg">登录系统</p>
            <!--
                action的地址必须是/login
                method 必须是post
            -->
            <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>

            <a href="#">忘记密码</a><br>


        </div>
        <!-- /.login-box-body -->
    </div>
    <!-- /.login-box -->

    <!-- jQuery 2.2.3 -->
    <!-- Bootstrap 3.3.6 -->
    <!-- iCheck -->
    <script
        src="${pageContext.request.contextPath}/plugins/jQuery/jquery-2.2.3.min.js"></script>
    <script
        src="${pageContext.request.contextPath}/plugins/bootstrap/js/bootstrap.min.js"></script>
    <script
        src="${pageContext.request.contextPath}/plugins/iCheck/icheck.min.js"></script>
    <script>
        $(function() {
            $('input').iCheck({
                checkboxClass : 'icheckbox_square-blue',
                radioClass : 'iradio_square-blue',
                increaseArea : '20%' // optional
            });
        });
    </script>
</body>

</html>
  • 再次启动项目就可以看到自定义的登录页面了。

SpringSecurity(一)

  • 但是当我们输入用户名为user,密码为user的时候,却会出现如下的页面:

SpringSecurity(一)

  • 403在Spring Security中是权限不足?为什么?Spring Security内置的认证页面源代码中有_csrf隐藏input,问题就在这里,而且后台日志是这样的。

SpringSecurity(一)

Spring Security的csrf防护机制

Spring Security中CsrfFilter过滤器的说明

public final class CsrfFilter extends OncePerRequestFilter {
    public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
    private final Log logger = LogFactory.getLog(this.getClass());
    private final CsrfTokenRepository tokenRepository;
    private RequestMatcher requireCsrfProtectionMatcher;
    private AccessDeniedHandler accessDeniedHandler;

    public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
        this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
        this.accessDeniedHandler = new AccessDeniedHandlerImpl();
        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
        this.tokenRepository = csrfTokenRepository;
    }
    //从这里可以看出Spring Security的csrf机制把请求方式分为两类
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }

        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
        //第一类:GET、HEAD、TRACE、OPTIONS四类请求可以直接通过
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
        } else {
            //第二类:除去上面的四种方式,包括POST、DELETE、PUT等都需要携带token才能通过
            String actualToken = request.getHeader(csrfToken.getHeaderName());
            if (actualToken == null) {
                actualToken = request.getParameter(csrfToken.getParameterName());
            }

            if (!csrfToken.getToken().equals(actualToken)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
                }

                if (missingToken) {
                    this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
                } else {
                    this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
                }

            } else {
                filterChain.doFilter(request, response);
            }
        }
    }

    public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
        Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
        this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
    }

    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
        this.accessDeniedHandler = accessDeniedHandler;
    }

    private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
        private final HashSet<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());
        }
    }
}
  • 通过源码,我们知道,我们自己的登录页面的请求方式是POST,但是却没有携带token,所以才会出现403权限不足的异常,那么如何处理?

    • ①直接禁用csrf,不推荐。
    • ②在认证页面携带token请求。

禁用csrf防护机制

  • 在Spring Security的主配置文件中添加禁用csrf防护的机制。
<?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过滤器的静态资源-->
    <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"/>
    <security:http pattern="/favicon.ico" security="none"/>

    <!--
        配置Spring Security
        auto-config="true"表示自定加载spring-security.xml配置文件
        use-expressions="true"表示使用spring的el表达式来配置spring security
    -->
    <security:http auto-config="true" use-expressions="true">

        <!--指定login.jsp页面可以被匿名访问-->
        <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 处理登录的处理器的地址
            default-target-url 登录成功跳转的地址
            authentication-failure-url 登录失败跳转的地址

            默认的用户名是username,密码是password,当然也可以使用username-parameter和password-parameter修改。
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"/>
        <!--
            指定退出登录后跳转的页面
            logout-url 处理退出登录的处理器的地址
            logout-success-url 退出登录成功跳转的地址
        -->
        <security:logout logout-url="/logout" logout-success-url="/login.jsp"/>

        <!-- 禁用csrf防护机制 -->
        <security:csrf disabled="true"/>
    </security:http>

    <!--设置Spring Security认证用户信息的来源-->
    <!--
        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>

在认证页面携带token请求

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%-- 添加标签库 --%>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>数据 - AdminLTE2定制版 | Log in</title>

<meta
    content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
    name="viewport">

<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css">
</head>

<body class="hold-transition login-page">
    <div class="login-box">
        <div class="login-logo">
            <a href="#"><b></b>后台管理系统</a>
        </div>
        <!-- /.login-logo -->
        <div class="login-box-body">
            <p class="login-box-msg">登录系统</p>
            <form action="${pageContext.request.contextPath}/login" method="post">
                <%-- 在认证页面携带token --%>
                <security:csrfInput/>

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

            <a href="#">忘记密码</a><br>


        </div>
        <!-- /.login-box-body -->
    </div>
    <!-- /.login-box -->

    <!-- jQuery 2.2.3 -->
    <!-- Bootstrap 3.3.6 -->
    <!-- iCheck -->
    <script
        src="${pageContext.request.contextPath}/plugins/jQuery/jquery-2.2.3.min.js"></script>
    <script
        src="${pageContext.request.contextPath}/plugins/bootstrap/js/bootstrap.min.js"></script>
    <script
        src="${pageContext.request.contextPath}/plugins/iCheck/icheck.min.js"></script>
    <script>
        $(function() {
            $('input').iCheck({
                checkboxClass : 'icheckbox_square-blue',
                radioClass : 'iradio_square-blue',
                increaseArea : '20%' // optional
            });
        });
    </script>
</body>

</html>

注销

  • 需要将header.jsp中的注销功能,改为form表单提交,并且提交的方式是POST提交,而且在表单携带token请求。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<!-- 页面头部 -->
<header class="main-header">
    <!-- Logo -->
    <a href="${pageContext.request.contextPath}/pages/main.jsp" class="logo">
        <!-- mini logo for sidebar mini 50x50 pixels -->
        <span class="logo-mini"><b>数据</b></span> <!-- logo for regular state and mobile devices -->
        <span class="logo-lg"><b>数据</b>后台管理</span>
    </a>
    <!-- Header Navbar: style can be found in header.less -->
    <nav class="navbar navbar-static-top">
        <!-- Sidebar toggle button-->
        <a href="#" class="sidebar-toggle" data-toggle="offcanvas"
           role="button"> <span class="sr-only">Toggle navigation</span>
        </a>

        <div class="navbar-custom-menu">
            <ul class="nav navbar-nav">

                <li class="dropdown user user-menu"><a href="#"
                                                       class="dropdown-toggle" data-toggle="dropdown"> <img
                        src="${pageContext.request.contextPath}/img/user2-160x160.jpg"
                        class="user-image" alt="User Image">
                    <span class="hidden-xs">
                            <%--<security:authentication property="principal.username" />--%>
                            <%--<security:authentication property="name" />--%>
                    </span>
                </a>
                    <ul class="dropdown-menu">
                        <!-- User image -->
                        <li class="user-header"><img
                                src="${pageContext.request.contextPath}/img/user2-160x160.jpg"
                                class="img-circle" alt="User Image"></li>

                        <!-- Menu Footer-->
                        <li class="user-footer">
                            <div class="pull-left">
                                <a href="#" class="btn btn-default btn-flat">修改密码</a>
                            </div>
                            <div class="pull-right">
                                <%-- 将原来的注销注释,使用form表单的形式提交,在表单携带token请求 --%>
                                <%--                                <a href="${pageContext.request.contextPath}/login.jsp"--%>
                                <%--                                   class="btn btn-default btn-flat">注销</a>--%>
                                <form action="${pageContext.request.contextPath}/logout" method="post">
                                    <security:csrfInput/>
                                    <input type="submit" class="btn btn-default btn-flat">注销</input>
                                </form>
                            </div>
                        </li>
                    </ul>
                </li>
            </ul>
        </div>
    </nav>
</header>
<!-- 页面头部 /-->

Spring Security使用数据库完成认证

认证流程分析

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter是用来负责认证的过滤器。

package org.springframework.security.web.authentication;

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    //视图认证的方法
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //必须为POST请求,否则会抛出异常
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            //将填写的用户名和密码封装到UsernamePasswordAuthenticationToken对象中
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            //调用AuthenticationManager对应进行认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

AuthenticationManager

  • 由上面的源码可知,真正的认证操作在AuthenticationManager里面。但是AuthenticationManager是接口,其子类是ProviderManager。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    private static final Log logger = LogFactory.getLog(ProviderManager.class);
    private AuthenticationEventPublisher eventPublisher;
    private List<AuthenticationProvider> providers;
    protected MessageSourceAccessor messages;
    private AuthenticationManager parent;
    private boolean eraseCredentialsAfterAuthentication;
    //注意AuthenticationProvider,Spring Security针对每一种认证,什么QQ登录,微信登录都封装到一个AuthenticationProvider对象中
    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, (AuthenticationManager)null);
    }

    public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
        this.eventPublisher = new ProviderManager.NullEventPublisher();
        this.providers = Collections.emptyList();
        this.messages = SpringSecurityMessageSource.getAccessor();
        this.eraseCredentialsAfterAuthentication = true;
        Assert.notNull(providers, "providers list cannot be null");
        this.providers = providers;
        this.parent = parent;
        this.checkState();
    }

    public void afterPropertiesSet() throws Exception {
        this.checkState();
    }

    private void checkState() {
        if (this.parent == null && this.providers.isEmpty()) {
            throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required");
        }
    }
    //认证的方法
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var8 = this.getProviders().iterator();
        //循环遍历所有的AuthenticationProvider,匹配当前认证类型
        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    //找到对应的认证类型继续调用AuthenticationProvider对象完成认证业务
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (InternalAuthenticationServiceException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = parentResult = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var11) {
            } catch (AuthenticationException var12) {
                parentException = var12;
                lastException = var12;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }

    private void prepareException(AuthenticationException ex, Authentication auth) {
        this.eventPublisher.publishAuthenticationFailure(ex, auth);
    }

    private void copyDetails(Authentication source, Authentication dest) {
        if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) {
            AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;
            token.setDetails(source.getDetails());
        }

    }

    public List<AuthenticationProvider> getProviders() {
        return this.providers;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
        Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
        this.eventPublisher = eventPublisher;
    }

    public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
        this.eraseCredentialsAfterAuthentication = eraseSecretData;
    }

    public boolean isEraseCredentialsAfterAuthentication() {
        return this.eraseCredentialsAfterAuthentication;
    }

    private static final class NullEventPublisher implements AuthenticationEventPublisher {
        private NullEventPublisher() {
        }

        public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
        }

        public void publishAuthenticationSuccess(Authentication authentication) {
        }
    }
}

AuthenticationProvider

  • AuthenticationProvider是接口,其子类是AbstractUserDetailsAuthenticationProvider。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    protected final Log logger = LogFactory.getLog(this.getClass());
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private UserCache userCache = new NullUserCache();
    private boolean forcePrincipalAsString = false;
    protected boolean hideUserNotFoundExceptions = true;
    private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    public AbstractUserDetailsAuthenticationProvider() {
    }

    protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;

    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        this.doAfterPropertiesSet();
    }
    //认证的方法
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                //获取UserDetails对象,即SpringSecurity自己的用户对象
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

       //这是个抽象方法,由子类实现
    protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException; 
}
  • DaoAuthenticationProvider是AbstractUserDetailsAuthenticationProvider的子类,有对应retrieveUser方法的实现。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    private PasswordEncoder passwordEncoder;
    private volatile String userNotFoundEncodedPassword;
    private UserDetailsService userDetailsService;
    private UserDetailsPasswordService userDetailsPasswordService;

    public DaoAuthenticationProvider() {
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    ////获取UserDetails对象,即SpringSecurity自己的用户对象
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            //UserDetails对象,即SpringSecurity自己的用户对象
            //loadUserByUsername是真正的认证逻辑,即我们可以直接编写一个UserDetailsService()的实现呢类,告诉SpringSecurity就可以了。
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }  
}

初步实现自己的认证功能

我们自己的UserService接口继承UserDetailService。

package com.weiwei.xu.service;

import com.weiwei.xu.domain.SysUser;
import org.springframework.security.core.userdetails.UserDetailsService;

import java.util.List;
import java.util.Map;

public interface UserService extends UserDetailsService {

    public void save(SysUser user);

    public List<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 (null == sysUser) {
            //如果用户名不对,直接返回null,表示认证失败
            return null;
        }

        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        List<SysRole> roles = sysUser.getRoles();

        if (null != roles && roles.size() != 0) {
            roles.forEach(role -> {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
                authorities.add(simpleGrantedAuthority);
            });
        }

        //返回UserDetails对象,"{noop}"+密码表示不加密认证
        UserDetails userDetails = new User(sysUser.getUsername(), "{noop}" + sysUser.getPassword(), authorities);


        return userDetails;
    }
}

修改SpringSecurity的主配置文件

<?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过滤器的静态资源-->
    <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"/>
    <security:http pattern="/favicon.ico" security="none"/>

    <!--
        配置Spring Security
        auto-config="true"表示自定加载spring-security.xml配置文件
        use-expressions="true"表示使用spring的el表达式来配置spring security
    -->
    <security:http auto-config="true" use-expressions="true">

        <!--指定login.jsp页面可以被匿名访问-->
        <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 处理登录的处理器的地址
            default-target-url 登录成功跳转的地址
            authentication-failure-url 登录失败跳转的地址

            默认的用户名是username,密码是password,当然也可以使用username-parameter和password-parameter修改。
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"/>
        <!--
            指定退出登录后跳转的页面
            logout-url 处理退出登录的处理器的地址
            logout-success-url 退出登录成功跳转的地址
        -->
        <security:logout logout-url="/logout" logout-success-url="/login.jsp"/>

        <!-- 禁用csrf防护机制 -->
<!--        <security:csrf disabled="true"/>-->
    </security:http>

    <!--设置Spring Security认证用户信息的来源-->
    <!--
        Spring Security的认证必须是加密的,{noop}表示不加密认证
    -->
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userServiceImpl">
           
        </security:authentication-provider>
    </security:authentication-manager>
</beans>

加密认证

在IOC容器中添加加密对象

  • 在SpringSecurity的主配置文件中添加加密对象。
<!-- 加密对象 -->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>

<!--设置Spring Security认证用户信息的来源-->
<!--
    Spring Security的认证必须是加密的,{noop}表示不加密认证
-->
<security:authentication-manager>
    <security:authentication-provider user-service-ref="userServiceImpl">
        <!--    指定认证使用的加密对象        -->
        <security:password-encoder ref="passwordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>

修改认证方法

  • 去掉nohup
@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        SysUser sysUser = userDao.findByName(username);

        if (null == sysUser) {
            //如果用户名不对,直接返回null,表示认证失败
            return null;
        }

        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        List<SysRole> roles = sysUser.getRoles();

        if (null != roles && roles.size() != 0) {
            roles.forEach(role -> {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
                authorities.add(simpleGrantedAuthority);
            });
        }

        //返回UserDetails对象,"{noop}"+密码表示不加密认证
        UserDetails userDetails = new User(sysUser.getUsername(), sysUser.getPassword(), authorities);


        return userDetails;
    }

修改添加用户的方法

@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;

@Override
public void save(SysUser user) {
    user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
    userDao.save(user);
}

手动修改数据库中用户对应的密码

  • 将xiaoming账号对应的密码改为
$2a$10$ynlaufZM048G5jsp98seeuvkAXNCVD5RFEudlrW.xiNihU.2Tjm9W

SpringSecurity(一)

原文  https://segmentfault.com/a/1190000022662831
正文到此结束
Loading...