SpringSecurity

  • Post author:
  • Post category:其他


SpringSecurity是Spring提供用于认证与授权检测处理的框架,利用它可以方便的实现登录认证与授权控制管理。

项目源码:

https://gitee.com/tirklee/lea-springsecurity.git

SpringSecurity是最早提供登录认证与授权检测的框架。当下最流行的是Shiro开发框架(即SSM开发框架Spring+Shiro+MyBatis组成),虽然SpringSecurity已经很少出现在Web项目项目中,但微服务架构(SpringCloud)的流行,使得很多项目使用了SpringSecurity。



SpringSecurity概述

SpringSecurity由Acegi Security开发框架演变而来,是一套完整的Web安全解决方案,给予SpringAOP与过滤器实现安全访问控制。主要有两大部分用户认证与用户授权。

  • 用户认证(Authentication):判断某个用户是否是系统的合法操作体,是否具有系统的操作权力。在进行用户认证处理中,核心信息为用户名与密码。
  • 用户授权(Authorization):一个系统中不同的用户拥有不同的权限(或称为角色),利用权限可以实现不同级别用户的划分,从而保证系统操作的安全。

为了实现安全管理,SpringSecurity提供了一系列访问过滤器。所有访问过滤器都围绕着认证管理和决策管理展开。认证管理由SpringSecurity负责,开发者只需要掌握UserDetailsService(用户认证服务)、AccessDecisionVoter(决策管理器)两个核心接口即可。
在这里插入图片描述
SpringSecurity中除了提供认证与授权外,还提供了Session管理、RememberMe(记住我)等常见功能。



SpringSecurity编程起步

1.【lea-springsecurity项目】创建一个MessageAction程序类,进行信息显示。

package com.xiyue.leaspring.action;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller//定义控制器
@RequestMapping("/pages/info")//定义访问父路径,与方法中的路径组合为完整的路径
public class MessageAction {//自定义Action程序
    @RequestMapping("/url")//访问的路径为url
    @ResponseBody
    public Object url(){
        return "www.xiyue.com";
    }
}

利用SpringSecurity组件利用Web过滤器对指定的请求拦截路径访问进行检测,检测通过后,可正常访问相应服务;如果检测失败,会显示相应的错误信息。

在这里插入图片描述

2.【lea-springsecurity项目】修改pom.xml配置文件,追加SpringSecurity相关依赖库管理。

<spring.security.version>5.4.5</spring.security.version>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>${spring.security.version}</version>
</dependency>

3.【lea-springsecurity项目】修改web.xml配置文件,追加过滤器配置。

 <!--SpringSecurity过滤器配置-->
 <filter-mapping>
     <filter-name>springSecurityFilterChain</filter-name>
     <url-pattern>/*</url-pattern>
 </filter-mapping>
 <!--配置编码过滤器,已解决数据传输乱码问题-->
 <filter-mapping><!--所有路径都必须经过此过滤器-->
     <filter-name>encoding</filter-name>
     <url-pattern>/*</url-pattern>
 </filter-mapping>

/*表示所有的请求都需要SpringSecurity进行检测。

4.【lea-springsecurity项目】在spring.xml中配置定义认证路径以及用户信息。

<security:http auto-config="true"><!--启用HTTP安全认证,并采用自动配置模式-->
        <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
        <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
    </security:http>
    <security:authentication-manager><!--定义认证管理器-->
        <security:authentication-provider><!--配置认证管理配置类-->
            <security:user-service><!--创建用户信息-->
                <!--定义用户名、密码(使用BCryptPasswordEncode加密器进行加密)、角色信息(必须追加ROLE_前缀,否则无法识别)-->
                <!--用户名:admin,密码:hello,角色:ADMIN、USER-->
                <security:user name="admin" authorities="ROLE_ADMIN,ROLE_USER"
                    password="{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16"/>
                <!--用户名:xiyue,密码:java,角色:USER-->
                <security:user name="xiyue" authorities="ROLE_USER"
                    password="{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>

spring.xml文件主要配置了要使用的用户认证与授权信息,且采用了自动配置模式(<security:http auto-config=“true”>),所以并没有具体的登录页面,而是登录时自动为用户提供内置登录页。

提问:关于密码定义的一些问题。进行用户信息配置时,密码使用“{bcrypt}$2a$10$2…”形式进行了定义。这是什么含义?是如何生成的?回答:bcrypt是Spring提供的一种加密算法。为了用户认证信息安全,密码访问时,SpringSecurity使用标准接口org.springframework.security.crypto.password.PasswordEncoder定义加密处理标准。此接口及其常用子类如图所示。

在这里插入图片描述

新版SpringSecurity推荐的加密算法为bcrypt,所以这里定义密码时使用了{bcrypt}标记。为了方便管理不同的加密器,SpringSecurity还提供了PasswordEncoderFactories工厂类,该类中注册了所有的加密器(有一些加密器已经不建议使用,如{noop}、{ldap}、{MD5}等)。如果想定义自己的密码,可利用如下程序完成。

package com.xiyue.leaspring.test;


import org.junit.Test;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

public class TestPasswordEncoder {

    @Test
    public void testPassWord(){
        String password = "java";//定义明文密码
        //默认采用BCryptPasswordEncode加密处理器
        PasswordEncoder passwordEncoder = PasswordEncoderFactories
                .createDelegatingPasswordEncoder();//获取加密器实例
        String encode = passwordEncoder.encode(password);//加密
        System.out.println("加密后的密码:"+encode);//加密密码
        System.out.println("密码比较:"+passwordEncoder.matches(password,encode));
    }
}

在SpringSecurity配置文件里,最重要的是拦截路径与授权信息检测表达式(access属性,该属性为boolean类型)。对于授权检测,常见配置如表所示。

在这里插入图片描述

5.【lea-springsecurity项目】程序配置完成后可以直接启动Web容器,当访问到/pages/info/url程序路径时会自动跳转到/login路径进行登录,如图所示。由于此时该路径访问需要具有ADMIN角色的用户才可以访问,所以输入admin/hello账户信息,随后将显示如图

在这里插入图片描述

在这里插入图片描述

提示:登录注销路径。 在默认情况下,SpringSecurity会提供登录表单,并且会自动将表单的提交路径设置为/login,如果登录后需要注销,可以使用/login?logout路径进行注销。

8.【lea-springsecurity项目】如果用户觉得利用表单登录不太方便,也可以采用http-basic模式实现登录控制,只需要修改spring.xml配置文件即可。

 <security:http auto-config="true"><!--启用HTTP安全认证,并采用自动配置模式-->
        <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
        <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:http-basic/><!--采用http-basic模式登录 -->
    </security:http>

由于采用了http-basic模式进行登录控制,当用户需要进行认证处理时将不会跳转到登录表单,会出现如图所示的弹出界面,在相应位置上输入用户名与密码即可正常访问。

在这里插入图片描述



CSRF访问控制

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的网络攻击模式,攻击者可以在受害者完全不知情的情况下,以受害者身份发送各种请求(如邮件处理、账号操作等),且服务器认为这些操作属于合法访问。CSRF攻击的基本操作流程如图所示。

在这里插入图片描述
用户访问服务器A时,会在客户端浏览器中记录相应的Cookie信息,利用此Cookie进行用户身份的标注。在访问服务器B时,被植入了恶意程序代码,因此这些代码会在用户不知情的情况下以服务器A认证的身份访问其中的数据。

在实际开发中,有3种形式可以解决CSRF漏洞:**验证HTTP请求头信息中的Referer信息,在访问路径中追加token标记,以及在HTTP信息头中定义验证属性。**在SpringSecurity中可以采用token的形式进行验证,下面通过程序演示CSRF攻击的防范操作,本程序所采用的访问流程如图所示;

在这里插入图片描述

1.【lea-springsecurity项目】创建EchoAction程序类,主要负责信息输入页面的跳转与内容回显处理。

package com.xiyue.leaspring.action;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller//定义控制器
@RequestMapping("/pages/message/")//定义访问父路径,与方法中的路径组合为完整的路径
public class EchoAction {//自定义Action程序

    @RequestMapping("/show")//访问的路径为url
    public ModelAndView echo(String msg){
        return new ModelAndView("message/message_show").addObject("echoMessage","[ECHO]msg="+msg);
    }

    @GetMapping("/input")//访问的路径
    public String input(){
        return "message/message_input";//jump route
    }
}

2.【lea-springsecurity项目】修改spring-security.xml配置文件,追加请求拦截路径。

 <security:http auto-config="true"><!--启用HTTP安全认证,并采用自动配置模式-->
        <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
        <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <security:http-basic/><!--采用http-basic模式登录 -->
    </security:http>

3.【lea-springsecurity项目】定义/WEB-INF/pages/message/message_input.jsp页面,在表单定义时传送CSRF-Token信息。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page isELIgnored="false" %><%--启动EL表达式解析--%>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
        +request.getContextPath();
    String message_input_url = basePath+"/pages/message/show";
%>
<base href="<%=basePath%>">
<form action="<%=message_input_url%>" method="post">
消息内容:<input type="text" name="msg" value="www.nieyi.com">
    <%--传递CSRF-Token信息,参数名称_csrf,参数内容为随机生成的Token数据--%>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
    <input type="submit" value="发送"><input type="reset" value="重置">
</form>

在进行表单定义时,利用隐藏域实现了CSRF-Token信息的定义。下图演示了生成后的token内容。

在这里插入图片描述

4.【lea-springsecurity项目】定义/WEB-INF/pages/message/message_show.jsp页面,回显输入内容。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
            +request.getContextPath();
    String message_input_url = basePath+"/pages/message/show";
%>
<base href="<%=basePath%>">
<h1>ECHO消息显示:${echoMessage}</h1>

至此,程序的基本流程开发完毕。用户进行信息输入时,如果发现没有CSRF-Token信息,将会跳转到security:access-denied-handler元素定义的错误信息显示页面。

5.【lea-springsecurity项目】虽然CSRF-Token可以解决CSRF攻击问题,但如果有些项目不需要处理CSRF漏洞,也可以通过配置的方式关闭CSRF校验。只要直接修改spring-security.xml配置文件中的security:http配置项即可。

 <security:http auto-config="true"><!--启用HTTP安全认证,并采用自动配置模式-->
   <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
    <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
    <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
    <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
    <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
    <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
    <security:http-basic/><!--采用http-basic模式登录 -->
    <security:csrf disabled="true"/><!--关闭CSRF校验-->
</security:http>

此时项目中,即便表单提交时没有CSRF-Token也可以正常访问。



扩展登录和注销功能

在SpringSecurity中用户也可以修改默认的登录与注销操作,自定义相关页面进行显示。

1.【lea-springsecurity项目】创建/WEB-INF/pages/login.jsp用户登录页面,该页面主要提供登录表单。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page isELIgnored="false" %><%--启动EL表达式解析--%>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"
            +request.getServerPort()+request.getContextPath()+"/";
    String login_url = basePath + "xylogin";
%>
<html>
<head>
    <title>登陆</title>
    <base href="<%=basePath%>">
</head>
<body>
<h3>用户登陆请输入用户名与密码</h3>

<form action="<%=login_url%>" method="post">
    用户名:
    <input name="mid" type="text" placeholder="请输入字符串用户名"><br>
    密码:
    <input name="pwd" type="password" placeholder="请输入密码"><br>
    <input type="submit" value="登陆">
</form>
</body>
</html>

自定义登陆路径/sclogin(在spring.xml中配置)。

2.【lea-springsecurity项目】创建/WEB-INF/pages/welcome.jsp页面,作为用户登录成功后的首页。

<%@page pageEncoding="UTF-8"%>
<html>
    <head>
        <%
            request.setCharacterEncoding("UTF-8");
            String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
                +request.getContextPath()+"/";
            String logoutUrl = basePath+"xylogout";
        %>
        <title>SpringSecurity安全框架</title>
        <base href="<%=basePath%>"/>
    </head>
    <body>
        <h2>登录成功,欢迎您回来,也可以选择<a href="<%=logoutUrl%>">注销</a></h2>
        <h3>更多内容请访问<a href="http://www.baidu.com">喜悦</a></h3>    
    </body>
</html>

页面中提供了注销路径/logout(需要在spring.xml中配置)

3.【lea-springsecurity项目】创建/WEB-INF/pages/logout.jsp页面进行注销后的显示页面。

<%@page pageEncoding="UTF-8"%>
<html>
    <head>
        <%
            request.setCharacterEncoding("UTF-8");
            String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
                +request.getContextPath();
        %>
        <title>SpringSecurity安全框架</title>
        <base href="<%=basePath%>"/>
    </head>
    <body>
        <h2>注销成功,欢迎您再来!</h2>
        <h3>更多内容请访问<a href="http://www.baidu.com">喜悦</a></h3>    
    </body>
</html>

4.【lea-springsecurity项目】由于所有JSP页面都保存在WEB-INF目录下,所以为了更方便访问页面,可以定义一个GlobalAction程序类,利用此程序类实现跳转。

package com.xiyue.leaspring.action;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class GlobalAction {

    @RequestMapping("/loginPage")
    public String loginpage(){
        return "login";
    }
    
    @RequestMapping("/welcomePage")
    public String welcome(){
        return "welcome";
    }
    
    @RequestMapping("/logoutPage")
    public String logout(){
        return "logout";
    }
}

5.【lea-springsecurity项目】修改spring.xml配置文件,配置登录与注销。

<security:http auto-config="true"><!--启用HTTP安全认证,并采用自动配置模式-->
        <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
        <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <!--采用http-basic模式登录 -->
        <!--
            <security:http-basic/>
        -->
        <!--登录成功后的首页,需要在用户已经认证后才可以显示-->
        <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
        <security:csrf disabled="true"/><!--关闭CSRF校验-->
        <!--配置表单登录-->
        <security:form-login
            username-parameter="mid"
            password-parameter="pwd"
            authentication-success-forward-url="/welcomePage"
            login-page="/loginPage"
            login-processing-url="/xylogin"
            authentication-failure-url="/loginPage?error=true"
            />
        <security:logout
            logout-url="/xylogout"
            logout-success-url="/logoutPage"
            delete-cookies="JSESSIONID"
        />
    </security:http>

创建完成后,SpringSecurity认证检测失败后会自动跳转到/loginPage.action路径,登录成功后会自动跳转到/welcomePage.action路径,注销时会自动清除对应的Cookie数据,从而实现了自定义登录与注销操作。



获取认证与授权信息用户认证成功后

SpringSecurity会将用户的认证信息与授权信息保存在Session中,而保存的信息类型为org.springframework.security.core.userdetails.User类对象,此类的继承结构如下:

在这里插入图片描述

SpringSecurity中有两个核心接口保存用户信息:

  • GrantedAuthority:保存授权信息。
  • UserDetails:描述用户的详情与用户授权信息。

    在SpringSecurity默认配置下,Spring容器会自动帮助用户创建User类对象,并且将用户对应的认证信息与授权信息保存在User类对象中,要想获取这些信息可以采用下表的方法完成。

    在这里插入图片描述

    1.【lea-springsecurity项目】Action中获取认证与授权信息时,所有认证数据都保存在Authentication接口实例中,所以要先获取Authentication接口对象,然后才可以通过Authentication得到UserDetails接口对象。
  @RequestMapping("/welcomePage")//访问路径
    public String welcome(){//登录成功路径
        Authentication authentication = SecurityContextHolder
                .getContext().getAuthentication();//获取认证对象
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();//用户详情
        String username = userDetails.getUsername();//获得用户名
        this.logger.info("用户名:"+username);
        //通过userDetail对象获取当前用户的所有授权信息
        Collection<? extends GrantedAuthority> authorities =userDetails.getAuthorities();
        this.logger.info("授权信息:"+authorities);
        return "welcome";//设置跳转路径
    }

2.【lea-springsecurity项目】在实际项目中,通常需要在JSP页面中获取相应的认证与授权信息,此时可以直接引入SpringSecurity的标签来获得。

<%@page pageEncoding="UTF-8"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<html>
    <head>
        <%
            request.setCharacterEncoding("UTF-8");
            String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
                +request.getContextPath()+"/";
            String logoutUrl = basePath+"xylogout";
        %>
        <title>SpringSecurity安全框架</title>
        <base href="<%=basePath%>"/>
    </head>
    <body>
        <security:authorize access="isAuthenticated()"><%--是否为认证过的用户--%>
            用户已经登录成功了!
        </security:authorize>
        <security:authorize access="hasRole('USER')"><%--是否拥有USER角色--%>
            拥有USER角色
        </security:authorize>
        <security:authorize access="hasRole('ADMIN')"><%--是否拥有ADMIN角色--%>
            拥有ADMIN角色
        </security:authorize>
        <h2>登录成功,欢迎【<security:authentication property="principal.username"/>】您回来,也可以选择<a href="<%=logoutUrl%>">注销</a></h2>
        <h3>更多内容请访问<a href="http://www.baidu.com">喜悦</a></h3>    
    </body>
</html>

本程序主要使用了两个标签,核心作用如下。

security:authentication:获取认证信息,通过Authentication获取UserDetails,得到用户名。

security:authorize:授权信息,采用Spring表达式进行判断(与拦截路径判断一致)。



基于数据库实现用户登录

为了灵活管理用户的登录信息,在实际项目中需要将用户信息保存在数据库中,登录时利用数据库对认证信息进行检测。SpringSecurity与数据库的认证整合处理。



基于SpringSecurity标准认证

SpringSecurity本身可以直接利用配置文件实现用户认证与授权信息的查询处理,但是需要开发者在项目中配置好相应的数据库连接,同时由于SpringSecurity自身的查询约定,在定义查询语句时也需要对返回查询列的名称统一。

提示:关于本次基于数据库查询的操作。

为了尽可能帮助读者理解SpringSecurity中的自动查询处理支持,在本程序中将使用自定义表结构的形式完成(在查询的时候将为列定义别名以符合SpringSecurity查询要求),同时对于数据库的配置也将使用之前讲解过的Druid连接池。

数据表结构如下:

在这里插入图片描述

1.【lea-springsecurity项目】定义数据库创建脚本,该脚本信息的组成与之前固定认证信息的结构相同。

--删除数据库
drop database lea_springsecurity;
--创建数据库
create database lea_springsecurity default character set utf8;
--使用数据库
use lea_springsecurity;
--创建用户表(mid:登录ID;name:真实姓名;password:登录密码;enabled:启用状态 )
--enabled取值有两种:启用(enabled=1),锁定(enabled=0)
CREATE TABLE member(
    mid varchar(50),
    name varchar(50),
    password varchar(68),
    enabled INT(1),
    CONSTRAINT pk_mid PRIMARY KEY(mid)
) ENGINE = innodb;
--创建角色表(rid:角色ID,也就是检测的名称;title:角色名称)
CREATE TABLE role(
    rid varchar(50),
    title varchar(50),
    CONSTRAINT pk_rid PRIMARY KEY(rid)
) ENGINE = innodb;
--创建用户角色关联表(mid:用户ID;rid:角色ID)
CREATE TABLE member_role(
    mid varchar(50),
    rid varchar(50),
    CONSTRAINT fk_mid FOREIGN KEY (mid) REFERENCES member(mid) ON DELETE CASCADE,
    CONSTRAINT fk_rid FOREIGN KEY (rid) REFERENCES role(rid) ON DELETE CASCADE
) ENGINE = innodb;
--增加用户数据(admin/hello,xiyue/java)
INSERT INTO member(mid,name,password,enabled)values('admin','admin','{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16',1);
INSERT INTO member(mid,name,password,enabled)values('xiyue','xiyue','{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em',0);
--增加角色数据
INSERT INTO role(rid,title)values('ROLE_ADMIN','ADMIN');
INSERT INTO role(rid,title)values('ROLE_USER','USER');
--增加用户与角色信息
INSERT INTO member_role(mid,rid)values('admin','ROLE_ADMIN');
INSERT INTO member_role(mid,rid)values('admin','ROLE_USER');
INSERT INTO member_role(mid,rid)values('xiyue','ROLE_USER');
--提交事务
COMMIT;

2.【lea-springsecurity项目】修改spring.xml配置文件中的security:authentication-provider元素定义,将固定信息验证修改为JDBC的形式。

<security:authentication-manager><!--定义认证管理器-->
        <security:authentication-provider><!--配置认证管理配置类-->
            <security:jdbc-user-service
                    data-source-ref="dataSource"
                    users-by-username-query="select mid as username,password,enabled from member where mid=?"
                    authorities-by-username-query="select mid as username,rid as authorities from member_role where mid=?"/>
        </security:authentication-provider>
    </security:authentication-manager>

本程序主要使用security:jdbc-user-service元素配置数据库认证,属性作用如下。

  • data-source-ref:定义要使用的数据源对象。
  • users-by-username-query:用户认证查询,要求返回用户名(username)、密码(password)、启用状态(enabled),由于数据表中列的名称与查询要求不符,所以在查询时需要为查询列定义别名。
  • authorities-by-username-query:用户角色查询,根据认证的用户名返回相应的角色信息,返回结构要求拥有用户名(username)、角色(authorities)两个信息。



UserDetailsService

在SpringSecurity中除了可以使用配置文件进行查询配置外,还可以由用户通过UserDetailsService接口标准实现自定义认证与授权信息查询处理,而使用UserDetailsService实现的查询会比配置文件定义查询更加灵活。UserDetailsService接口如下。

在这里插入图片描述

在UserDetailsService接口里只有一个loadUserByUsername方法,此方法会根据用户名查询对应的用户信息与授权信息,而用户和授权信息由于都保存在数据库中,所以可利用SpringDataJPA实现查询操作。下面演示UserDetailsService接口的使用。

1.【lea-springsecurity项目】定义用户信息数据层操作接口。

package com.xiyue.leaspring.po;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.io.Serializable;

@Entity
public class Member implements Serializable {

    @Id
    private String mid;
    private String name;
    private String password;
    private Integer enabled;

    public String getMid() {
        return mid;
    }

    public void setMid(String mid) {
        this.mid = mid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getEnabled() {
        return enabled;
    }

    public void setEnabled(Integer enabled) {
        this.enabled = enabled;
    }

    @Override
    public String toString() {
        return "Member{" +
                "mid='" + mid + '\'' +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", enabled=" + enabled +
                '}';
    }
}
package com.xiyue.leaspring.dao;

import com.xiyue.leaspring.po.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface IMemberDAO extends JpaRepository<Member,String> {
}

2.【lea-springsecurity项目】定义授权信息数据层操作接口。

package com.xiyue.leaspring.po;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.io.Serializable;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 18:44:48
 */
@Entity
public class Role implements Serializable {

    @Id
    private String rid;
    private String title;

    public String getRid() {
        return rid;
    }

    public void setRid(String rid) {
        this.rid = rid;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return "Role{" +
                "rid='" + rid + '\'' +
                ", title='" + title + '\'' +
                '}';
    }
}
package com.xiyue.leaspring.dao;

import com.xiyue.leaspring.po.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Set;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 18:47:46
 */
public interface IRoleDAO extends JpaRepository<Role,String> {
    /**
     * 根据用户ID查询对应的角色ID
     * @param mid 用户ID
     * @return 用户拥有的全部角色ID
     */
    @Query(nativeQuery = true,value = "select rid from member_role where " +
            "mid=:mid")
    public Set<String> findAllByMember(@Param("mid") String mid);
}

3.【lea-springsecurity项目】定义UserDetailsService接口子类,注入相应的数据层接口对象,实现数据查询。在对查询结果进行判断时,可以用AuthenticationException异常类抛出异常。

AuthenticationException异常类的常用子类如下图。

在这里插入图片描述

package com.xiyue.leaspring.service.impl;

import com.xiyue.leaspring.dao.IMemberDAO;
import com.xiyue.leaspring.dao.IRoleDAO;
import com.xiyue.leaspring.po.Member;
import com.xiyue.leaspring.service.UserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * 用户接口实现类
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 18:59:10
 */
@Service(value = "userDetailService")//注解配置,此名称要在spring.xml中使用
public class UserDetailsServiceImpl implements UserDetailService {

    @Autowired
    private IMemberDAO memberDAO;//注入用户数据操作接口

    @Autowired
    private IRoleDAO roleDAO;//注入角色操作接口

    @Override
    public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
        Optional<Member> optionalMember = this.memberDAO.findById(username);//根据用户ID进行查询
        if(!optionalMember.isPresent()){//用户信息不存在
            throw new UsernameNotFoundException("用户“"+
                    username+"”信息不存在,无法进行登录。");
        }
        Member member = optionalMember.get();//获取用户对象
        //用户对应的所有角色需要通过GrantedAuthority集合保存
        List<GrantedAuthority> allGrantedAuthority = new ArrayList<>();
        Set<String> allRoles= this.roleDAO.findAllByMember(username);//获取用户角色信息
        Iterator<String> roleIter = allRoles.iterator();//迭代输出角色信息
        while (roleIter.hasNext()){
            allGrantedAuthority.add(new SimpleGrantedAuthority(roleIter.next()));
        }
        boolean enabled = member.getEnabled().equals(1);//判断用状态
        UserDetails userDetails = new User(username,member.getPassword(),enabled
            ,true,true,true,allGrantedAuthority);

        return userDetails;//返回UserDetails对象
    }
}

4.【lea-springsecurity项目】修改spring.xml配置文件,使用UserDetailsService处理登录。

<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
    <context:component-scan base-package="com.xiyue.leaspring">
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.action"/>
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.dao"/>
    </context:component-scan>
    <context:property-placeholder location="classpath:database.properties"/>
    <!--<bean id="dataSource"
          class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${database.driverClass}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.user}"/>
        <property name="password" value="${database.password}"/>
    </bean>-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init">
        <property name="driverClassName" value="${database.druid.driverClassName}"/><!--驱动-->
        <property name="url" value="${database.druid.url}"/><!--地址-->
        <property name="username" value="${database.druid.username}"/><!--用户-->
        <property name="password" value="${database.druid.password}"/><!--密码-->
        <property name="maxActive" value="${database.druid.maxActive}"/><!--最大连接数-->
        <property name="minIdle" value="${database.druid.minIdle}"/><!--最小连接池-->
        <property name="initialSize" value="${database.druid.initialSize}"/><!--初始化连接大小-->
        <property name="maxWait" value="${database.druid.maxWait}"/><!--最大等待时间-->
        <property name="timeBetweenEvictionRunsMillis" value="${database.druid.timeBetweenEvictionRunsMillis}"/><!--检测空闲连接间隔-->
        <property name="minEvictableIdleTimeMillis" value="${database.druid.minEvictableIdleTimeMillsis}"/><!--连接最小生存时间-->
        <property name="validationQuery" value="${database.druid.validationQuery}"/><!--验证-->
        <property name="testWhileIdle" value="${database.druid.testWhileIdle}"/><!--申请检测-->
        <property name="testOnBorrow" value="${database.druid.testIOnBorrow}"/><!--有效检测-->
        <property name="testOnReturn" value="${database.druid.testIOnReturn}"/><!--归还检测-->
        <property name="poolPreparedStatements" value="${database.druid.poolPreparedStatements}"/><!--是否缓存preparedStatement,也就是PSCache。PSCache能提升支持游标的数据库性能,如Oracle、Mysql下建议关闭-->
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${database.druid.maxpoolPreparedStatementPerConnectionSize}"/><!--启用PSCache,必须配置大于0,当大于0时-->
        <property name="filters" value="${database.druid.filters}"/><!--驱动-->
    </bean>
    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/><!-- 数据源 -->
        <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/><!-- JPA核心配置文件 -->
        <property name="persistenceUnitName" value="LEA_SPRING_JPA"/><!-- 持久化单元名称 -->
        <property name="packagesToScan" value="com.xiyue.leaspring.dao"/><!-- PO类扫描包 -->
        <property name="persistenceProvider"><!-- 持久化提供类,本次为hibernate -->
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
        </property>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
        </property>
    </bean>
    <!-- 定义SpringDataJPA的数据层接口所在包,该包中的接口一定义是Repository子接口 -->
    <jpa:repositories base-package="com.xiyue.leaspring.dao"/>
    <!-- 定义事务管理的配置,必须配置PlatformTransactionManager接口子类 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <security:http auto-config="true"><!--启用HTTP安全认证,并采用自动配置模式-->
        <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
        <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <!--采用http-basic模式登录 -->
        <!--
            <security:http-basic/>
        -->
        <!--登录成功后的首页,需要在用户已经认证后才可以显示-->
        <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
        <security:csrf disabled="true"/><!--关闭CSRF校验-->
        <!--配置表单登录-->
        <security:form-login
            username-parameter="mid"
            password-parameter="pwd"
            authentication-success-forward-url="/welcomePage"
            login-page="/loginPage"
            login-processing-url="/xylogin"
            authentication-failure-url="/loginPage?error=true"
            />
        <security:logout
            logout-url="/xylogout"
            logout-success-url="/logoutPage"
            delete-cookies="JSESSIONID"
        />
    </security:http>
    <security:authentication-manager><!--定义认证管理器-->
        <security:authentication-provider><!--配置认证管理配置类-->
            <security:jdbc-user-service
                    data-source-ref="dataSource"
                    users-by-username-query="select mid as username,password,enabled from member where mid=?"
                    authorities-by-username-query="select mid as username,rid as authorities from member_role where mid=?"/>
        </security:authentication-provider>
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>
</beans>

配置完成后,当前程序会使用UserDetailsServiceImpl实现子类进行用户认证信息与授权信息的获取,并且按照SpringSecurity的要求所有的信息都会包装在UserDetails接口对象中。



Session管理

在系统管理中,为了用户信息的安全往往会对同一个账户的并发登录状态进行控制,所以在这种情况下往往需要对用户登录状态进行监听,即需要在内存中保存相应用户的Session列表,当出现账户重复登录的时候就可以进行指定Session的剔除操作。

1.【lea-springsecurity项目】修改web.xml配置文件,追加Session管理监听器。

<!--定义Session管理监听器-->
<listener>
   <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

2.【lea-springsecurity项目】修改spring.xml配置文件,取消自动配置,同时追加Session并发管理。

<security:http auto-config="false">
        <security:session-management invalid-session-url="/loginPage">
            <!--并发Session管理-->
            <!--max-sessions="1" 每个账户并发访问量-->
            <!--error-if-maximum-exceeded="false" Session剔除模式-->
            <!--expired-url="/loginPage" Session剔除后的错误显示路径-->
            <security:concurrency-control
                max-sessions="1"
                error-if-maximum-exceeded="false"
                expired-url="/loginPage"
            />
        </security:session-management>
        <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
        <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <!--采用http-basic模式登录 -->
        <!--
            <security:http-basic/>
        -->
        <!--登录成功后的首页,需要在用户已经认证后才可以显示-->
        <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
        <security:csrf disabled="true"/><!--关闭CSRF校验-->
        <!--配置表单登录-->
        <security:form-login
            username-parameter="mid"
            password-parameter="pwd"
            authentication-success-forward-url="/welcomePage"
            login-page="/loginPage"
            login-processing-url="/xylogin"
            authentication-failure-url="/loginPage?error=true"
            />
        <security:logout
            logout-url="/xylogout"
            logout-success-url="/logoutPage"
            delete-cookies="JSESSIONID"
        />
    </security:http>

对于并发Session管理,需要考虑的是要剔除之前已登录的Session还是剔除之后登录的Session用户,这点可通过error-if-maximum-exceeded属性进行配置。该属性配置为true,表示剔除新登录Session用户;为false,表示剔除之前登录过的Session用户。



RememberMe

为了防止用户重复登录表单的填写,在实际项目中往往采用RememberMe功能,将用户登录信息暂时保存在Cookie中,这样每次访问时就可以通过请求Cookie信息获取用户登录状态。SpringSecurity对这一功能提供了配置实现。

1.【lea-springsecurity项目】修改login.jsp页面,追加免登录组件(复选框)。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page isELIgnored="false" %><%--启动EL表达式解析--%>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"
            +request.getServerPort()+request.getContextPath()+"/";
    String login_url = basePath + "xylogin";
%>
<html>
<head>
    <title>登陆</title>
    <base href="<%=basePath%>">
</head>
<body>
<h3>用户登陆请输入用户名与密码</h3>

<form action="<%=login_url%>" method="post">
    用户名:
    <input name="mid" type="text" placeholder="请输入字符串用户名"><br>
    密码:
    <input name="pwd" type="password" placeholder="请输入密码"><br>
    <input type="checkbox" id="remember" name="remember" value="true"/>下次免登录</br>
    <input type="submit" value="登录">
    <input type="reset" value="重置">
</form>
</body>
</html>

2.【lea-springsecurity项目】修改spring.xml配置文件,追加RememberMe配置项。

<security:http auto-config="false">
    <security:session-management invalid-session-url="/loginPage">
        <!--并发Session管理-->
        <!--max-sessions="1" 每个账户并发访问量-->
        <!--error-if-maximum-exceeded="false" Session剔除模式-->
        <!--expired-url="/loginPage" Session剔除后的错误显示路径-->
        <security:concurrency-control
            max-sessions="1"
            error-if-maximum-exceeded="false"
            expired-url="/loginPage"/>
    </security:session-management>
    <!--启用RememberMe功能-->
    <!-- remember-me-parameter="remember" 登录表单参数-->
    <!-- key="xiyue-li" Cookies加密密钥-->
    <!-- token-validity-seconds="2592000" 免登录失效(单位为s)-->
    <!-- remember-me-cookie="xiyue-remember-cookies" Cookies名称-->
    <!-- user-service-ref="userDetailService" 处理类-->
    <!-- data-source-ref="dataSource" 持久化保存数据源-->
    <security:remember-me
        remember-me-parameter="remember"
        key="xiyue-li"
        token-validity-seconds="2592000"
        remember-me-cookie="xiyue-remember-cookies"
        user-service-ref="userDetailService"
        data-source-ref="dataSource"
    />
    <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
    <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
    <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
    <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
    <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
    <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
    <!--采用http-basic模式登录 -->
    <!--
        <security:http-basic/>
    -->
    <!--登录成功后的首页,需要在用户已经认证后才可以显示-->
    <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
    <security:csrf disabled="true"/><!--关闭CSRF校验-->
    <!--配置表单登录-->
    <security:form-login
        username-parameter="mid"
        password-parameter="pwd"
        authentication-success-forward-url="/welcomePage"
        login-page="/loginPage"
        login-processing-url="/xylogin"
        authentication-failure-url="/loginPage?error=true"
        />
    <security:logout
        logout-url="/xylogout"
        logout-success-url="/logoutPage"
        delete-cookies="JSESSIONID"
    />
</security:http>

本配置文件中定义了表单要使用的登录参数remember,这样当用户登录后会自动在Cookie中提供mldn-rememberme-cookie内容下图。该Cookie的失效时间为30天,因此用户在30天年内重新打开浏览器不必再重复编写登录表单。

在这里插入图片描述

3.【lea-springsecurity项目】现在已经实现了RememberMe功能,此时的用户信息是在服务器内存中保存的。如果有需要,也可以将所有RememberMe信息在数据库中记录。此时需要使用如下的数据库创建脚本。

--使用数据库
use lea_springsecurity;
--创建数据表保存免费登录信息(数据表名称默认为persistent_logins)
CREATE TABLE persistent_logins(
    series varchar(64),
    username varchar(100),
    token varchar(64),
    last_used TIMESTAMP,
    CONSTRAINT pk_series PRIMARY KEY (series)
);

4.【lea-springsecurity项目】修改spring.xml配置文件,在RememberMe的配置中追加数据源设置。

  <!--true启用HTTP安全认证,并采用自动配置模式-->
    <!--flase取消自动配置模式-->
    <security:http auto-config="false">
        <!--启用RememberMe功能-->
        <!-- remember-me-parameter="remember" 登录表单参数-->
        <!-- key="xiyue-li" Cookies加密密钥-->
        <!-- token-validity-seconds="2592000" 免登录失效(单位为s)-->
        <!-- remember-me-cookie="xiyue-remember-cookies" Cookies名称-->
        <!-- user-service-ref="userDetailService" 处理类-->
        <!-- data-source-ref="dataSource" 持久化保存数据源-->
        <security:remember-me
                remember-me-parameter="remember"
                key="xiyue-li"
                token-validity-seconds="2592000"
                remember-me-cookie="xiyue-rememberme-cookies"
                data-source-ref="dataSource"
                user-service-ref="userDetailService"/>
        <security:session-management invalid-session-url="/loginPage">
            <!--并发Session管理-->
            <!--max-sessions="1" 每个账户并发访问量-->
            <!--error-if-maximum-exceeded="false" Session剔除模式-->
            <!--expired-url="/loginPage" Session剔除后的错误显示路径-->
            <security:concurrency-control
                max-sessions="1"
                error-if-maximum-exceeded="false"
                expired-url="/loginPage"/>
        </security:session-management>

        <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
        <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <!--采用http-basic模式登录 -->
        <!--
            <security:http-basic/>
        -->
        <!--登录成功后的首页,需要在用户已经认证后才可以显示-->
        <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
        <security:csrf disabled="true"/><!--关闭CSRF校验-->
        <!--配置表单登录-->
        <security:form-login
            username-parameter="mid"
            password-parameter="pwd"
            authentication-success-forward-url="/welcomePage"
            login-page="/loginPage"
            login-processing-url="/xylogin"
            authentication-failure-url="/loginPage?error=true"
            />
        <security:logout
            logout-url="/xylogout"
            logout-success-url="/logoutPage"
            delete-cookies="JSESSIONID"
        />
    </security:http>

在进行持久化保存时只需要按照数据表结构建立数据表,随后配置上要使用的数据源,这样在用户进行免登录选择的时候就可以自动将相应的信息保存在数据库中,即便服务器重新启动也不会丢失用户的免登录配置。



过滤器

SpringSecurity的核心操作是依据过滤器实现的认证与授权检测,但是在DelegatingFilterProxy过滤器中为了方便进行认证与授权管理还提供了一套自定义的过滤链见下表进行配置。同时这些过滤链拥有严格的执行顺序,才可以实现最终安全检测。

在这里插入图片描述

下面通过自定义过滤器实现一个登录验证码的检测处理操作,由于验证码的检测需要结合用户登录处理,所以本次将直接继承UsernamePasswordAuthenticationFilter父类实现过滤器定义。

提示:使用kaptcha实现验证码。

为了方便将直接使用Google开源的验证码组件kaptcha,该组件的核心配置如下。

1.修改pom.xml配置文件,追加kaptcha组件依赖。

<kaptcha.version>0.0.9</kaptcha.version>
<dependency>
    <groupId>com.github.axet</groupId>
    <artifactId>kaptcha</artifactId>
    <version>${kaptcha.version}</version>
</dependency>

2.为方便配置建立一个KaptchaConfig配置类,进行DefaultKaptcha类对象的创建。

package com.xiyue.leaspring.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 22:19:28
 */
@Configuration
public class KaptchaConfig {
    @Bean
    public DefaultKaptcha captchaProducer(){
        DefaultKaptcha captchaProducer = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.setProperty("kaptcha.border","yes");
        properties.setProperty("kaptcha.border.color","105,179,90");
        properties.setProperty("kaptcha.textproducer.font.color","red");
        properties.setProperty("kaptcha.image.width","125");
        properties.setProperty("kaptcha.image.heigth","45");
        properties.setProperty("kaptcha.textproducer.font.size","35");
        properties.setProperty("kaptcha.session.key","captcha");
        properties.setProperty("kaptcha.textproducer.char.length","4");
        properties.setProperty("kaptcha.textproducer.font.names","宋体,楷体,微软雅黑");
        Config config = new Config(properties);
        captchaProducer.setConfig(config);
        return  captchaProducer;
    }
}

本程序中,通过properties.setProperty(“kaptcha.session.key”, “captcha”);语句设置了验证码的名称。

3.在GlobalAction类中追加一个验证码的显示路径。

 @RequestMapping(value = "/RandomCode")
 public ModelAndView kaptcha(){
     HttpServletRequest request = ((ServletRequestAttributes)
             RequestContextHolder.getRequestAttributes()).getRequest();
     HttpServletResponse response = ((ServletRequestAttributes)
             RequestContextHolder.getRequestAttributes()).getResponse();
     HttpSession session = request.getSession();
     response.setHeader("Pragma","No-cache");//不缓存数据
     response.setHeader("Cache-Control","no-cache");//不缓存数据
     response.setDateHeader("Expires",0);//不失效
     response.setContentType("image/jpeg");//MIME类型
     String capText = captchaProducer.createText();//获取验证码上的文字
     //将验证码上的文字保存在Session中
     session.setAttribute(Constants.KAPTCHA_SESSION_KEY,capText);
     String code = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
     this.logger.info("验证码:"+code);
     BufferedImage image = this.captchaProducer.createImage(capText);//图像
     try {
         OutputStream outputStream = response.getOutputStream();
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         ImageIO.write(image,"JPEG",bos);//图像输出
         byte[] buf = bos.toByteArray();
         response.setContentLength(buf.length);
         outputStream.write(buf);
         bos.close();
         outputStream.close();
     }catch (Exception e){

     }

     return null;
 }

上述程序中,session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);语句非常关键,作用是保存Session中的属性名称,随后的代码将通过此属性名称获取生成的验证码数据以实现与输入验证码的匹配。

4.验证码设置在根路径上显示,所以还需要在spring.xml配置文件中追加拦截路径。

 <security:intercept-url pattern="/**" access="permitAll()"/>

为了方便,本处只列出了核心代码,完整代码可以参考对应项目中的程序文件。

1.【lea-springsecurity项目】修改login.jsp页面,追加验证码输入框。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page isELIgnored="false" %><%--启动EL表达式解析--%>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"
            +request.getServerPort()+request.getContextPath()+"/";
    String login_url = basePath + "xylogin";
%>
<html>
<head>
    <title>登陆</title>
    <base href="<%=basePath%>">
</head>
<body>
<h3>用户登陆请输入用户名与密码</h3>

<form action="<%=login_url%>" method="post">
    用户名:
    <input name="mid" type="text" placeholder="请输入字符串用户名"><br>
    密码:
    <input name="pwd" type="password" placeholder="请输入密码"><br>
    验证码:<input type="text" maxlength="4" size="4" name="code">
                <img src="/RandomCode"></br>
            </input>
    <input type="checkbox" id="remember" name="remember" value="true" checked="true"/>下次免登录</br>
    <input type="submit" value="登录">
    <input type="reset" value="重置">
</form>
</body>
</html>

2.【lea-springsecurity项目】创建UsernamePasswordAuthenticationFilter子类,以实现验证码检测。

package com.xiyue.leaspring;

import com.google.code.kaptcha.Constants;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 23:26:08
 */
public class ValidatorCodeUsernamePasswordAuthenticationFilter extends
        UsernamePasswordAuthenticationFilter {
    private String codeParameter = "code";
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String captcha = (String) request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);//获取生成的验证码
        String code = request.getParameter(this.codeParameter);//获取输入验证码
        String username = super.obtainUsername(request).trim();//获取用户名
        String password = super.obtainPassword(request).trim();//取得密码
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,password);//生成认证标记
        super.setDetails(request,authRequest);//设置认证详情
        //当没有输入验证,没有生成验证码时或者验证码不匹配时会提示错误
        if(captcha == null || "".equals(captcha) || code == null ||
            "".equals(code) || !captcha.equalsIgnoreCase(code)){
            request.getSession().setAttribute("SPRING_SECURITY_LAST_USERNAME",username);
            throw new AuthenticationServiceException("验证码不正确!");
        }
        return  super.getAuthenticationManager().authenticate(authRequest);
    }


    public void setCodeParameter(String codeParameter){
        this.codeParameter = codeParameter;
    }
}

3.【lea-springsecurity项目】此时要采用的是自定义的登录认证过滤器,所以最好的做法是单独配置一个登录控制操作,替换原始的security:form-login配置项,具体配置项如下。

  • 定义一个新的登录处理终端,实现认证控制。
<bean id="authenticationEntryPoint"   class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg index="0" value="/loginPage"/>
</bean>
  • 定义登录成功处理Bean。
<bean id="loginLogAuthenticationSuccessHandler"
class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
   <property name="defaultTargetUrl" value="/welcomePage"/>
</bean>
  • 定义登录失败处理Bean。
<bean id="simpleUrlAuthenticationFailureHandle"  class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="loginPage?error=true"/>
</bean>
  • 由于此时需要单独配置登录操作,所以还需要为认证管理器设置一个ID,方便引用。
<security:authentication-manager><!--定义Security认证管理器-->
    <security:authentication-provider user-service-ref="userDetailService"/>
</security:authentication-manager>
  • 配置新定义的过滤器ValidatorCodeUsernamePasswordAuthenticationFilter,配置好相应的路径处理类对象,与认证管理器引用。
<bean id="validatorCode"     class="com.xiyue.leaspring.filter.ValidatorCodeUsernamePasswordAuthenticationFilter">
    <property name="authenticationSuccessHandler" ref="loginLogAuthenticationSuccessHandler"/>
    <property name="authenticationFailureHandler" ref="simpleUrlAuthenticationFailureHandle"/>
    <property name="authenticationManager" ref="authenticationManager"/>
    <property name="filterProcessesUrl" value="/xylogin"/>
    <property name="usernameParameter" value="mid"/>
    <property name="passwordParameter" value="pwd"/>
</bean>
  • 修改security:http元素配置,配置新的认证终端处理,同时需要在登录前使用验证码过滤器。
<security:http auto-config="false" entry-point-ref="authenticationEntryPoint">
   <security:custom-filter ref="validatorCode" before="FORM_LOGIN_FILTER"/>
  <!--启用RememberMe功能-->
  <!-- remember-me-parameter="remember" 登录表单参数-->
  <!-- key="xiyue-li" Cookies加密密钥-->
  <!-- token-validity-seconds="2592000" 免登录失效(单位为s)-->
  <!-- remember-me-cookie="xiyue-remember-cookies" Cookies名称-->
  <!-- user-service-ref="userDetailService" 处理类-->
  <!-- data-source-ref="dataSource" 持久化保存数据源-->
  <security:remember-me
          remember-me-parameter="remember"
          key="xiyue-li"
          token-validity-seconds="2592000"
          remember-me-cookie="xiyue-rememberme-cookies"
          data-source-ref="dataSource"
          user-service-ref="userDetailService"/>
  <security:session-management invalid-session-url="/loginPage">
      <!--并发Session管理-->
      <!--max-sessions="1" 每个账户并发访问量-->
      <!--error-if-maximum-exceeded="false" Session剔除模式-->
      <!--expired-url="/loginPage" Session剔除后的错误显示路径-->
      <security:concurrency-control
          max-sessions="1"
          error-if-maximum-exceeded="false"
          expired-url="/loginPage"/>
  </security:session-management>

  <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
  <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
  <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
  <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
  <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
  <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
  <!--采用http-basic模式登录 -->
  <!--
      <security:http-basic/>
  -->
  <!--登录成功后的首页,需要在用户已经认证后才可以显示-->
  <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
  <security:intercept-url pattern="/**" access="permitAll()"/>
  <security:csrf disabled="true"/><!--关闭CSRF校验-->
  <!--配置表单登录-->
  <!--<security:form-login
      username-parameter="mid"
      password-parameter="pwd"
      authentication-success-forward-url="/welcomePage"
      login-page="/loginPage"
      login-processing-url="/xylogin"
      authentication-failure-url="/loginPage?error=true"/>-->
  <security:logout
      logout-url="/xylogout"
      logout-success-url="/logoutPage"
      delete-cookies="JSESSIONID"
  />
</security:http>

至此可以实现验证码检测,当验证码检测不通过时将不会进行用户信息认证操作。



SpringSecurity注解

对于访问路径的安全检测除了拦截路径的配置外,还可以通过注解的形式进行控制层或业务层中指定方法的访问验证,在SpringSecurity中提供的注解有如下两组。

  • @Secured:该注解为早期注解,可以直接进行角色验证,但是不支持SpEL表达式。
  • @PreAuthorize / @PostAuthorize:该注解支持SpEL表达式,其中@PreAuthorize在方法执行前验证,而@PostAuthorize在方法执行后验证,一般使用较少。

    1.【lea-springsecurity项目】由于所有注解都要写在Action或业务层中,所以修改spring-mvc.xml配置文件,添加SpringSecurity注解的启用配置,同时删除spring.xml配置文件在security:http元素中定义的请求拦截路径。
 <!--启用SpringSecurity注解功能-->
 <!--启用@PreAuthorize/PostAuthorize功能-->
 <!--启用@secured-->
 <security:global-method-security
         pre-post-annotations="enabled"
         secured-annotations="enabled"/>

2.【lea-springsecurity项目】修改GlobalAction类中的welcomePage路径。该路径要求认证过的用户才可以访问。

@PreAuthorize("isAuthenticated()")
@RequestMapping("/welcomePage")//访问路径
public String welcome(){//登录成功路径
    Authentication authentication = SecurityContextHolder
            .getContext().getAuthentication();//获取认证对象
    UserDetails userDetails = (UserDetails) authentication.getPrincipal();//用户详情
    String username = userDetails.getUsername();//获得用户名
    this.logger.info("用户名:"+username);
    //通过userDetail对象获取当前用户的所有授权信息
    Collection<? extends GrantedAuthority> authorities =userDetails.getAuthorities();
    this.logger.info("授权信息:"+authorities);
    return "welcome";//设置跳转路径
}

由于需要使用SpEL表达式,所以在进行认证检测时直接使用了@PreAuthorize注解。这样,当进行访问时如果当前用户没有登录过,则会跳转到登录表单页面;如果用户登录过,就可以直接访问。

3.【lea-springsecurity项目】修改EchoAction程序类,追加角色判断。

package com.xiyue.leaspring.action;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller//定义控制器
@RequestMapping("/pages/message")//定义访问父路径,与方法中的路径组合为完整的路径
public class EchoAction {//自定义Action程序

    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping("/show")//访问的路径为url
    public ModelAndView echo(String msg){
        return new ModelAndView("/message/message_show").addObject("echoMessage","[ECHO]msg="+msg);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/input")//访问的路径
    public String input(){
        return "/message/message_input";//jump route
    }
}

本程序利用注解为具体的Action方法进行了授权检测,当不具有指定角色用户访问时会自动跳转到错误页显示。



投票器

对于安全访问,除了使用拦截路径形式进行访问控制外,还可以利用决策管理器根据实际业务实现安全访问。利用决策管理器中的投票机制,可以决定是否需要进行授权控制。

在之前的访问控制中使用过两个投票器:角色投票器(RoleVoter)与认证投票器(AuthenticatedVoter)。所有的投票器都会被访问决策管理器所管理,这些类之间的结构如下图所示。

在这里插入图片描述

通过上图可以发现,决策管理器的父接口为AccessDecisionManager,同时在此接口中定义了投票的3种状态:赞成(ACCESS_GRANTED)、弃权(ACCESS_ABSTAIN)、反对(ACCESS_DENIED)。在SpringSecurity中定义了3个常用的投票管理器,具体作用如下。

  • org.springframework.security.access.vote.AffirmativeBased:一票通过,只要有一个支持就允许访问。
  • org.springframework.security.access.vote.ConsensusBased:半数以上支持票数就可以访问。
  • org.springframework.security.access.vote.UnanimousBased:全票通过后才可以访问。

如果要实现一个自定义的投票器并且可以被投票管理器所管理,那么该投票类需要实现AccessDecisionVoter父接口,在AccessDecisionVoter接口中定义的方法如下表所示。

在这里插入图片描述



AccessDecisionVoter

下面采用自定义投票器的方式实现本地访问控制。本例中,通过localhost(127.0.0.1)访问的用户不需要登录就可以直接进行操作。

1.【lea-springsecurity项目】建立一个IP地址的投票器。

package com.xiyue.leaspring.config;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import java.util.Collection;
import java.util.Iterator;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/19 22:28:38
 */
public class IPAdressVoter implements AccessDecisionVoter<Object> {

    private static final String LOCAL_FLAG = "LOCAL_IP";//需要判断的访问标记

    @Override
    public boolean supports(ConfigAttribute attribute) {//如果有指定配置属性,则执行投票器
        return attribute!=null &&
                attribute.toString().contains(LOCAL_FLAG);
    }

    @Override
    public boolean supports(Class<?> clazz) {//对所有访问类均支持投票
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if(!(authentication.getDetails() instanceof WebAuthenticationDetails)){//如果不是来自Web访问
            return AccessDecisionVoter.ACCESS_DENIED;//拒绝该用户访问
        }
        //通过认证信息获取用户的详情内容,该内容类型WebAuthenticationDetails
        WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
        String ip = details.getRemoteAddress();//取得当前操作的IP地址
        Iterator<ConfigAttribute> iterator = attributes.iterator();//获取每一个配置属性
        while (iterator.hasNext()){//循环每一个配置属性
            ConfigAttribute configAttribute = iterator.next();//获取属性
            if(configAttribute.toString().contains(LOCAL_FLAG)){//如果在本地执行
                if("0:0:0:0:0:0:0:1".equals(ip) || "127.0.0.1".equals(ip)){//本地访问
                    return AccessDecisionVoter.ACCESS_GRANTED;//访问通过
                }
            }
        }
        return AccessDecisionVoter.ACCESS_ABSTAIN;//弃权不参与投票
    }
}

本程序中设置了一个新的访问控制标记LOCAL_IP,如果拦截路径上使用了此访问类型,同时又属于本机直接访问的情况,将不会进行认证处理,可以直接使用。

2.【lea-springsecurity项目】由于需要引入新的投票器,所以此时需要修改spring.xml配置文件,定义新的投票管理器,且配置相应的投票器。

<bean id="accessDecisionManager"
        class="org.springframework.security.access.vote.AffirmativeBased"><!--管理器-->
    <constructor-arg name="decisionVoters"><!--投票器-->
         <list>
             <!--定义角色投票器,进行角色认证-->
             <bean class="org.springframework.security.access.vote.RoleVoter"/>
             <!--定义认证投票器,用于判断用户是否已经认证-->
             <bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
             <!--自定义投票器,如果是本机IP则不进行过滤-->
             <bean class="com.xiyue.leaspring.config.IPAdressVoter"/>
             <!--开启表达式支持,这样就可以在进行拦截时使用SpEL-->
             <bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
         </list>
     </constructor-arg>
 </bean>

在本次定义的决策管理器中一共设置了3个投票器,由于使用的是AffirmativeBased管理器,所以只要有一个投票器投出赞成票,就可以进行访问。

3.【lea-springsecurity项目】修改spring.xml配置文件中的security:http配置项,引入自定义的访问决策管理器,同时在需要验证的路径上定义LOCAL_IP标记。

  <security:http auto-config="false"
                   entry-point-ref="authenticationEntryPoint"
                    access-decision-manager-ref="accessDecisionManager">
    <security:custom-filter ref="validatorCode" before="FORM_LOGIN_FILTER"/>
     <!--启用RememberMe功能-->
     <!-- remember-me-parameter="remember" 登录表单参数-->
     <!-- key="xiyue-li" Cookies加密密钥-->
     <!-- token-validity-seconds="2592000" 免登录失效(单位为s)-->
     <!-- remember-me-cookie="xiyue-remember-cookies" Cookies名称-->
     <!-- user-service-ref="userDetailService" 处理类-->
     <!-- data-source-ref="dataSource" 持久化保存数据源-->
     <security:remember-me
             remember-me-parameter="remember"
             key="xiyue-li"
             token-validity-seconds="2592000"
             remember-me-cookie="xiyue-rememberme-cookies"
             data-source-ref="dataSource"
             user-service-ref="userDetailService"/>
     <security:session-management invalid-session-url="/loginPage">
         <!--并发Session管理-->
         <!--max-sessions="1" 每个账户并发访问量-->
         <!--error-if-maximum-exceeded="false" Session剔除模式-->
         <!--expired-url="/loginPage" Session剔除后的错误显示路径-->
         <security:concurrency-control
                 max-sessions="1"
                 error-if-maximum-exceeded="false"
                 expired-url="/loginPage"/>
     </security:session-management>

     <!--定义授权检测失败时的显示页面,一旦拒绝,将自动自动进行跳转-->
     <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
     <!--定义要拦截的路径,可以时具体路径,也可以使用路径匹配符设置要拦截的父路径-->
     <!--表达式“hasRole(角色名称)”表示拥有此角色的用可以才可以访问-->
     <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
     <security:intercept-url pattern="/pages/message/**" access="hasRole('USER') or hasRole('LOCAL_IP')"/>
     <!--采用http-basic模式登录 -->
     <!--
         <security:http-basic/>
     -->
     <!--登录成功后的首页,需要在用户已经认证后才可以显示-->
     <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
     <security:intercept-url pattern="/**" access="permitAll()"/>
     <security:csrf disabled="true"/><!--关闭CSRF校验-->
     <!--配置表单登录-->
     <!--<security:form-login
         username-parameter="mid"
         password-parameter="pwd"
         authentication-success-forward-url="/welcomePage"
         login-page="/loginPage"
         login-processing-url="/xylogin"
         authentication-failure-url="/loginPage?error=true"/>-->
     <security:logout
             logout-url="/xylogout"
             logout-success-url="/logoutPage"
             delete-cookies="JSESSIONID"/>
 </security:http>

配置成功后,当通过localhost或者127.0.0.1访问/pages/message/**路径中的信息时,将不再受到角色限制。如果是远程访问,则依然需要按照传统方式登录认证后才可以访问。



RoleHierarchy

为了进一步完善授权访问的级别层次,SpringSecurity提供了角色继承概念,即使用者可定义继承的层次关系,这样,拥有更高级别角色的用户可以直接访问低级别角色的信息。

提示:关于角色继承的描述。

假设在spring.xml中有如下两个拦截路径。

<security:intercept-url pattern="/pages/message/input" access="hasRole('USER')"/>
<security:intercept-url pattern="pages/message/show" access="hasRole('ADMIN')"/>

此时两个访问路径分别配置了两种角色,按照之前的定义,假设ROLE_ADMIN是最高管理员权限,在这样的配置下如果一个用户只拥有ROLE_ADMIN角色,依然无法访问ROLE_USER对应的信息。配置了角色层次关系后,即便拥有ROLE_ADMIN的用户没有ROLE_USER角色,也可以访问ROLE_USER角色对应的信息。

为了实现这一操作,需要先删除member_role表中admin用户对应的ROLE_USER角色信息。

在SpringSecurity中,对于角色继承提供了org.springframework.security.access.hierarchicalroles.RoleHierarchy接口,此接口定义如下。

在这里插入图片描述

此接口定义了获取全部可用控制权限的方法,SpringSecurity框架还提供了一个基础的实现子类RoleHierarchyImpl,下面将利用此类结合配置文件,实现角色继承。配置文件中,角色继承的配置格式为:角色1 >角色2 > … >角色n。

1.【lea-springsecurity项目】修改spring.xml配置文件,配置角色继承关系。

<bean id="roleHierarchy"   class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
    <property name="hierarchy"><!--定义继承层次关系-->
        <value>ROLE_ADMIN>ROLE_USER</value><!--定义角色层次-->
    </property>
</bean>

2.【lea-springsecurity项目】由于所有的角色检测操作都通过表达式进行配置,所以需要在WebExpressionVoter投票器中进行角色继承配置,修改spring.xml配置文件。

<bean id="accessDecisionManager"
    class="org.springframework.security.access.vote.AffirmativeBased"><!--管理器-->
    <constructor-arg name="decisionVoters"><!--投票器-->
        <list>
            <!--定义角色投票器,进行角色认证-->
            <bean class="org.springframework.security.access.vote.RoleVoter"/>
            <!--定义认证投票器,用于判断用户是否已经认证-->
            <bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
            <!--自定义投票器,如果是本机IP则不进行过滤-->
            <bean class="com.xiyue.leaspring.config.IPAdressVoter"/>
            <!--定义角色继承投票器,此投票器可以在注解中使用-->
            <bean class="org.springframework.security.access.vote.RoleHierarchyVoter">
                <constructor-arg ref="roleHierarchy"/><!--引用角色继承配置-->
            </bean>
            <!--由于需要通过SpEL定义访问继承关系,所以要在Web表达式投票器中配置角色继承定义-->
            <!--开启表达式支持,这样就可以在进行拦截时使用SpEL-->
            <bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
                <property name="expressionHandler"><!--定义表达式处理器-->
                    <bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">
                        <!--引用角色继承配置-->
                        <property name="roleHierarchy" ref="roleHierarchy"/>
                    </bean>
                </property>
            </bean>
        </list>
    </constructor-arg>
</bean>

配置完成后,当用户拥有了ROLE_ADMIN角色就可以访问ROLE_USER对应的资源。



基于Bean配置

SpringSecurity的所有核心配置,除了可以利用配置文件实现外,也可以采用Bean配置完成。SpringSecurity中提供了WebSecurityConfigurer接口,开发者只需要实现此接口或继承WebSecurityConfigurerAdapter抽象类,即可实现配置。SpringSecurity的基本定义结构如下图所示。

在这里插入图片描述

自定义SpringSecurity配置类时,可以根据需要覆写WebSecurityConfigurerAdapter抽象类中的configure方法,如下表所示。

在这里插入图片描述



基础配置

通过Bean实现一个固定认证信息的登录、注销、认证与授权控制的配置操作。需要注意的是,如果通过配置类定义SpringSecurity配置,需要满足如下两项。

  • 配置Bean需要设置在扫描路径中,并且需要使用@Configuration注解声明。
  • 由于该Bean主要负责SpringSecurity配置,所以需要在类定义中使用@EnableWeb Security注解。


    范例

    :定义WebSecurityConfiguration配置类,进行SpringSecurity基础配置。
package com.xiyue.leaspring.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/19 23:48:24
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //进行用户角色配置时不需要追加ROLE_前缀,系统会自动添加
        auth.inMemoryAuthentication()//固定认证信息
                .withUser("admin")//用户名
                .password("{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16")//密码
                .roles("USER","ADMIN");//角色
        auth.inMemoryAuthentication()//固定认证信息
                .withUser("xiyue")//用户名
                .password("{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em")//密码
                .roles("USER");//角色
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//禁用CSRF验证
        //配置拦截路径的匹配地址与限定授权访问
        http.authorizeRequests()//配置认证请求
            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授权访问
            .antMatchers("/welcomePage").authenticated()//认证访问
            .antMatchers("/**").permitAll();//任意访问
        //配置HTTP登录,注销与错误路径
        http.formLogin()//登录配置
            .usernameParameter("mid")//用户名参数设置
            .passwordParameter("pwd")//用户密码参数设置
            .successForwardUrl("/welcomePage")//登录成功路径
            .loginPage("/loginPage")//登录表单页面
            .loginProcessingUrl("/xylogin")//登录路径
            .failureForwardUrl("/loginPage?error=true")//登录失败路径
            .and()//配置连接
            .logout()//注销配置
            .logoutUrl("/xylogout")//注销路径
            .logoutSuccessUrl("/logoutPage")//注销成功路径
            .deleteCookies("JSESSIONID")//删除Cookie
            .and()//配置连接
            .exceptionHandling()//认证错误配置
            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授权错误
    }
}

本程序实现了一个基本的SpringSecurity使用配置,主要使用了两个configure方法。

  • configure(AuthenticationManagerBuilder auth):配置认证用户。这里使用auth.inMemory Authentication方法声明了两个用户,并为其分配了相应角色。
  • configure(HttpSecurity http):配置拦截路径、登录、注销与授权错误。



深入配置

SpringSecurity中除了基础的登录认证外,还有UserDetails、Session管理、过滤器、访问注解等相关配置。这些配置也可以直接通过Bean实现管理。


范例

:自定义认证处理操作。

<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
    <context:component-scan base-package="com.xiyue.leaspring">
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.action"/>
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.dao"/>
    </context:component-scan>
    <context:property-placeholder location="classpath:database.properties"/>
    <!--<bean id="dataSource"
          class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${database.driverClass}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.user}"/>
        <property name="password" value="${database.password}"/>
    </bean>-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init">
        <property name="driverClassName" value="${database.druid.driverClassName}"/><!--驱动-->
        <property name="url" value="${database.druid.url}"/><!--地址-->
        <property name="username" value="${database.druid.username}"/><!--用户-->
        <property name="password" value="${database.druid.password}"/><!--密码-->
        <property name="maxActive" value="${database.druid.maxActive}"/><!--最大连接数-->
        <property name="minIdle" value="${database.druid.minIdle}"/><!--最小连接池-->
        <property name="initialSize" value="${database.druid.initialSize}"/><!--初始化连接大小-->
        <property name="maxWait" value="${database.druid.maxWait}"/><!--最大等待时间-->
        <property name="timeBetweenEvictionRunsMillis" value="${database.druid.timeBetweenEvictionRunsMillis}"/><!--检测空闲连接间隔-->
        <property name="minEvictableIdleTimeMillis" value="${database.druid.minEvictableIdleTimeMillsis}"/><!--连接最小生存时间-->
        <property name="validationQuery" value="${database.druid.validationQuery}"/><!--验证-->
        <property name="testWhileIdle" value="${database.druid.testWhileIdle}"/><!--申请检测-->
        <property name="testOnBorrow" value="${database.druid.testIOnBorrow}"/><!--有效检测-->
        <property name="testOnReturn" value="${database.druid.testIOnReturn}"/><!--归还检测-->
        <property name="poolPreparedStatements" value="${database.druid.poolPreparedStatements}"/><!--是否缓存preparedStatement,也就是PSCache。PSCache能提升支持游标的数据库性能,如Oracle、Mysql下建议关闭-->
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${database.druid.maxpoolPreparedStatementPerConnectionSize}"/><!--启用PSCache,必须配置大于0,当大于0时-->
        <property name="filters" value="${database.druid.filters}"/><!--驱动-->
    </bean>
    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/><!-- 数据源 -->
        <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/><!-- JPA核心配置文件 -->
        <property name="persistenceUnitName" value="LEA_SPRING_JPA"/><!-- 持久化单元名称 -->
        <property name="packagesToScan" value="com.xiyue.leaspring.dao"/><!-- PO类扫描包 -->
        <property name="persistenceProvider"><!-- 持久化提供类,本次为hibernate -->
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
        </property>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
        </property>
    </bean>
    <!-- 定义SpringDataJPA的数据层接口所在包,该包中的接口一定义是Repository子接口 -->
    <jpa:repositories base-package="com.xiyue.leaspring.dao"/>
    <!-- 定义事务管理的配置,必须配置PlatformTransactionManager接口子类 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <!--开启注解事务-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <security:authentication-manager><!--定义认证管理器-->
        <security:authentication-provider><!--配置认证管理配置类-->
            <security:jdbc-user-service
                    data-source-ref="dataSource"
                    users-by-username-query="select mid as username,password,enabled from member where mid=?"
                    authorities-by-username-query="select mid as username,rid as authorities from member_role where mid=?"/>
        </security:authentication-provider>
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>

    <security:authentication-manager id="authenticationManager"><!--定义Security认证管理器-->
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>
    <bean id="accessDecisionManager"
        class="org.springframework.security.access.vote.AffirmativeBased"><!--管理器-->
        <constructor-arg name="decisionVoters"><!--投票器-->
            <list>
                <!--定义角色投票器,进行角色认证-->
                <bean class="org.springframework.security.access.vote.RoleVoter"/>
                <!--定义认证投票器,用于判断用户是否已经认证-->
                <bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
                <!--自定义投票器,如果是本机IP则不进行过滤-->
                <bean class="com.xiyue.leaspring.config.IPAdressVoter"/>
                <!--定义角色继承投票器,此投票器可以在注解中使用-->
                <bean class="org.springframework.security.access.vote.RoleHierarchyVoter">
                    <constructor-arg ref="roleHierarchy"/><!--引用角色继承配置-->
                </bean>
                <!--由于需要通过SpEL定义访问继承关系,所以要在Web表达式投票器中配置角色继承定义-->
                <!--开启表达式支持,这样就可以在进行拦截时使用SpEL-->
                <bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
                    <property name="expressionHandler"><!--定义表达式处理器-->
                        <bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">
                            <!--引用角色继承配置-->
                            <property name="roleHierarchy" ref="roleHierarchy"/>
                        </bean>
                    </property>
                </bean>
            </list>
        </constructor-arg>
    </bean>
    <bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
        <property name="hierarchy"><!--定义继承层次关系-->
            <value>ROLE_ADMIN>ROLE_USER</value><!--定义角色层次-->
        </property>
    </bean>
</beans>
package com.xiyue.leaspring.config;

import com.xiyue.leaspring.filter.ValidatorCodeUsernamePasswordAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;

import javax.sql.DataSource;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/19 23:48:24
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)//启用注解配置
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;//数据源
    @Autowired
    private UserDetailsService userDetailsService;//用户服务
    @Autowired
    private SavedRequestAwareAuthenticationSuccessHandler successHandler;//成功页
    @Autowired
    private SimpleUrlAuthenticationFailureHandler failureHandler;//失败页
    @Autowired
    private SessionInformationExpiredStrategy sessionExpiredStrategy;//Session失效策略
    @Autowired
    private UsernamePasswordAuthenticationFilter authenticationFilter;//认证过滤器
    @Autowired
    private JdbcTokenRepositoryImpl tokenRepository;//token存储
    @Autowired
    private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;



    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //进行用户角色配置时不需要追加ROLE_前缀,系统会自动添加
//        auth.inMemoryAuthentication()//固定认证信息
//                .withUser("admin")//用户名
//                .password("{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16")//密码
//                .roles("USER","ADMIN");//角色
//        auth.inMemoryAuthentication()//固定认证信息
//                .withUser("xiyue")//用户名
//                .password("{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em")//密码
//                .roles("USER");//角色
        auth.userDetailsService(this.userDetailsService);//基于数据库认证
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();//禁用CSRF验证
//        //配置拦截路径的匹配地址与限定授权访问
//        http.authorizeRequests()//配置认证请求
//            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授权访问
//            .antMatchers("/welcomePage").authenticated()//认证访问
//            .antMatchers("/**").permitAll();//任意访问
//        //配置HTTP登录,注销与错误路径
//        http.formLogin()//登录配置
//            .usernameParameter("mid")//用户名参数设置
//            .passwordParameter("pwd")//用户密码参数设置
//            .successForwardUrl("/welcomePage")//登录成功路径
//            .loginPage("/loginPage")//登录表单页面
//            .loginProcessingUrl("/xylogin")//登录路径
//            .failureForwardUrl("/loginPage?error=true")//登录失败路径
//            .and()//配置连接
//            .logout()//注销配置
//            .logoutUrl("/xylogout")//注销路径
//            .logoutSuccessUrl("/logoutPage")//注销成功路径
//            .deleteCookies("JSESSIONID")//删除Cookie
//            .and()//配置连接
//            .exceptionHandling()//认证错误配置
//            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授权错误
        http.csrf().disable();//禁用CSRF验证
        http.httpBasic().authenticationEntryPoint(this.authenticationEntryPoint);
        //进行拦截路径的匹配地址配置与授权访问限定
        http.authorizeRequests()//配置认证请求
            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授权访问
            .antMatchers("/welcomePage").authenticated();//认证访问
        //进行http注销与错误路径配置,登录操作将由过滤器负责完成
        http.logout()//注销配置
            .logoutUrl("/xylogout")//注销路径
            .logoutSuccessUrl("/logoutPage")//注销成功路径
            .deleteCookies("JSESSIONID")//删除Cookie
            .and()//配置连接
            .exceptionHandling()//认证错误配置
            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授权错误页
        http.rememberMe()//开启RememberMe
            .rememberMeParameter("remember")//表单参数
            .key("xy-xiyue")//加密key
            .tokenValiditySeconds(2592000)//失效时间
            .rememberMeCookieName("xiyue-rememberme-cookie")//Cookie名称
            .tokenRepository(this.tokenRepository);//持久化
        http.sessionManagement()//Session管理
            .invalidSessionUrl("/loginPage")//失效路径
            .maximumSessions(1)//并发Session
            .expiredSessionStrategy(this.sessionExpiredStrategy);//失效策略
        http.addFilterBefore(this.authenticationFilter,UsernamePasswordAuthenticationFilter.class);//追加过滤器
    }
    @Override
    public void configure(WebSecurity web) throws Exception{
        web.ignoring().antMatchers("/index.jsp");//忽略的验证路径
    }

    @Bean
    public UsernamePasswordAuthenticationFilter getAuthenticationFilter() throws Exception{
        ValidatorCodeUsernamePasswordAuthenticationFilter filter = new
                ValidatorCodeUsernamePasswordAuthenticationFilter();
        filter.setAuthenticationManager(super.authenticationManager());//认证管理器
        filter.setAuthenticationSuccessHandler(this.successHandler);//登录成功页面
        filter.setAuthenticationFailureHandler(this.failureHandler);//登录失败页面
        filter.setFilterProcessesUrl("/xylogin");//登录路径
        filter.setUsernameParameter("mid");//参数名称
        filter.setPasswordParameter("pwd");//参数名称
        return filter;
    }

    @Bean
    public LoginUrlAuthenticationEntryPoint getAuthenticationEntryPoint() {
        return new LoginUrlAuthenticationEntryPoint("/loginPage");
    }

    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler getSuccessHandler() {//认证成功
        SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setDefaultTargetUrl("/welcomePage");//成功页面
        return successHandler;
    }

    @Bean
    public SimpleUrlAuthenticationFailureHandler getFailureHandler() {//认证失败处理
        SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler();
        handler.setDefaultFailureUrl("/loginPage?error=true");//失败页面
        return handler;
    }

    @Bean
    public JdbcTokenRepositoryImpl getTokenRepository() {//持久化Cookie
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(this.dataSource);
        return tokenRepository;
    }

    @Bean
    public SessionInformationExpiredStrategy getSessionExpiredStrategy() {
        //Session失效策略,同时配置失效后的跳转路径
        return new SimpleRedirectSessionInformationExpiredStrategy("/logoffPage");
    }
}

本程序针对之前的配置,追加了RememberMe(包括数据库持久化)、验证码检测和并发Session访问控制。考虑到Spring开发的标准型,将所有可能使用到的对象以Bean的形式进行配置,随后根据需要进行注入。



配置投票管理器

SpringSecurity配置类提供了投票管理器,按照下面的顺序实现投票器的配置即可。本程序依然使用AffirmativeBased投票管理器,该投票管理器需要通过构造方法配置所有的投票器对象。

**范例:**配置投票管理器。

package com.xiyue.leaspring.config;

import com.xiyue.leaspring.filter.ValidatorCodeUsernamePasswordAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.RoleHierarchyVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/19 23:48:24
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)//启用注解配置
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;//数据源
    @Autowired
    private UserDetailsService userDetailsService;//用户服务
    @Autowired
    private SavedRequestAwareAuthenticationSuccessHandler successHandler;//成功页
    @Autowired
    private SimpleUrlAuthenticationFailureHandler failureHandler;//失败页
    @Autowired
    private SessionInformationExpiredStrategy sessionExpiredStrategy;//Session失效策略
    @Autowired
    private UsernamePasswordAuthenticationFilter authenticationFilter;//认证过滤器
    @Autowired
    private JdbcTokenRepositoryImpl tokenRepository;//token存储
    @Autowired
    private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private RoleHierarchy roleHierarchy;//角色继承

    @Autowired
    private SecurityExpressionHandler<FilterInvocation> expressionHandler;//表达式处理器

    @Autowired
    private AccessDecisionManager accessDecisionManager;//投票管理器

    @Bean
    public AccessDecisionManager getAccessDecisionManager() {
        //将所有用到的投票器设置到List集合中
        List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<>();
        decisionVoters.add(new RoleVoter());//角色投票器
        decisionVoters.add(new AuthenticatedVoter());//认证投票器
        decisionVoters.add(new RoleHierarchyVoter(this.roleHierarchy));//角色继承投票器
        WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
        webExpressionVoter.setExpressionHandler(this.expressionHandler);
        decisionVoters.add(webExpressionVoter);//表达式解析
        AffirmativeBased accessDecisionManager = new AffirmativeBased(decisionVoters);//定义投票管理器
        return accessDecisionManager;
    }

    @Bean
    public SecurityExpressionHandler<FilterInvocation> getExpressionHandler() {//配置表达式
        DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
        expressionHandler.setRoleHierarchy(this.roleHierarchy);//设置角色继承
        return expressionHandler;
    }

    @Bean
    public RoleHierarchy getRoleHierarchy() {//角色继承设置
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();//角色继承
        roleHierarchy.setHierarchy("ROLE_ADMIN>ROLE_USER");//继承关系
        return roleHierarchy;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //进行用户角色配置时不需要追加ROLE_前缀,系统会自动添加
//        auth.inMemoryAuthentication()//固定认证信息
//                .withUser("admin")//用户名
//                .password("{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16")//密码
//                .roles("USER","ADMIN");//角色
//        auth.inMemoryAuthentication()//固定认证信息
//                .withUser("xiyue")//用户名
//                .password("{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em")//密码
//                .roles("USER");//角色
        auth.userDetailsService(this.userDetailsService);//基于数据库认证
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();//禁用CSRF验证
//        //配置拦截路径的匹配地址与限定授权访问
//        http.authorizeRequests()//配置认证请求
//            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授权访问
//            .antMatchers("/welcomePage").authenticated()//认证访问
//            .antMatchers("/**").permitAll();//任意访问
//        //配置HTTP登录,注销与错误路径
//        http.formLogin()//登录配置
//            .usernameParameter("mid")//用户名参数设置
//            .passwordParameter("pwd")//用户密码参数设置
//            .successForwardUrl("/welcomePage")//登录成功路径
//            .loginPage("/loginPage")//登录表单页面
//            .loginProcessingUrl("/xylogin")//登录路径
//            .failureForwardUrl("/loginPage?error=true")//登录失败路径
//            .and()//配置连接
//            .logout()//注销配置
//            .logoutUrl("/xylogout")//注销路径
//            .logoutSuccessUrl("/logoutPage")//注销成功路径
//            .deleteCookies("JSESSIONID")//删除Cookie
//            .and()//配置连接
//            .exceptionHandling()//认证错误配置
//            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授权错误
        http.csrf().disable();//禁用CSRF验证
        http.httpBasic().authenticationEntryPoint(this.authenticationEntryPoint);
        //进行拦截路径的匹配地址配置与授权访问限定
        http.authorizeRequests()//配置认证请求
            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授权访问
            .antMatchers("/welcomePage").authenticated();//认证访问
        //进行http注销与错误路径配置,登录操作将由过滤器负责完成
        http.logout()//注销配置
            .logoutUrl("/xylogout")//注销路径
            .logoutSuccessUrl("/logoutPage")//注销成功路径
            .deleteCookies("JSESSIONID")//删除Cookie
            .and()//配置连接
            .exceptionHandling()//认证错误配置
            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授权错误页
        http.rememberMe()//开启RememberMe
            .rememberMeParameter("remember")//表单参数
            .key("xy-xiyue")//加密key
            .tokenValiditySeconds(2592000)//失效时间
            .rememberMeCookieName("xiyue-rememberme-cookie")//Cookie名称
            .tokenRepository(this.tokenRepository);//持久化
        http.sessionManagement()//Session管理
            .invalidSessionUrl("/loginPage")//失效路径
            .maximumSessions(1)//并发Session
            .expiredSessionStrategy(this.sessionExpiredStrategy);//失效策略
        http.addFilterBefore(this.authenticationFilter,UsernamePasswordAuthenticationFilter.class);//追加过滤器
    }
    @Override
    public void configure(WebSecurity web) throws Exception{
        web.ignoring().antMatchers("/index.jsp");//忽略的验证路径
    }

    @Bean
    public UsernamePasswordAuthenticationFilter getAuthenticationFilter() throws Exception{
        ValidatorCodeUsernamePasswordAuthenticationFilter filter = new
                ValidatorCodeUsernamePasswordAuthenticationFilter();
        filter.setAuthenticationManager(super.authenticationManager());//认证管理器
        filter.setAuthenticationSuccessHandler(this.successHandler);//登录成功页面
        filter.setAuthenticationFailureHandler(this.failureHandler);//登录失败页面
        filter.setFilterProcessesUrl("/xylogin");//登录路径
        filter.setUsernameParameter("mid");//参数名称
        filter.setPasswordParameter("pwd");//参数名称
        return filter;
    }

    @Bean
    public LoginUrlAuthenticationEntryPoint getAuthenticationEntryPoint() {
        return new LoginUrlAuthenticationEntryPoint("/loginPage");
    }

    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler getSuccessHandler() {//认证成功
        SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setDefaultTargetUrl("/welcomePage");//成功页面
        return successHandler;
    }

    @Bean
    public SimpleUrlAuthenticationFailureHandler getFailureHandler() {//认证失败处理
        SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler();
        handler.setDefaultFailureUrl("/loginPage?error=true");//失败页面
        return handler;
    }

    @Bean
    public JdbcTokenRepositoryImpl getTokenRepository() {//持久化Cookie
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(this.dataSource);
        return tokenRepository;
    }

    @Bean
    public SessionInformationExpiredStrategy getSessionExpiredStrategy() {
        //Session失效策略,同时配置失效后的跳转路径
        return new SimpleRedirectSessionInformationExpiredStrategy("/logoffPage");
    }
}
<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
    <context:component-scan base-package="com.xiyue.leaspring">
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.action"/>
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.dao"/>
    </context:component-scan>
    <context:property-placeholder location="classpath:database.properties"/>
    <!--<bean id="dataSource"
          class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${database.driverClass}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.user}"/>
        <property name="password" value="${database.password}"/>
    </bean>-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init">
        <property name="driverClassName" value="${database.druid.driverClassName}"/><!--驱动-->
        <property name="url" value="${database.druid.url}"/><!--地址-->
        <property name="username" value="${database.druid.username}"/><!--用户-->
        <property name="password" value="${database.druid.password}"/><!--密码-->
        <property name="maxActive" value="${database.druid.maxActive}"/><!--最大连接数-->
        <property name="minIdle" value="${database.druid.minIdle}"/><!--最小连接池-->
        <property name="initialSize" value="${database.druid.initialSize}"/><!--初始化连接大小-->
        <property name="maxWait" value="${database.druid.maxWait}"/><!--最大等待时间-->
        <property name="timeBetweenEvictionRunsMillis" value="${database.druid.timeBetweenEvictionRunsMillis}"/><!--检测空闲连接间隔-->
        <property name="minEvictableIdleTimeMillis" value="${database.druid.minEvictableIdleTimeMillsis}"/><!--连接最小生存时间-->
        <property name="validationQuery" value="${database.druid.validationQuery}"/><!--验证-->
        <property name="testWhileIdle" value="${database.druid.testWhileIdle}"/><!--申请检测-->
        <property name="testOnBorrow" value="${database.druid.testIOnBorrow}"/><!--有效检测-->
        <property name="testOnReturn" value="${database.druid.testIOnReturn}"/><!--归还检测-->
        <property name="poolPreparedStatements" value="${database.druid.poolPreparedStatements}"/><!--是否缓存preparedStatement,也就是PSCache。PSCache能提升支持游标的数据库性能,如Oracle、Mysql下建议关闭-->
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${database.druid.maxpoolPreparedStatementPerConnectionSize}"/><!--启用PSCache,必须配置大于0,当大于0时-->
        <property name="filters" value="${database.druid.filters}"/><!--驱动-->
    </bean>
    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/><!-- 数据源 -->
        <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/><!-- JPA核心配置文件 -->
        <property name="persistenceUnitName" value="LEA_SPRING_JPA"/><!-- 持久化单元名称 -->
        <property name="packagesToScan" value="com.xiyue.leaspring.dao"/><!-- PO类扫描包 -->
        <property name="persistenceProvider"><!-- 持久化提供类,本次为hibernate -->
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
        </property>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
        </property>
    </bean>
    <!-- 定义SpringDataJPA的数据层接口所在包,该包中的接口一定义是Repository子接口 -->
    <jpa:repositories base-package="com.xiyue.leaspring.dao"/>
    <!-- 定义事务管理的配置,必须配置PlatformTransactionManager接口子类 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <!--开启注解事务-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <security:authentication-manager><!--定义认证管理器-->
        <security:authentication-provider><!--配置认证管理配置类-->
            <security:jdbc-user-service
                    data-source-ref="dataSource"
                    users-by-username-query="select mid as username,password,enabled from member where mid=?"
                    authorities-by-username-query="select mid as username,rid as authorities from member_role where mid=?"/>
        </security:authentication-provider>
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>

    <security:authentication-manager id="authenticationManager"><!--定义Security认证管理器-->
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>
</beans>

由于投票管理器中需要考虑到表达式的支持,所以在本程序创建投票管理器对象时,依然在表达式配置类中注入了角色继承关系,最终的投票管理器需要通过HttpSecurity类对象完成配置。



版权声明:本文为sinat_27674731原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。