spring security技术分享

Spring Security技术专题

    • 一、初识认证和授权
      • 1.1 认证
      • 1.2 会话
      • 1.3 授权
      • 1.4 授权的数据模型
      • 1.5 RBAC
        • 1.5.1 角色访问控制
        • 1.5.2 资源访问控制
      • 1.6 小结
    • 二、基于Session的认证方式
      • 2.1 认证流程
      • 2.2 创建工程
        • 2.2.1 创建maven工程
        • 2.2.2 spring容器配置
        • 2.2.3 servletContext配置
        • 2.2.4 加载Spring容器
      • 2.3 实现认证功能
        • 2.3.1 认证页面
        • 2.3.2 认证接口
      • 2.4 实现会话功能
      • 2.5 实现授权功能
      • 2.6 小结
    • 三、初识Spring Security
      • 3.1 简介
      • 3.2 创建工程
        • 3.2.1 创建maven工程
        • 3.2.2 Spring容器配置
        • 3.2.4 Servlet Context配置
        • 3.2.4 加载Spring容器
      • 3.3 认证
        • 3.3.1 认证页面
        • 3.3.2 安全配置
        • 3.3.3 初始化
        • 3.3.4 默认根路径请求
        • 3.3.5 认证成功页面
        • 3.3.6 测试
      • 3.4 授权
    • 四、Spring Security
      • 4.1 集成 SpringBoot
        • 4.1.1 Spring Boot 简介
        • 4.1.2 创建maven工程
        • 4.1.3 spring容器配置
        • 4.1.4 Servlet Context配置
        • 4.1.5 安全配置
        • 4.1.6 测试
      • 4.2 工作原理
        • 4.2.1 结构总览
        • 4.2.2 认证流程
          • 4.2.2.1 AuthenticationProvider
          • 4.2.2.2 UserDetailsService
          • 4.2.2.3 PasswordEncoder
        • 4.2.3 授权流程
      • 4.3 自定义认证
        • 4.3.1 自定义登录页面
          • 4.3.1.1 认证页面
          • 4.3.1.2 配置认证页面
          • 4.3.1.3 安全配置
          • 4.3.1.4 测试
        • 4.3.2 连接数据库认证
          • 4.3.2.1 创建数据库
          • 4.3.2.2 代码实现
          • 4.3.2.3 定义 UserDetailService
          • 4.3.2.3 测试
          • 4.3.2.4 使用BCryptPasswordEncoder
      • 4.4 会话
        • 4.4.1 获取用户身份
        • 4.4.2 会话控制
      • 4.6 退出
      • 4.7 授权
        • 4.7.1 概述
        • 4.7.2 准备环境
          • 4.7.2.1 数据库环境
          • 4.7.2.2 修改 UserDetailService
        • 4.7.2 web授权
        • 4.7.3 方法授权



一、初识认证和授权

1.1 认证

1、什么是认证?

输入账号和密码 登录微信的过程就是认证。

2、为什么要认证?

认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

3、认证的定义?

用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。
认证是确认某主体在某系统中是否合法、可用的过程。这里的主体既可以是登录系统的用户,也可以是接入的设备或者其他系统。

4、常见的用户身份认证方式都有哪些?

用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。

1.2 会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

基于session的认证方式如下图:

spring security技术分享_第1张图片

它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的 session_id存放至cookie中,这样用户客户端请求时带上session_id就可以验证服务器端是否存在session数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

基于token方式如下图: spring security技术分享_第2张图片

它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到cookie或localstorage等存储中,每次请求时带上token,服务端收到token通过验证后即可确认用户身份。

基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持 cookie 。
基于token的方式则一般不需要服务端存储token ,并且不限制客户端的存储方式。
如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

1.3 授权

1、什么是授权?

微信群主解散微信群,这个根据用户的权限来控制用户使用资源的过程就是授权。

2、为什么要授权?

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。

3、授权的定义?

授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
授权是指当主体通过认证之后,是否允许其执行某项操作的过程。

1.4 授权的数据模型

一句话可以总结授权的数据模型:主体对资源进行权限操作
这里的主体既可以是登录系统的用户,也可以是接入的设备或者其他系统。

spring security技术分享_第3张图片

1.5 RBAC

1.5.1 角色访问控制

RBAC基于角色的访问控制(Role-Based Access Control),是按角色进行授权。

举个例子,部长可以分配任务:

if(user.hasRole("部长角色id")){
	分配任务;
}

若此时分配任务的角色产生变化,变为部长和产品经理,那么就要修改代码,如下:

if(user.hasRole("部长角色id")||user.hasRole("产品经理角色id")){
	分配任务;
}

由此可见,当根据角色进行授权时,需要修改代码,系统的可扩展性差。

1.5.2 资源访问控制

RBAC基于资源的访问控制(Resource-Based Access Control),是按资源(或权限)进行授权。

举个例子:用户必须具有分配任务权限才可以给员工分配任务:

if(user.hasPermission("分配任务权限标识")){
	分配任务;
}

由此可见,只要系统设计时定义好分配任务的权限标识,即使分配任务所需要的角色产生变化也不需要修改代码,系统可扩展性强。

1.6 小结

以上的这些概念,也并非Spring Security独有,而是应用安全的基本关注点,Spring Security可以帮助我们更加快捷的完成认证和授权。很多时候,一个系统的安全性完全取决于系统开发人员的安全意识。例如,在我们从未听过SQL注入的时候,如何意识到要对SQL注入做防护?关于Web系统安全的攻击方式非常多,诸如:XSS、CSRF等,未来还会暴露出更多的攻击方式,我们只有在充分的了解其攻击原理后,才能提出完善而有效策略。在笔者看来,学习Spring Security并非局限于降低java应用的安全开发成本,通过Spring Security了解常见的安全攻击手段以及对应的防护手段也尤为重要,这些是脱离具体开发语言而存在的。

二、基于Session的认证方式

2.1 认证流程

基于Session认证方式的流程:

用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话),而发给客户端的sesssion_id存放到cookie中,这样用客户端请求时带上sesssion_id就可以验证服务器端是否存在session数据,以此完成用户的合法校验。当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

下图是session认证方式的流程图:
spring security技术分享_第4张图片

基于Session的认证机制由Servlet规范定制,Servlet容器已实现,用户通过HttpSession的操作方法即可实现。

如下是HttpSession相关的操作API。

方法 含义
HttpSession getSession(Boolean create) 获取当前HttpSession对象
void setAttribute(String name,Object value) 向session中存放对象
object getAttribute(String name) 从session中获取对象
void removeAttribute(String name) 移除session中对象
void invalidate() 使 HttpSession 失效

2.2 创建工程

工程环境:使用maven进行构建,使用SpringMVC、Servlet3.0实现。

2.2.1 创建maven工程

工程结构 spring security技术分享_第5张图片

引入依赖如下:
1、 由于是web工程,packaging设置为war
2、 使用tomcat7-maven-plugin插件来运行工程


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.unclegroupId>
    <artifactId>security-springmvcartifactId>
    <version>1.0-SNAPSHOTversion>
    <packaging>warpackaging>

    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <maven.compiler.source>1.8maven.compiler.source>
        <maven.compiler.target>1.8maven.compiler.target>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-webmvcartifactId>
            <version>5.1.5.RELEASEversion>
        dependency>

        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>javax.servlet-apiartifactId>
            <version>3.0.1version>
            <scope>providedscope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.8version>
        dependency>
    dependencies>
    <build>
        <finalName>security-springmvcfinalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.mavengroupId>
                    <artifactId>tomcat7-maven-pluginartifactId>
                    <version>2.2version>
                plugin>
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-compiler-pluginartifactId>
                    <configuration>
                        <source>1.8source>
                        <target>1.8target>
                    configuration>
                plugin>

                <plugin>
                    <artifactId>maven-resources-pluginartifactId>
                    <configuration>
                        <encoding>utf-8encoding>
                        <useDefaultDelimiters>trueuseDefaultDelimiters>
                        <resources>
                            <resource>
                                <directory>src/main/resourcesdirectory>
                                <filtering>truefiltering>
                                <includes>
                                    <include>**/*include>
                                includes>
                            resource>
                            <resource>
                                <directory>src/main/javadirectory>
                                <includes>
                                    <include>**/*.xmlinclude>
                                includes>
                            resource>
                        resources>
                    configuration>
                plugin>
            plugins>
        pluginManagement>
    build>

project>

运行项目方式:

spring security技术分享_第6张图片

2.2.2 spring容器配置

在config包下定义ApplicationConfig.java,它对应web.xml中ContextLoaderListener的配置。

package com.uncle.security.springmvc.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:26
 */
@Configuration
@ComponentScan(basePackages = "com.uncle.security.springmvc"
        ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
public class ApplicationConfig {

}

2.2.3 servletContext配置

本案例采用Servlet3.0无web.xml方式,在config包下定义WebConfig.java,它对应DispatcherServlet配置。

package com.uncle.security.springmvc.config;

import com.uncle.security.springmvc.interceptor.SimpleAuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:34
 */
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.uncle.security.springmvc"
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}


2.2.4 加载Spring容器

在init包下定义Spring容器初始化类SpringApplicationlnitializer,此类实现WebApplicationlnitializer接口
, Spring容器启动时加载WebApplicationlnitializer接口的所有实现类。

package com.uncle.security.springmvc.init;

import com.uncle.security.springmvc.config.ApplicationConfig;
import com.uncle.security.springmvc.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:47
 */
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //spring容器,相当于加载 applicationContext.xml
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class};
    }

    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    //url-mapping
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

SpringApplicationlnitializer相当于web.xml ,使用了servlet3.0开发则不需要再定义web.xml,ApplicationConfig.class对应以下配置的application-context.xml ,WebConfig.class对应以下配置的spring-mvc.xml , web.xml的内容参考:

<web-app>
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class> listener>
	<context-param>
		<param-name>contextConfigLocationparam-name>
		<param-value>/WEB-INF/application-context .xmlparam-value>
	context-param>

	<servlet>
		<servlet-name>springmvcservlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class> 
			<init-param>
				<param-name>contextConfigLocationparam-name> <param-value>/WEB-INF/spring-mvc.xmlparam-value>
			init-param>
		<load-on-startup>lload-on-startup>
	servlet>
	<servlet-mapping>
		<servlet-name>springmvcservlet-name>
		<url-pattern>/url-pattern>
	servlet-mapping>
web-app>

2.3 实现认证功能

2.3.1 认证页面

在webapp/WEB-INF/views下定义认证页面login.jsp,页面实 现可填入用户名 密码,触发登录将提交表单信息至/login ,内容如下:

<%--
  Created by IntelliJ IDEA.
  User: uncle
  Date: 2021/7/22
  Time: 下午9:43
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录title>
head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>   码:
    <input type="password" name="password"><br>
    <input type="submit" value="登录">
form>
body>
html>

在WebConfig中新增如下配置,将/直接导向login.jsp页面:

package com.uncle.security.springmvc.config;

import com.uncle.security.springmvc.interceptor.SimpleAuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:34
 */
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.uncle.security.springmvc"
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
    }
}


启动项目,访问/路径地址,进行测试

启动


点击控制台生成的地址

spring security技术分享_第7张图片

2.3.2 认证接口

用户进入认证页面,输入账号和密码,点击登录,请求/login进行身份认证。

定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常:

package com.uncle.security.springmvc.service;

import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:22
 */
public interface AuthenticationService {
    /**
     * 用户认证
     * @param authenticationRequest 用户认证请求,账号和密码
     * @return 认证成功的用户信息
     */
    UserDto authentication(AuthenticationRequest authenticationRequest);
}

认证请求结构:

package com.uncle.security.springmvc.model;

import lombok.Data;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:25
 */
@Data
public class AuthenticationRequest {
    //认证请求参数,账号、密码。
    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

}

认证成功后返回的用户详细信息,也就是当前登录用户的信息:

package com.uncle.security.springmvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:25
 */
@Data
@AllArgsConstructor
public class UserDto {
    //用户身份信息
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

认证实现类,根据用户名查找用户信息,并校验密码,这里模拟了两个用户:

package com.uncle.security.springmvc.service;

import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:27
 */
@Service
public class AuthenticationServiceImpl implements  AuthenticationService{
    /**
     * 用户认证,校验用户身份信息是否合法
     *
     * @param authenticationRequest 用户认证请求,账号和密码
     * @return 认证成功的用户信息
     */
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
        //校验参数是否为空
        if(authenticationRequest == null
                || StringUtils.isEmpty(authenticationRequest.getUsername())
                || StringUtils.isEmpty(authenticationRequest.getPassword())){
            throw new RuntimeException("账号和密码为空");
        }
        //根据账号去查询数据库,这里测试程序采用模拟方法
        UserDto user = getUserDto(authenticationRequest.getUsername());
        //判断用户是否为空
        if(user == null){
            throw new RuntimeException("查询不到该用户");
        }
        //校验密码
        if(!authenticationRequest.getPassword().equals(user.getPassword())){
            throw new RuntimeException("账号或密码错误");
        }
        //认证通过,返回用户身份信息
        return user;
    }
    //根据账号查询用户信息
    private UserDto getUserDto(String userName){
        return userMap.get(userName);
    }
    //用户信息
    private Map<String,UserDto> userMap = new HashMap<>();
    {
        userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443"));
        userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553"));
    }
}

登录Controller,对/login请求处理,它调用AuthenticationService完成认证并返回登录结果提示信息:

package com.uncle.security.springmvc.controller;

import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import com.uncle.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:33
 */
@RestController
public class LoginController {

    @Autowired
    AuthenticationService authenticationService;

    @RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest, HttpSession session){
    
        UserDto userDto = authenticationService.authentication(authenticationRequest);
        return userDto.getUsername() +"登录成功";
    }
}

启动项目,访问/路径地址,进行测试

启动
点击控制台生成的地址 spring security技术分享_第8张图片

填入错误的用户信息,页面返回错误信息:

填入正确的用户信息,页面提示登录成功: spring security技术分享_第9张图片

以上的测试全部符合预期,到目前为止最基础的认证功能已经完成,它仅仅实现了对用户身份凭证的校验,若某用户认证成功,只能说明他是该系统的一个合法用户,仅此而已。

2.4 实现会话功能

会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。认证的目的是对系统资源的保护,每次对资源的访问,系统必须得知道是谁在访问资源,才能对该请求进行合法性拦截。因此,在认证成功后,一般会把认证成功的用户信息放入Session中,在后续的请求中,系统能够从Session中获取到当前用户,用这样的方式来实现会话机制。

1、增加会话控制
首先在UserDto中定义一个SESSION_USER_KEY ,作为Session中存放登录用户信息的key。

package com.uncle.security.springmvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:25
 */
@Data
@AllArgsConstructor
public class UserDto {
    public static final String SESSION_USER_KEY = "_user";
    //用户身份信息
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;

}

然后修改Logincontroller,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为失效。

package com.uncle.security.springmvc.controller;

import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import com.uncle.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:33
 */
@RestController
public class LoginController {

    @Autowired
    AuthenticationService authenticationService;

    @RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest, HttpSession session){
        UserDto userDto = authenticationService.authentication(authenticationRequest);
        //存入session
        session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
        return userDto.getUsername() +"登录成功";
    }

    @GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
    public String logout(HttpSession session){
        session.invalidate();
        return "退出成功";
    }
}

2、增加测试资源
修改Logincontroller ,增加测试资源1 ,它从当前会话session中获取当前登录用户,并返回提示信息给前台。

package com.uncle.security.springmvc.controller;

import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import com.uncle.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:33
 */
@RestController
public class LoginController {

    @Autowired
    AuthenticationService authenticationService;

    @RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest, HttpSession session){
        UserDto userDto = authenticationService.authentication(authenticationRequest);
        //存入session
        session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
        return userDto.getUsername() +"登录成功";
    }

    @GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
    public String logout(HttpSession session){
        session.invalidate();
        return "退出成功";
    }

    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(HttpSession session){
        String fullname = null;
        Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
        if(object == null){
            fullname = "匿名";
        }else{
            UserDto userDto = (UserDto) object;
            fullname = userDto.getFullname();
        }
        return fullname+"访问资源r1";
    }
}

3、测试

未登录情况下直接访问测试资源/r/r1 :
spring security技术分享_第10张图片

成功登录的情况下访问测试资源/r/r1 :
spring security技术分享_第11张图片
spring security技术分享_第12张图片
退出,访问地址/logout
spring security技术分享_第13张图片
spring security技术分享_第14张图片

测试结果说明,在用户登录成功时,该用户信息已被成功放入session ,并且后续请求可以正常从session中获取当前登录用户信息,退出时,session失效,再次访问为匿名访问,符合预期结果。

2.5 实现授权功能

现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户(从Session中获取)的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:

  • 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
  • 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。

1、增加权限数据
为了实现这样的功能,我们需要在UserDto里增加权限属性,用于表示该登录用户所拥有的权限,同时修改 UserDto的构造方法。

package com.uncle.security.springmvc.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:25
 */
@Data
@AllArgsConstructor
public class UserDto {
    public static final String SESSION_USER_KEY = "_user";
    //用户身份信息
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
    /**
     * 用户权限
     */
    private Set<String> authorities;
}

并在AuthenticationServicelmpI中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限。

package com.uncle.security.springmvc.service;

import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:27
 */
@Service
public class AuthenticationServiceImpl implements  AuthenticationService{
    /**
     * 用户认证,校验用户身份信息是否合法
     *
     * @param authenticationRequest 用户认证请求,账号和密码
     * @return 认证成功的用户信息
     */
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
        //校验参数是否为空
        if(authenticationRequest == null
                || StringUtils.isEmpty(authenticationRequest.getUsername())
                || StringUtils.isEmpty(authenticationRequest.getPassword())){
            throw new RuntimeException("账号和密码为空");
        }
        //根据账号去查询数据库,这里测试程序采用模拟方法
        UserDto user = getUserDto(authenticationRequest.getUsername());
        //判断用户是否为空
        if(user == null){
            throw new RuntimeException("查询不到该用户");
        }
        //校验密码
        if(!authenticationRequest.getPassword().equals(user.getPassword())){
            throw new RuntimeException("账号或密码错误");
        }
        //认证通过,返回用户身份信息
        return user;
    }
    //根据账号查询用户信息
    private UserDto getUserDto(String userName){
        return userMap.get(userName);
    }
    //用户信息
    private Map<String,UserDto> userMap = new HashMap<>();
    {
        Set<String> authorities1 = new HashSet<>();
        authorities1.add("p1");//这个p1我们人为让它和/r/r1对应
        Set<String> authorities2 = new HashSet<>();
        authorities2.add("p2");//这个p2我们人为让它和/r/r2对应
        userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
        userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
    }
}

2、增加测试资源
我们想实现针对不同的用户能访问不同的资源,前提是得有多个资源,因此在Logincontroller中增加测试资源2。

package com.uncle.security.springmvc.controller;

import com.uncle.security.springmvc.model.AuthenticationRequest;
import com.uncle.security.springmvc.model.UserDto;
import com.uncle.security.springmvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:33
 */
@RestController
public class LoginController {

    @Autowired
    AuthenticationService authenticationService;

    @RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest, HttpSession session){
        UserDto userDto = authenticationService.authentication(authenticationRequest);
        //存入session
        session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
        return userDto.getUsername() +"登录成功";
    }

    @GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
    public String logout(HttpSession session){
        session.invalidate();
        return "退出成功";
    }

    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(HttpSession session){
        String fullname = null;
        Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
        if(object == null){
            fullname = "匿名";
        }else{
            UserDto userDto = (UserDto) object;
            fullname = userDto.getFullname();
        }
        return fullname+"访问资源r1";
    }
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(HttpSession session){
        String fullname = null;
        Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY);
        if(userObj != null){
            fullname = ((UserDto)userObj).getFullname();
        }else{
            fullname = "匿名";
        }
        return fullname + " 访问资源2";
    }
}

3、实现授权拦截器
在interceptor包下定义SimpleAuthenticationlnterceptor拦截器,实现授权拦截:

  • 校验用户是否登录
  • 校验用户是否拥有操作权限
package com.uncle.security.springmvc.interceptor;

import com.uncle.security.springmvc.model.UserDto;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:53
 */
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //在这个方法中校验用户请求的url是否在用户的权限范围内
        //取出用户身份信息
        Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
        if(object == null){
            //没有认证,提示登录
            writeContent(response,"请登录");
        }
        UserDto userDto = (UserDto) object;
        //请求的url
        String requestURI = request.getRequestURI();
        if( userDto.getAuthorities().contains("p1") && requestURI.contains("/r/r1")){
            return true;
        }
        if( userDto.getAuthorities().contains("p2") && requestURI.contains("/r/r2")){
            return true;
        }
        writeContent(response,"没有权限,拒绝访问");

        return false;
    }

    //响应信息给客户端
    private void writeContent(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.print(msg);
        writer.close();
    }
}

在WebConfig中配置拦截器,匹配/r/**的资源为受保护的系统资源,访问该资源的请求进入SimpleAuthenticationInterceptor 拦截器。

package com.uncle.security.springmvc.config;

import com.uncle.security.springmvc.interceptor.SimpleAuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:34
 */
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.uncle.security.springmvc"
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
    }
}


4、测试

未登录情况下,/r/r1与/r/r2均提示"请先登录
spring security技术分享_第15张图片
spring security技术分享_第16张图片

张三登录情况下,由于张三有p1权限,因此可以访问/r/r1 ,张三没有p2权限,访问/r/r2时提示"权限不足"。
spring security技术分享_第17张图片
spring security技术分享_第18张图片
spring security技术分享_第19张图片
spring security技术分享_第20张图片

同理,李四登录情况下,由于李四有p2权限,因此可以访问/r/r2,李四没有p1权限,访问/r/r1时提示"权限不足"。(图略过)
测试结果全部符合预期结果。

2.6 小结

基于Session的认证方式是一种常见的认证方式,至今还有非常多的系统在使用。我们在此小节使用Springmvc技术对它进行简单实现,旨在让大家更清晰实在的了解用户认证、授权以及会话的功能意义及实现套路,也就是它们分别干了哪些事儿?大概需要怎么做?而在正式生产项目中,我们往往会考虑使用第三方安全框架(如spring security ,shiro等安全框架)来实现认证授权功能,因为这样做能一定程度提高生产力,提高软件标准化程度,另外往往这些框架的可扩展性考虑的非常全面。但是缺点也非常明显,这些通用化组件为了提高支持范围会增加很多可能我们不需要的功能,结构上也会比较抽象,如果我们不够了解它,一旦出现问题,将会很难定位。

三、初识Spring Security

3.1 简介

SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在springboot项目中加入Spring Security更是十分简单,使用SpringSecurity减少了为企业系统安全控制编写大量重复代码的工作。

Spring Security的前身是Acegi Security,在被收纳为Spring子项目后正式更名为SpringSecurity。笔者在完成这篇文章时,SpringSecurity已经升级到了5.5.1版本(5.3.10.RELEASE),SpringSecurity5.x以后不仅增加了原生的OAuth框架,还支持更加现代化的密码加密方式,可以预见,在java应用安全领域,SpringSecurity会成为首先被推崇的安全解决方案。

3.2 创建工程

3.2.1 创建maven工程

创建maven工程 ,工程结构如下:
spring security技术分享_第21张图片

引入以下依赖:


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.unclegroupId>
    <artifactId>spring-security-springmvcartifactId>
    <version>1.0-SNAPSHOTversion>
    <packaging>warpackaging>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <maven.compiler.source>1.8maven.compiler.source>
        <maven.compiler.target>1.8maven.compiler.target>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-webartifactId>
            <version>5.1.4.RELEASEversion>
        dependency>

        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-configartifactId>
            <version>5.1.4.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-webmvcartifactId>
            <version>5.1.5.RELEASEversion>
        dependency>

        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>javax.servlet-apiartifactId>
            <version>3.0.1version>
            <scope>providedscope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.8version>
        dependency>
    dependencies>
    <build>
        <finalName>security-springmvcfinalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.mavengroupId>
                    <artifactId>tomcat7-maven-pluginartifactId>
                    <version>2.2version>
                plugin>
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-compiler-pluginartifactId>
                    <configuration>
                        <source>1.8source>
                        <target>1.8target>
                    configuration>
                plugin>

                <plugin>
                    <artifactId>maven-resources-pluginartifactId>
                    <configuration>
                        <encoding>utf-8encoding>
                        <useDefaultDelimiters>trueuseDefaultDelimiters>
                        <resources>
                            <resource>
                                <directory>src/main/resourcesdirectory>
                                <filtering>truefiltering>
                                <includes>
                                    <include>**/*include>
                                includes>
                            resource>
                            <resource>
                                <directory>src/main/javadirectory>
                                <includes>
                                    <include>**/*.xmlinclude>
                                includes>
                            resource>
                        resources>
                    configuration>
                plugin>
            plugins>
        pluginManagement>
    build>


project>

3.2.2 Spring容器配置

package com.uncle.security.springmvc.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:26
 */
@Configuration
@ComponentScan(basePackages = "com.uncle.security.springmvc"
        ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
public class ApplicationConfig {

}


3.2.4 Servlet Context配置

package com.uncle.security.springmvc.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:34
 */
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.uncle.security.springmvc"
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {

    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }

}


此处插一句题外话,有人问我这个配置什么意思

@Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }

这个配置确实不常见,但是确实有很大用处,其实理解起来也很容易,上面的写法等同于如下的写法

@Controller
public class TestContrller {
    
    @RequestMapping("/test")
    public String test() {
        return "redirect:/login";
    }
}

这时可能会有同学问,如果当redirect后的字符串同时存在于url和页面会重定向哪里,经笔者验证后,答案是页面的优先级会更高,感兴趣的读者可以自行去验证,笔者就不再赘述。

3.2.4 加载Spring容器

在init包下定义Spring容器初始化类SpringApplicationlnitializer,此类实现WebApplicationlnitializer接口, Spring容器启动时加载WebApplicationlnitializer接口的所有实现类。

package com.uncle.security.springmvc.init;

import com.uncle.security.springmvc.config.ApplicationConfig;
import com.uncle.security.springmvc.config.WebConfig;
import com.uncle.security.springmvc.config.WebSecurityConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:47
 */
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //spring容器,相当于加载 applicationContext.xml
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
    }

    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    //url-mapping
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

3.3 认证

3.3.1 认证页面

SpringSecurity默认提供认证页面。
spring security技术分享_第22张图片

3.3.2 安全配置

spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。

在config包下定义WebSecurityConfig ,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。

package com.uncle.security.springmvc.config;

import org.springframework.context.annotation.Bean;
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;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @program: spring-security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 00:41
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //定义用户信息服务(查询用户信息)
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}

在userDetailsService()方法中,我们返回了一个UserDetailsService给spring容器,Spring
Security会使用它来获取用户信息。我们暂时使用InMemoryUserDetailsManager实现类,并在其中分别创建了zhangsan、lisi两个用户,并设置密码和权限。

而在configure()中,我们通过HttpSecurity设置了安全拦截规则,其中包含了以下内容:

  • url匹配"/**的资源,经过认证后才能访问。
  • 其他url完全开放。
  • 支持form表单认证,认证成功后转向/login-success。

加载 WebSecurityConfig

修改SpringApplicationlinitializer的getRootConfigClasses()方法,添加WebSecurityConfig.class:

package com.uncle.security.springmvc.init;

import com.uncle.security.springmvc.config.ApplicationConfig;
import com.uncle.security.springmvc.config.WebConfig;
import com.uncle.security.springmvc.config.WebSecurityConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:47
 */
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //spring容器,相当于加载 applicationContext.xml
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
    }

    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    //url-mapping
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

3.3.3 初始化

Spring Security初始化,这里有两种情况

  • 若当前环境没有使用Spring或Spring MVC ,则需要将WebSecurityConfig(Spring Security配置类)传入超类,以确保获取配置,并创建spring context。
  • 相反,若当前环境已经使用spring ,我们应该在现有的springContext中注册Spring Security(上一步已经将 WebSecurityConfig加载至rootcontext),此方法可以什么都不做。
package com.uncle.security.springmvc.init;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

/**
 * @program: spring-security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 00:46
 */
@EnableWebSecurity
public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
    public SpringSecurityApplicationInitializer() {
        //super(WebSecurityConfig.class);
    }
}

3.3.4 默认根路径请求

在WebConfig.java中添加默认请求根路径跳转到/login ,此url为spring security提供:

package com.uncle.security.springmvc.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 21:34
 */
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.uncle.security.springmvc"
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {

    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }

}


spring security默认提供的登录页面。

spring security技术分享_第23张图片

3.3.5 认证成功页面

在安全配置中,认证成功将跳转到/login-success ,代码如下:

package com.uncle.security.springmvc.config;

import org.springframework.context.annotation.Bean;
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;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @program: spring-security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 00:41
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //定义用户信息服务(查询用户信息)
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}

spring security支持form表单认证,认证成功后转向/login-success。

在 Logincontroller 中定义/login-success:

package com.uncle.security.springmvc.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:33
 */
@RestController
public class LoginController {

    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        return " 登录成功";
    }

}

3.3.6 测试

启动项目,访问http://localhost:8080/spring-security-springmvc路径地址
spring security技术分享_第24张图片

页面会根据WebConfig中addViewControllers配置规则,跳转至/login , /login是Spring
Security提供的登录页面。

登录

输入错误的用户名、密码
spring security技术分享_第25张图片

输入正确的用户名、密码,登录成功
spring security技术分享_第26张图片

退出

请求/logout退出 spring security技术分享_第27张图片
spring security技术分享_第28张图片

退出后再访问资源自动跳转到登录页面

3.4 授权

实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security默认提供授权实现方法。

在LoginController 添加/r/r1 或/r/r2

package com.uncle.security.springmvc.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

/**
 * @program: security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-22 23:33
 */
@RestController
public class LoginController {

    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        return " 登录成功";
    }

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(){
        return " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(){
        return " 访问资源2";
    }
}

在安全配置类WebSecurityConfig.java中配置授权规则:

package com.uncle.security.springmvc.config;

import org.springframework.context.annotation.Bean;
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;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @program: spring-security-springmvc
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 00:41
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //定义用户信息服务(查询用户信息)
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}

测试

登录成功
spring security技术分享_第29张图片
spring security技术分享_第30张图片
spring security技术分享_第31张图片

访问/r/r1和/r/r2 ,有权限时则正常访问,否则返回403 (拒绝访问)

四、Spring Security

4.1 集成 SpringBoot

4.1.1 Spring Boot 简介

Spring Boot是一套Spring的快速开发框架,基于Spring 4.0设计,使用Spring Boot开发可以避免一些繁琐的工程配置,同时它集成了大量的常用框架,快速导入依赖包,避免依赖包的冲突。基本上常用的开发框架都支持 SpringBoot开发,例如:MyBatis、Dubbo等,Spring 家族更是如此,例如:Spring Cloud、Spring mvc、Spring Security等,使用Spring Boot开发可以大大得高生产率,所以Spring Boo的使用率非常高。

本节讲解如何通过Spring Boot开发Spring Security应用,SpringBoot提供spring-boot-starter-security用于开发Spring Security应用。

4.1.2 创建maven工程

创建maven工程结构如下:
spring security技术分享_第32张图片

引入以下依赖


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.unclegroupId>
    <artifactId>spring-boot-securityartifactId>
    <version>1.0-SNAPSHOTversion>

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.3.RELEASEversion>
    parent>

    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <maven.compiler.source>1.8maven.compiler.source>
        <maven.compiler.target>1.8maven.compiler.target>
    properties>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>


        
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>javax.servlet-apiartifactId>
            <scope>providedscope>
        dependency>
        
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>jstlartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-tomcatartifactId>
            <scope>providedscope>
        dependency>
        
        <dependency>
            <groupId>org.apache.tomcat.embedgroupId>
            <artifactId>tomcat-embed-jasperartifactId>
            <scope>providedscope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.0version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.47version>
        dependency>
    dependencies>
    <build>
        <finalName>security-springbootfinalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.mavengroupId>
                    <artifactId>tomcat7-maven-pluginartifactId>
                    <version>2.2version>
                plugin>
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-compiler-pluginartifactId>
                    <configuration>
                        <source>1.8source>
                        <target>1.8target>
                    configuration>
                plugin>

                <plugin>
                    <artifactId>maven-resources-pluginartifactId>
                    <configuration>
                        <encoding>utf-8encoding>
                        <useDefaultDelimiters>trueuseDefaultDelimiters>
                        <resources>
                            <resource>
                                <directory>src/main/resourcesdirectory>
                                <filtering>truefiltering>
                                <includes>
                                    <include>**/*include>
                                includes>
                            resource>
                            <resource>
                                <directory>src/main/javadirectory>
                                <includes>
                                    <include>**/*.xmlinclude>
                                includes>
                            resource>
                        resources>
                    configuration>
                plugin>
            plugins>
        pluginManagement>
    build>


project>

4.1.3 spring容器配置

SpringBoot工程启动会自动扫描启动类所在包下的所有Bean,加载到spring容器。

Spring Boot配置文件

在resources下添加application.yml,内容如下:

server:
  #端口
  port: 8080
  #应用的上下文路径,也可以称为项目路径,是构成url地址的一部分
  servlet:
    context-path: /spring-boot-security
    
#项目名
spring:
  application:
    name: spring-boot-security


#默认的配置为/templates/和.html
#这里笔者就不用jsp了,前面用jsp旨在让读者理解配置前缀和后缀
#spring.mvc.view.prefix=/WEB-INF/view/
#spring.mvc.view.suffix=.jsp

Spring Boot 启动类

package com.uncle.seciruty.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @program: spring-boot-security
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 19:35
 */
@SpringBootApplication
public class SecuritySpringBootApp {
    public static void main(String[] args) {
        SpringApplication.run(SecuritySpringBootApp.class,args);
    }
}

4.1.4 Servlet Context配置

由于Spring boot starter自动装配机制,这里无需使用@EnableWebMvc与@ComponentScan

WebConfig如下

package com.uncle.seciruty.springboot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @program: spring-boot-security
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 19:38
 */
@Configuration//就相当于springmvc.xml文件
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }

}


关于视图解析器

#默认的配置为/templates/和.html
#这里笔者就不用jsp了,前面用jsp旨在让读者理解视图解析器的配置
#spring.mvc.view.prefix=/WEB-INF/view/
#spring.mvc.view.suffix=.jsp

4.1.5 安全配置

由于Spring boot starter自动装配机制,这里无需使用@EnableWebSecurity

WebSecurityConfig内容如下


@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //定义用户信息服务(查询用户信息)
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}

4.1.6 测试

Logincontroller的内容

package com.uncle.seciruty.springboot.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: spring-boot-security
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 19:41
 */
@RestController
public class LoginController {

    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        //提示具体用户名称登录成功
        return getUsername()+" 登录成功";
    }

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(){
        return getUsername()+" 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(){
        return getUsername()+" 访问资源2";
    }
}

测试过程

不出意外的话,此时应该是报错了
spring security技术分享_第33张图片

原因:

这是因为添加了数据库组件,所以autoconfig会去读取数据源配置,而新建的项目还没有配置数据源URL地址错误,所以会导致异常出现。

解决方案:
在启动类的@EnableAutoConfiguration或@SpringBootApplication中添加exclude ={DataSourceAutoConfiguration.class},排除此类的autoconfig,启动以后就可以正常运行。

package com.uncle.seciruty.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

/**
 * @program: spring-boot-security
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 19:35
 */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class SecuritySpringBootApp {
    public static void main(String[] args) {
        SpringApplication.run(SecuritySpringBootApp.class,args);
    }
}

接下来,我们开始正式测试

1、测试认证

spring security技术分享_第34张图片
spring security技术分享_第35张图片
spring security技术分享_第36张图片
spring security技术分享_第37张图片

2、测试退出 spring security技术分享_第38张图片
spring security技术分享_第39张图片

3、测试授权 spring security技术分享_第40张图片

4.2 工作原理

4.2.1 结构总览

Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。

当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为org.springframework.security.web.FilterchainProxy ,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过滤器链结构图:

spring security技术分享_第41张图片

FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的关系图:

spring security技术分享_第42张图片

spring Security功能的实现主要是由一系列过滤器链相互配合完成

下面介绍过滤器链中主要的几个过滤器及其作用:

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给SecurityContextHolder,在请求完成后将SecurityContextHolder持有的Securitycontext再保存到配置好的 SecurityContextRepository ,同时清除SecurityContextHolder 所持有的SecurityContext

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和
AuthenticationFailureHandler,这些都可以根据需求做相关改变

Filtersecurityinterceptor

Filtersecurityinterceptor是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问

ExceptionTranslationFilter

ExceptionTranslationFilter能够捕获来自FilterChain所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException和 AccessDeniedException ,其它的异常它会继续抛出。

4.2.2 认证流程

流程图
spring security技术分享_第43张图片

认证过程分析:

1.用户提交用户名 密码被 SecurityFilterChain 中的UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication ,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

2.然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证

3.认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除)Authentication实例。

4.SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口 ,也是发起认证的出发点,它的实现类为ProviderManager。而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。

认证核心组件的大体关系如下:
spring security技术分享_第44张图片

4.2.2.1 AuthenticationProvider

通过前面的Spring Security认证流程我们得知,认证管理器(AuthenticationManager)委托 AuthenticationProvider 完成认证工作。

AuthenticationProvider是一个接口 ,定义如下:

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

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

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

    boolean supports(Class<?> var1);
}

authenticate()方法定义了认证的实现过程,它的参数是一个Authentication ,里面包含了登录用户所提交的用户 密码等。而返回值也是一个Authentication ,这个Authentication则是在认证成功后,将用户的权限及其他信息重新组装后生成。

Spring Security中维护着一个List列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProviderl ,短信登录时使用 Authentication Provider2等等这样的例子很多。

每个AuthenticationProvider需要实现supports ()方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken ,它是一个Authentication ,里面封装着用户提交的用户名、密码信息。

而对应哪个AuthenticationProvider来处理它?

我们在 DaoAuthenticationProvider的基类 AbstractUserDetailsAuthenticationProvider 发现以下代码:

    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }//149行

也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理。

最后,我们来看一下Authentication(认证信息)的结构,它是一个接口 ,我们之前提到的 UsernamePasswordAuthenticationToken就是它的实现之一:

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

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

public interface Authentication extends Principal, Serializable {//1
    Collection<? extends GrantedAuthority> getAuthorities();//2

    Object getCredentials();//3

    Object getDetails();//4

    Object getPrincipal();//5

    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

(1 ) Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于 java.security 包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。

(2 ) getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。

(3 ) getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。

(4 ) getDetails(),细节信息,web应用中的实现接口通常为WebAuthenticationDetails ,它记录了访问者的ip地址和sessionld的值。

(5 ) getPrincipal(),身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。

附参考代码:

/*
 * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 */

package java.security;

import javax.security.auth.Subject;

/**
 * This interface represents the abstract notion of a principal, which
 * can be used to represent any entity, such as an individual, a
 * corporation, and a login id.
 *
 * @see java.security.cert.X509Certificate
 *
 * @author Li Gong
 */
public interface Principal {

    /**
     * Compares this principal to the specified object.  Returns true
     * if the object passed in matches the principal represented by
     * the implementation of this interface.
     *
     * @param another principal to compare with.
     *
     * @return true if the principal passed in is the same as that
     * encapsulated by this principal, and false otherwise.
     */
    public boolean equals(Object another);

    /**
     * Returns a string representation of this principal.
     *
     * @return a string representation of this principal.
     */
    public String toString();

    /**
     * Returns a hashcode for this principal.
     *
     * @return a hashcode for this principal.
     */
    public int hashCode();

    /**
     * Returns the name of this principal.
     *
     * @return the name of this principal.
     */
    public String getName();

    /**
     * Returns true if the specified subject is implied by this principal.
     *
     * 

The default implementation of this method returns true if * {@code subject} is non-null and contains at least one principal that * is equal to this principal. * *

Subclasses may override this with a different implementation, if * necessary. * * @param subject the {@code Subject} * @return true if {@code subject} is non-null and is * implied by this principal, or false otherwise. * @since 1.8 */ public default boolean implies(Subject subject) { if (subject == null) return false; return subject.getPrincipals().contains(this); } }

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

package org.springframework.security.core;

import java.io.Serializable;

public interface GrantedAuthority extends Serializable {
    String getAuthority();
}

4.2.2.2 UserDetailsService

DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal) 。 这个身份信息就是一个object ,大多数情况下它可以被强转为UserDetails对象。

DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的UserDetailsService公开为spring bean来自定义身份验证。

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

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认证流程,同时会把UserDetails填充至Authentication。

上面一直提到UserDetails是用户信息:

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

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

它和 Authentication 接口很类似,比如它们都拥有 username ,authorities。Authentication 的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。Authentication接口中的getDetails()方法,其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。

通过实现UserDetailsService和UserDetails ,我们可以完成对用户信息获取方式以及用户信息字段的扩展。

Spring Security提供的lnMemoryllserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。

测试

自定义UserDetailsService

package com.uncle.seciruty.springboot.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @program: spring-boot-security
 * @description:
 * @author: 步尔斯特
 * @create: 2021-08-06 22:05
 */
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    //根据 账号查询用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //将来连接数据库根据账号查询用户信息
        UserDto userDto = userDao.getUserByUsername(username);
        if(userDto == null){
            //如果用户查不到,返回null,由provider来抛出异常
            return null;
        }
        //根据用户的id查询用户的权限
        List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
        //将permissions转成数组
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build();
        return userDetails;
    }
}

屏蔽安全配置类中UserDetailsService的定义

package com.uncle.seciruty.springboot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @program: spring-boot-security
 * @description:
 * @author: 步尔斯特
 * @create: 2021-07-23 19:40
 */

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //定义用户信息服务(查询用户信息)
//    @Bean
//    public UserDetailsService userDetailsService(){
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
//        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
//        return manager;
//    }

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址

    }
}


重启工程,请求认证,SpringDataUserDetailsService的loadUserByUsername方法被调用,查询用户信息。

4.2.2.3 PasswordEncoder

DaoAuthenticationProvide以证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求 Authentication中的密码做对比呢?在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过 PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:

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

package org.springframework.security.crypto.password;

public interface PasswordEncoder {
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如下声明即可,如下:

@Bean
public PasswordEncoder passwordEncoder() {
	return NoOpPasswordEncoder.getInstance();
}

NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:

1、用户输入密码(明文)
2、DaoAuthenticationProvider获取UserDetails (其中存储了用户的正确密码)
3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。

NoOpPasswordEncode啲校验规则拿输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则校验失败。

实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder,SCryptPasswordEncoder等。

使用BCryptPasswordEncoder

1、 配置BCryptPasswordEncoder 在安全配置类中定义:

@Bean 
public PasswordEncoder passwordEncoder() { 
	return new BCryptPasswordEncoder(); 
}

测试发现认证失败,提示:Encoded password does not look like BCrypt。

 public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

原因:

由于UserDetails中存储的是原始密码(比如:123 ),它不是BCrypt格式。 跟踪DaoAuthenticationProvider第33行代码查看userDetails中的内容,跟踪第38行代码查看 Password Encoder的类型。

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

测试BCrypt
通过下边的代码测试BCrypt加密及校验的方法

添加依赖:

<dependency>
	<groupld>org.springframework.bootgroupld>
	<artifactld>spring-boot-starter-testartifactld>
	<scope>testscope>
dependency>

编写测试方法:

package com.uncle.seciruty.springboot.util;

import org.springframework.security.crypto.bcrypt.BCrypt;

/**
 * @program: spring-boot-security
 * @description:
 * @author: 步尔斯特
 * @create: 2021-08-06 22:12
 */
public class BCryptUtil {
    public static void main(String[] args) {
        String hashpw = BCrypt.hashpw("456", BCrypt.gensalt());
        System.out.println(hashpw);

        boolean checkpw = BCrypt.checkpw("456","$2a$10$bcJXXryMCxXtkxRkG1UekOkOe0BqxiqOYKJzGni64jnyWAD15wmDy");
        System.out.println(checkpw);

        //123 -> $2a$10$8iHn2TEvyzkUgO2np9glzufe.wtRyjA5u3xfvs/D.9FCzm1XvCAGm
        //456

    }
}

修改安全配置类

将UserDetails中的原始密码修改为BCrypt格式:

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。

4.2.3 授权流程

Spring Security可以通过http.authorizeRequests()对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。

Spring Security的授权流程如下:

spring security技术分享_第45张图片

分析授权流程:

1.拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FiltersecurityInterceptor的子类拦截。

2.获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterlnvocationSecurityMetadataSource获取要访问当前资源所需要的权限Collection

SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则,读取访问策略如:

http
	.authorizeRequests() 
		.antMatchers("/r/r1").hasAuthority("p1")
		.antMatchers("/r/r2").hasAuthority( "p2")

3.Filtersecurityinterceptor会调用AccessDecisionManager进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。

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

package org.springframework.security.access;

import java.util.Collection;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;

public interface AccessDecisionManager {
	/**
	*通过传递的参数来决定用户是否有访问对应受保护资源的权限
	*/
    void decide(Authentication var1, Object var2, Collection<ConfigAttribute> var3) throws AccessDeniedException, InsufficientAuthenticationException;

    boolean supports(ConfigAttribute var1);

    boolean supports(Class<?> var1);
}

这里着重说明一下decide的参数:

authentication :要访问资源的访问者的身份

object :要访问的受保护资源,web请求对应Fi足revocation

configAttributes :是受保护资源的访问策略,通过SecurityMetadataSource获取。

decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

授权决策

AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。

AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。

AccessDecisionVoter是一个接口 ,其中定义有三个方法,具体结构如下所示。

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

package org.springframework.security.access;

import java.util.Collection;
import org.springframework.security.core.Authentication;

public interface AccessDecisionVoter<S> {
    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = -1;

    boolean supports(ConfigAttribute var1);

    boolean supports(Class<?> var1);

    int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}

vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。

  • ACCESS_GRANTED表示同意
  • ACCESS_DENIED表示拒绝
  • ACCESS_ABS7AIN表示弃权

如果一个AccessDecisionVoter不能判定当前 Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。

Spring Security内置了三个基于投票的AccessDecisionManager实现类,它们分别是
AffirmativeBased、ConsensusBased和UnanimousBased。

AffirmativeBased的逻辑是:

  • 只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
  • 如果全部弃权也表示鮑;
  • 如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException, Spring security默认使用的是AffirmativeBased。

ConsensusBased的逻辑是:

  • 如果赞成票多于反对票则表示通过。
  • 反过来,如果反对票多于赞成票则将抛出AccessDeniedException,
  • 如果赞成票与反对票相同且不等于0 ,并且属性allowlfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowlfEqualGrantedDeniedDecisions的值默认为true。
  • 如果所有的AccessDecisionVoter都弃权了,则将视参数allowlfAIIAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedExceptiono。参数allowlfAIIAbstainDecisions的值默认为false。

UnanimousBased

UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。

UnanimousBased的逻辑具体来说是这样的:

  • 如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出 AccessDeniedExceptiono
  • 如果没有反对票,但是有赞成票,则表示通过。
  • 如果全部弃权了,则将视参数allowlfAIIAbstainDecisions的值而定,true则通过,false则抛出 AccessDeniedExceptiono SpringSecurity也内置一些投票者实现类如RoleVoter、AuthenticatedVoter和WebExpressionVoter等,可以自行查阅资料进行学习。

4.3 自定义认证

Spring Security提供了非常好的认证扩展方法,比如:将用户信息存储到内存中,实际开发中用户信息通常在数据库,Spring Security可以实现从数据库读取用户信息,Spring security还支持多种授权方法。

4.3.1 自定义登录页面

Spring Security的默认配置没有明确设定一个登录页面的URL ,因此Spring Security会根据启用的功能自动生成一个登录页面URL ,并使用默认URL处理登录的提交内容,登录后跳转的到默认URL等等。尽管自动生成的登录页面很方便快速启动和运行,但大多数应用程序都希望定义自己的登录页面。

4.3.1.1 认证页面

将security-springmvc工程的login.jsp拷贝到security-springboot下,目录保持一致。

在这里插入代码片
4.3.1.2 配置认证页面

在WebConfig.java中配置认证页面地址:

//默认Url根路径剧阵专至Ij/login ,此url为spring security提供
@0verride
public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/").setViewName("login-view")
  		registry.addViewController("login-view").setViewName("login");
}
4.3.1.3 安全配置

在WebSecurityConfig中配置表单登录信息:


(1)允许表单登录
(2 )指定我们自己的登录页,spring security以重定向方式将路径转发到/login-view
(3)指定登录处理的URL ,也就是用户名、密码表单提交的目的路径
(4)指定登录成功后的跳转URL
(5 )我们必须允许所有用户访问我们的登录页(例如为验证的用户),这个formLogin() .permitAll()方法允许任意用户访问基于表单登录的所有的URL。

4.3.1.4 测试

当用户没有认证时访问系统的资源会重定向到login-view页面

在这里插入代码片

输入账号和密码,点击登录,报错:

在这里插入代码片

问题解决:

spring security为防止CSRF ( Cross-site request forgery跨站请求伪造)的发生,限制了除了get以外的大多数方 法。

解决方法1:

屏蔽CSRF控制,即spring security不再限制CSRF。
配置 WebSecurityConfig

@0verride
protected void configure(HttpSecurity http) throws Exception {
 	 http.csrf().disable()//屏蔽CSRF控制,即 spring security 不再限制CSRF
}

解决办法2 :

在login.jsp页面添加一个token , spring security会验证token ,如果token合法则可以继续请求。
修改 login.jsp

<form action="login" method="post">
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
form>

4.3.2 连接数据库认证

前边的例子我们是将用户信息存储在内存中,实际项目中用户信息存储在数据库中。根据前边对认证流程研究,只需要重新定义UserDetailService即可实现根据用户账号查询数据库。

4.3.2.1 创建数据库

创建user_db数据库

CREATE DATABASE ‘user_db’ CHARACTER SET 'utf8‘ COLLATE ‘utf8_general_ci’;

创建t_user 表

CREATE TABLE 't_user' (
	'id' bigint(20) NOT NULL COMMENT,用户id',
	'username' varchar(64) NOT NULL,
	'password' varchar(64) NOT NULL,
	'fullname' varchar(255) NOT NULL COMMENT,用户姓名',
	'mobile' varchar(ll) DEFAULT NULL COMMENT,手机号
	PRIMARY KEY ('id') USING BTREE
)ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC
4.3.2.2 代码实现

1 )定义dataSource
在application.properties 配置

spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=mysql
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

2)添加依赖

<dependency>
	<groupld>org.springframework.bootgroupld>
	<artifactld>spring-boot-starter-testartifactld>
	<scope>testscope>
dependency>
<dependency>
	<groupld>org.springframework.bootgroupld>
	<artifactld>spring-boot-starter-jdbcartifactld>
dependency>
<dependency>
	<groupld>mysqlgroupld>
	<artifactld>mysql-connector-javaartifactld>
	<version>5.1.47version>
dependency>

3 )定义Dao

定义模型类型,在model包定义UserDto:

@Data
public class UserDto { private String id;
	private String username;
	private String password;
	private String fullname;
	private String mobile;
}

在Dao包定义UserDao :

@Repository
public class UserDao {
	@Autowired
	ZJdbcTemplate jdbcTemplate;
	public UserDto getUserByUsername(String username){
		String sql =r,select id,username,password,funname from t_user where username = ?";
		List<UserDto> list = jdbcTemplate.query(sqlj new Object[Jlusername}^ new BeanPropertyRowMappero(UserDto. class));
		if(list == null && list.size() <= 0)(
			return null;
		}
		return list.get(0);
	}
}
4.3.2.3 定义 UserDetailService

在service包下定义SpringDatallserDetailsService :

@Service
public class SpringDatallserDetailsService implements UserDetailsService {
		@Autowired
		UserDao userDao;
		
		@Override
		public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
			//登录账号
			System.out. println();
			
			//根据账号去数据库查询...
			UserDto user = userDao.getUserByUsername(username);
			
			if(user == null){
				return null;
			}
			//这里暂时使用静态麴居
			UserDetails userDetails =User.withUsername(user.getFullname()).password(user.getPassword()).authorities("pl").build(); 
			return userDetails;
		}
}
4.3.2.3 测试

输入账号和密码请求认证,跟踪代码。

4.3.2.4 使用BCryptPasswordEncoder

按照我们前边讲的PasswordEncoder的使用方法,使用BCryptPasswordEncoder需要完成如下工作:

1、在安全配置类中定义BCryptPasswordEncoder

@Bean
public PasswordEncoder passwordEncoder() {
	return new BCryptPasswordEncoder();
}

2、UserDetails中的密码存储BCrypt格式

前边实现了从数据库查询用户信息,所以数据库中的密码应该存储BCrypt格式

package com.uncle.seciruty.springboot.util;

import org.springframework.security.crypto.bcrypt.BCrypt;

/**
 * @program: spring-boot-security
 * @description:
 * @author: 步尔斯特
 * @create: 2021-08-06 22:12
 */
public class BCryptUtil {
    public static void main(String[] args) {
        String hashpw = BCrypt.hashpw("456", BCrypt.gensalt());
        System.out.println(hashpw);

        boolean checkpw = BCrypt.checkpw("456","$2a$10$bcJXXryMCxXtkxRkG1UekOkOe0BqxiqOYKJzGni64jnyWAD15wmDy");
        System.out.println(checkpw);

        //123 -> $2a$10$8iHn2TEvyzkUgO2np9glzufe.wtRyjA5u3xfvs/D.9FCzm1XvCAGm
        //456 -> $2a$10$bcJXXryMCxXtkxRkG1UekOkOe0BqxiqOYKJzGni64jnyWAD15wmDy

    }
}

4.4 会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring
security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。

4.4.1 获取用户身份

编写LoginController,实现/r/r1、 /r/r2的测试资源,并修改loginSuccess方法,注意getUsername方法,Spring Security 获取当前登录用户信息的方法为 SecurityContextHolder.getContext().getAuthentication()

@RestController
public class Logincontroller {
		/**
		*用户登录成功
		* @return
		*/
		@RequestMapping( value = "/login-success", produces = {"text/plain; charset=UTF-8"}) 
		public String loginSuccess(){
			String username = getUsername();
			return username + "登录成功"}
		
		/**
		*获取当前登录用户名
		* @return
		*/
		private String getUsername(){
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			if(!authentication.isAuthenticated()){
			return null;
			}
			Object principal = authentication.getPrincipal();
			String username = null;
			if (principal instanceof org.springframework.security.core.userdetails.UserDetails) { 
			username =((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();
			} else {
				username = principal.toString();
			}
			return username;
		}
	

测试
登录前访问资源 被重定向至登录页面。
登录后访问资源 成功访问资源,如下:

zhang san访冋资源1

4.4.2 会话控制

我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:

机制 描述
always 如果没有session存在就创建一个
ifRequired 如果需要就仓U建一个Session (默认)登录时
never SpringSecurity将不会创建Session ,但是如果应用中其他地方创建了Session ,那么Spring Security将会使用它。
stateless SpringSecurity将绝对不会创建Session ,也不使用Session

通过以下配置方式对该选项进行配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
		http.sessionManagement()
			.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
}

默认情况下,Spring Security会为每个登录成功的用户会新建一个Session ,就是ifRequired。

若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了 session ,月B么Spring Security会用它的。

若使用stateless,则说明Spring Security对登录成功的用户不会创建Session ,你的应用程序也不会允许新建 session。并且它会暗示不使用cookie ,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API及袞状态认证机制。

会话超时

可以在sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s ; spring boot配置文件:

server.servlet.session.timeout=3600s

session超时之后,可以通过Spring Security设置跳转的路径。

http.sessionManagement()
	.expiredUrl( "/login-view?error=EXPIRED_SESSI0Nn")
	.invalidSessionUrl("/login-view?error=INVALID_SESSION");

expired指session过期,invalidSession指传入的sessionid无效。

安全会话cookie

我们可以使用httpOnly和secure标签来保护我们的会话cookie :

  • httpOnly :如果为true ,那么浏览器脚本将无法访问cookie
  • secure :如果为true,则cookie将仅通过HTTPS连接发送

spring boot配置文件:

server.servlet.session.cookie.http-only=true server.servlet.session.cookie.secure=true

4.6 退出

Spring security默认实现了logout退出,访问/logout,果然不出所料,退出功能Spring也替我们做好了。


点击"Log Out”退出成功。

退出后访问其它url判断是否成功退出。

这里也可以自定义退出成功的页面:

在WebSecurityConfig的protected void configure(HttpSecurity http)中配置:

.and()
	.logout()
	.logoutUrl("/logout")
	.logoutSuccessUrl("/login-view?logout");

当退出操作出发时,将发生:

  • 使HTTP Session 无效
  • 清除 SecurityContextHolder 路径转到 /login-view?logout

但是,类似于配置登录功能,咱们可以进一步自定义退出功能:

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//...
.and()
	.logout()	(1)
	.logoutUrl("/logout")	(2)
	.logoutSuccessUrl("/login-viewPlogout")	(3)
	.logoutSuccessHandler(logoutSuccessHandler)	(4)
	.addLogoutHandler(logoutHandler)	(5)
	.invalidateHttpSession(true);	(6)
}

(1 )提供系统退出支持,使用WebSecurityConfigurerAdapter会自动被应用。

(2 )设置触发退出操作的URL (默认是/logout )。

(3 )退出之后跳转的URL,默认是/login?logout。

(4 )定制的LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么 logoutSuccessUrl()的设置会被忽略。

(5 )添加一个LogoutHandler ,用于实现用户退出时的清理工作.默认SecuritycontextLogoutHandler会被添加为—个 LogoutHandler。

(6 )指定是否在退出时让HttpSession无效。默认设置为true。 注意:如果让logout在GET请求下生效,必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF ,必须使用
post方式请求/logout

logoutHandler :

一般来说,LogoutHandler的实现类被用来执行必要的清理,因而他们不应该抛出异常。 下面是Spring Security提供的一些实现:

  • PersistentTokenBasedRememberMeServices 基于持久化token的RememberMe功能的相关清理
  • TokenBasedRememberMeService 基于token的RememberMe功能的相关清理
  • CookieClearingLogoutHandler 退出时Cookie的相关清理
  • CsrfLogoutHandler 负责在退出时移除csrfToken
  • SecurityContextLogoutHandler 退出时Securitycontext的相关清理 链式API提供了调用相应的LogoutHandler实现的快捷方式,比如deleteCookies().

4.7 授权

4.7.1 概述

授权的方式包括web授权和方法授权,web授权是通过url拦截进行授权,方法授权是通过方法拦截进行授权。他们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecuritylnterceptor;若为方法授权则拦截器为Methodsecurityinterceptor。如果同时通过web授权和方法授权则先执行web授权,再执行方法授权。最后决策通过,则允许访问资源,否则将禁止访问。

4.7.2 准备环境

4.7.2.1 数据库环境

在t_user数据库创建如下表:

角色表:

CREATE TABLE 't_role' (
	'id' varchar(32) NOT NULL,
	'role_name' varchar(255) DEFAULT NULL,
	'description' varchar(255) DEFAULT NULL,
	'create_time' datetime DEFAULT NULL,
	'update_time' datetime DEFAULT NULL,
	'status' char(l) NOT NULL,
	PRIMARY KEY ('id'),
	UNIQUE KEY 'unique_role_name' ('role_name')
)ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into 't_role'('id','role_name', 'description','create_time', ' update_time', 'status') values ('1''管理员',NULL,NULL,NULL)

用户角色关系表:

CREATE TABLE 't_user_role' (
	'userid' varchar(32) NOT NULL,
	'roleid' varchar(32) NOT NULL,
	'create_time' datetime DEFAULT NULL,
	'creator' varchar(255) DEFAULT NULL,
	PRIMARY KEY ('userid','role_id')
)ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into 't_user_role'('user_id','roleid','create_time','creator') values
('1','1',NULL,NULL);

权限表:

CREATE TABLE 't_permission' (
	'id' varchar(32) NOT NULL,
	'code' varchar(32) NOT NULL COMMENT,权限标识符
	'description' varchar(64) DEFAULT NULL COMMENT '描述
	'url' varchar(128) DEFAULT NULL COMMENT '请求地址
	PRIMARY KEY ('id')
)ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into 't_permission' (' id',' code','description',' url') values (' 1’,’ pl','测试资源 l','/r/rl'),('2','p3','测试资源2','/r/r2');

角色权限关系表:

CREATE TABLE 't_role_permission' (
	'roleid' varchar(32) NOT NULL,
	'permission_id' varchar(32) NOT NULL,
	PRIMARY KEY ('role_id','permissionid')
)ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into 't_role_permission'(' role_id','permission_id') values ('1','1')
4.7.2.2 修改 UserDetailService

1、修改dao接口
在UserDao中添加:

〃根据用户id查询用户权限
public List<String> findPermissionsByUserId(String userld){
	String sql=...略
	List<PermissionDto> list = jdbcTemplate.query(sql,new Object[]{userid}new BeanPropertyRowMappero(PermissionDto.class));
	List<String> permissions = new ArrayList<>();
	list.iterator().forEachRemaining(c->permissions.add(c.getCode())); 
	return permissions;

2、修改UserDetailService
实现从数据库读取权限

@0verride
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //登录略
	System.out.printin("username="+username);
	//根据账号去数据库查询...
	UserDto user = userDao.getUserByUsername(username);
	if(user == null)(
	return null;
	}
	//查询用户权限
	List<String> permissions = userDao.findPermissionsByUserId(user.getId());
	Stringf] perarray = new String[permissions.size()];
	permissions.toArray(perarray);
	//创建 userDetails
	UserDetails userDetails =
	User.withUsername(user.getFullname()).password(user.getPassword()).authorities(perarray)build(); 
	return userDetails;
}

4.7.2 web授权

在上面例子中我们完成了认证拦截,并对/r/**下的某些资源进行简单的授权保护,但是我们想进行灵活的授权控制该怎么做呢?通过给http.authorizeRequests()添加多个子节点来定制需求 ,如下代码:

@Override
protected void configure(HttpSecurity http) throws Exception {	
http .authorizeRequests()	(1)
	.antMatchers("/r/rl").hasAuthority("pl")	(2)
	.antMatchers("/r/r2").hasAuthority("p2")	(3)
	.antMatchers("/r/rS").access("hasAuthority('p1') and hasAuthority('p2')")	(4)
	.antMatchers("/r/**")]authenticated()	(5)
	.anyRequest().permitAll().and()
	.formLogin()

}		

(1 ) http.authorizeRequests()方法有多个子节点,每个macher按照他们的声明顺扇丸行。
(2)指定"/r/r1 “URL,拥有p1权限能够访问
(3 )指定”/r/r2"URL ,拥有p2权限能够访问
(4 )指定了”/r/r3”URL ,同时拥有p1和p2权限才能够访问
(5 )指定了除了r1、r2、r3之外’7r/**’'资源,同时通过身份认证就能够访问,这里使用SpEL ( Spring Expression Language )表达式。
(6 )剩余的尚未匹配的资源,不做保护。

注意: 规则的顺序是重要的,更具体的规则应该先写.现在以/admin开始的所有内容都需要具有ADMIN角色的身份验证用 户,即使是/
admin / login路径(因为/ admin / login已经被/ admin / **规则匹配,因此第二个规则被忽略).

.antMatchers("/admin/*\*").hasRole("ADMIN")
.antMatchers("/admin/login").permitAll()

因此登录页面的规则应该在/ admin / **规则之前.例如.

.antMatchers("/admin/login").permitAll()
.antMatchers("/admin/*\*").hasRole("ADMIN")

保护URL常用的方法有:

authenticated()保护URL ,需要用户登录 permitAII()指定URL无需保护,一般应用与静态资源文件

hasRole(String role)限制单个角色访问,角色将被增加"ROLE_".所以’ADMIN"将和"ROLE_ADMIN"进行比较.

hasAuthority(String authority)限制单个权限访问

hasAnyRole(String… roles)允许多个角色访问. hasAnyAuthority(String… authorities)允许多个权限访问.

access(String attribute)该方法使用SpEL表达式,所以可以创建复杂的限制.

haslpAddress(String ipaddressExpression)限制IP地址或子网

4.7.3 方法授权

现在我们已经掌握了使用如何使用http.authorizeRequests()对web资源进行授权保护,从Spring Security2.0版本开始,它支持服务层方法的安全性的支持。

@PreAuthorize,@PostAuthorize, @Secured三类注解。

我们可以在任(可@Configuration实例上使用@EnableGlobalMethodSecurity注释来启用基于注解的安全性。

以下内容将启用Spring Security的@Secured注释。

@EnableGlobalMethodSecurity(securedEnabled = true) 
public class MethodSecurityConfig (// ...
}

然后向方法(在类或接口上)添加注解就会限制对该方法的访问。Spring Security的原生注释支持为该方法定义了 一组属性。这些将被传递给AccessDecisionManager以供它作出实际的决定:

public interface BankService { 
	@Secured()
	public Account readAccount(Long id);
	@Secured()
	public Accountf] findAccounts();
	@Secured()
	public Account post(Account account, double amount);
}

以上配置标明readAccount、findAccounts方法可匿名访问,底层使用WebExpressionVoter投票器,可从 AffirmativeBased 第23 行代码跟踪。 post方法需要有TELLER角色才能访问,底层使用RoleVoter投票器。
使用如下代码可启用prePost注解的支持

@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class MethodSecurityConfig {
//...
}

相应Java代码如下:

public interface BankService {
	@PreAuthorize("isAnonymous()")
	public Account readAccount(Long id);
	@PreAuthorize("isAnonymous()")
	public Accountf] findAccounts。;
	@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')") public Account post(Account account, double amount);
}

你可能感兴趣的:(深入浅出,百炼成仙,安全框架,spring,springsecurity)