菜鸟的spring security学习教程

  • 说明
  • 一、Spring Security简介
  • 二、Spring Security入门系列
  • (1)默认登录与注销
  • (2)自定义表单登录
  • (3)自定义表单用户授权
  • (4)基于数据库的自定义表单认证
  • (5)基于数据库的自定义表单授权
  • (6)获取当前登录用户的信息
  • (7)前后端分离下的基于表单数据的登录验证
  • (8)前后端分离下的基于json数据的登录验证
  • 三、SpringSecurity核心组件
  • (1)Authentication
  • (2)SecurityContext
  • (3)SecurityContextHolder
  • (4)UserDetails
  • (5)UserDetailsService
  • (6)AuthenticationManager
  • 四、部分源码解析
  • (1)用户认证流程
  • 认证大致流程
  • 认证具体流程
  • (2)默认登录用户名与密码配置


说明

更新时间:2020/5/31 22:50,更新了基于数据库的认证与授权
更新时间:2020/6/6 17:45,更新了SpringSecurity核心组件

近期要用到spring security这个框架,由于spring security是之前学的,而且当时也没有深入的学习,对于该框架的用法有点陌生了,现重新学习spring security并在此做好笔记,本文会持续更新,不断地扩充

本文仅为记录学习轨迹,如有侵权,联系删除

一、Spring Security简介

Spring Security 是 Spring 家族中的一个安全管理框架,主要用于 Spring 项目组中提供安全认证服务,该框架主要的核心功能有认证授权攻击防护

二、Spring Security入门系列

(1)默认登录与注销

文件名:springboot_security2

pom配置

<dependencies>
        <!--thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--SpringSecurity框架整合-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>


        <!-- thymeleaf和springsecurity5的整合 -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>

    </dependencies>

经过测试,发现一般只要配置了SpringSecurity之后,即pom导入配置后,只要一访问控制器的接口,都会被拦截,自动跳转到SpringSecurity自定义的登录界面,界面如下:

SpringEL 菜鸟 spring security菜鸟教程_Security


Security自定义的账号是user,密码则是由控制台生成

SpringEL 菜鸟 spring security菜鸟教程_spring boot_02


输入账号和密码即可登录成功,并跳转到一开始输入要访问的页面

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_03


在url后面输入logout即可退出登录,logout接口也是Security自己内部的接口。

后面真正使用的时候会自己重写配置,配置自己写的登录页面,以及做一些用户权限处理。

(2)自定义表单登录

文件名:springboot_security2
可以看到如果配置了SpringSecurity,Security会有自己的登录页面,并且会拦截任何页面,Security会有自己的内部接口login和logout。当然,很多时候是不会用它自己内部的登陆页面,更多的是用自己自定义的登录页面,用自定义的表单,只需要自己做一下配置即可。

首先自己新建一个配置类,并且继承WebSecurityConfigurerAdapter,这是官方要求的,官方说明如下:

/**
 * Provides a convenient base class for creating a {@link WebSecurityConfigurer}
 * instance. The implementation allows customization by overriding methods.
 *
 * <p>
 * Will automatically apply the result of looking up
 * {@link AbstractHttpConfigurer} from {@link SpringFactoriesLoader} to allow
 * developers to extend the defaults.
 * To do this, you must create a class that extends AbstractHttpConfigurer and then create a file in the classpath at "META-INF/spring.factories" that looks something like:
 * </p>
 * <pre>
 * org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer
 * </pre>
 * If you have multiple classes that should be added you can use "," to separate the values. For example:
 *
 * <pre>
 * org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer, sample.OtherThatExtendsAbstractHttpConfigurer
 * </pre>
 *
 */

意思是说 WebSecurityConfigurerAdapter 提供了一种便利的方式去创建 WebSecurityConfigurer的实例,只需要重写 WebSecurityConfigurerAdapter 的方法,即可配置拦截什么URL、设置什么权限等安全控制。

创建配置类

package com.zsc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Objects;

/**
 * 没有添加改配置,页面会强制跳转到springsecurity自己的登录页面
 * 参考链接:
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    // 指定密码的加密方式,不然定义认证规则那里会报错
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return Objects.equals(charSequence.toString(), s);
            }
        };
    }

    //配置忽略掉的 URL 地址,一般用于js,css,图片等静态资源
    @Override
    public void configure(WebSecurity web) throws Exception {
        //web.ignoring() 用来配置忽略掉的 URL 地址,一般用于静态文件
        web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");
    }

    // (认证)配置用户及其对应的角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //数据在内存中定义,一般要去数据库取,jdbc中去拿,
        /**
         * 懒羊羊,灰太狼,喜羊羊,小灰灰分别具有vip0,vip1,vip2,vip3的权限
         * root则同时又vip0到vip3的所有权限
         */
        //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
        //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
        //spring security 官方推荐的是使用bcrypt加密方式。
        auth.inMemoryAuthentication()
                .withUser("懒羊羊").password("123").roles("vip0")
                .and()
                .withUser("灰太狼").password("123").roles("vip1")
                .and()
                .withUser("喜羊羊").password("123").roles("vip2")
                .and()
                .withUser("小灰灰").password("123").roles("vip3")
                .and()
                .withUser("root").password("123").roles("vip1","vip2","vip3");

    }

    // (授权)配置 URL 访问权限,对应用户的权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();//开启运行iframe嵌套页面

        //任何请求都必须经过身份验证
        http.authorizeRequests()
                .anyRequest().authenticated();//任何请求都必须经过身份验证


        //开启表单验证
        http.formLogin()
                .and()
                .formLogin()//开启表单验证
                .loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("name")//自定义表单的用户名的name,默认为username
                .passwordParameter("pwd")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/doLogin")//表单请求的地址,一般与form的action属性一致,注意:不用自己写doLogin接口,只要与form的action属性一致即可
                .successForwardUrl("/index")//登录成功后跳转的页面(重定向)
                .failureForwardUrl("/toLogin")//登录失败后跳转的页面(重定向)
                .and()
                .logout()//开启注销功能
                .logoutSuccessUrl("/toLogin")//注销后跳转到哪一个页面
                .logoutUrl("/logout") // 配置注销登录请求URL为"/logout"(默认也就是 /logout)
                .clearAuthentication(true) // 清除身份认证信息
                .invalidateHttpSession(true) //使Http会话无效
                .permitAll() // 允许访问登录表单、登录接口
                .and().csrf().disable(); // 关闭csrf
    }
}

这里配置了几个用户懒羊羊,灰太狼,喜羊羊,小灰灰等用于登录,一般这些用户要从数据库获取,另外这里给他们设置了对应的权限,vip0到vip3的权限,都是自己自定义的权限,主要是为了下一节做授权操作(这里还没做授权操作)

除此之外,这里有一个自己之前一直搞错的重点如下

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_04


运行并访问主页index,会被拦截并且跳到自定义表单

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_05


随便输入自己上面定义好的用户,跳转到首页,并且所有的页面都可以访问,vip0到vip3对应所有页面均可以访问

SpringEL 菜鸟 spring security菜鸟教程_java_06


注销登录(退出)

SpringEL 菜鸟 spring security菜鸟教程_Security_07


以上就完成了自定义表单的登录与注销,下面开始做用户授权。

(3)自定义表单用户授权

文件名:springboot_security2

用户授权简单理解就是什么用户可以访问什么页面,不同的用户可以访问不同的页面,上一节已经给不同的用户设置了的权限,下面给不同用户做授权,配置类如下

package com.zsc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Objects;

/**
 * 没有添加改配置,页面会强制跳转到springsecurity自己的登录页面
 * 参考链接:
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    // 指定密码的加密方式,不然定义认证规则那里会报错
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return Objects.equals(charSequence.toString(), s);
            }
        };
    }

    //配置忽略掉的 URL 地址,一般用于js,css,图片等静态资源
    @Override
    public void configure(WebSecurity web) throws Exception {
        //web.ignoring() 用来配置忽略掉的 URL 地址,一般用于静态文件
        web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");
    }

    // (认证)配置用户及其对应的角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //数据在内存中定义,一般要去数据库取,jdbc中去拿,
        /**
         * 懒羊羊,灰太狼,喜羊羊,小灰灰分别具有vip0,vip1,vip2,vip3的权限
         * root则同时又vip0到vip3的所有权限
         */
        //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
        //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
        //spring security 官方推荐的是使用bcrypt加密方式。
        auth.inMemoryAuthentication()
                .withUser("懒羊羊").password("123").roles("vip0")
                .and()
                .withUser("灰太狼").password("123").roles("vip1")
                .and()
                .withUser("喜羊羊").password("123").roles("vip2")
                .and()
                .withUser("小灰灰").password("123").roles("vip3")
                .and()
                .withUser("root").password("123").roles("vip1","vip2","vip3");

    }

    // (授权)配置 URL 访问权限,对应用户的权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();//开启运行iframe嵌套页面

        //任何请求都必须经过身份验证
        http.authorizeRequests()
//                .anyRequest().authenticated()//任何请求都必须经过身份验证
                .antMatchers("/vip/vip0/**").hasRole("vip0")//vip1具有的权限:只有vip1用户才可以访问包含url路径"/vip/vip0/**"
                .antMatchers("/vip/vip1/**").hasRole("vip1")//vip1具有的权限:只有vip1用户才可以访问包含url路径"/vip/vip1/**"
                .antMatchers("/vip/vip2/**").hasRole("vip2")//vip2具有的权限:只有vip2用户才可以访问url路径"/vip/vip2/**"
                .antMatchers("/vip/vip3/**").hasRole("vip3");//vip3具有的权限:只有vip3用户才可以访问url路径"/vip/vip3/**"


        //开启表单验证
        http.formLogin()
                .and()
                .formLogin()//开启表单验证
                .loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("name")//自定义表单的用户名的name,默认为username
                .passwordParameter("pwd")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/doLogin")//表单请求的地址,一般与form的action属性一致,注意:不用自己写doLogin接口,只要与form的action属性一致即可
                .successForwardUrl("/index")//登录成功后跳转的页面(重定向)
                .failureForwardUrl("/toLogin")//登录失败后跳转的页面(重定向)
                .and()
                .logout()//开启注销功能
                .logoutSuccessUrl("/toLogin")//注销后跳转到哪一个页面
                .logoutUrl("/logout") // 配置注销登录请求URL为"/logout"(默认也就是 /logout)
                .clearAuthentication(true) // 清除身份认证信息
                .invalidateHttpSession(true) //使Http会话无效
                .permitAll() // 允许访问登录表单、登录接口
                .and().csrf().disable(); // 关闭csrf
    }
}

主要增加了用户权限的配置,具体如下图

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_08


不需要登录直接进首页,因为没对首页index做限制,但是点击vip0到vip3的任意页面都会被拦截,并且自动跳转到登录页面进行登录

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_09


点击vip0下对应的页面、

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_10


灰太狼账号登录后,可以访问vip1权限的页面,其余权限的页面不可以访问,如果访问会抛出异常,因为没做相应异常的处理,所以异常会显示在页面

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_11


登录具有不同权限的用户,可以访问对应权限的页面,以上就是用户授权的最基本的用户。

(4)基于数据库的自定义表单认证

文件名:springboot_security3

首先是数据库的创建,实际上登录认证至少要有5张表,用户表角色表权限表角色权限中间表用户角色中间表,这里按照上面的例子,将权限直接写死在自定义的SecurityConfig配置类中。

SpringEL 菜鸟 spring security菜鸟教程_java_12


所以这里的登录认证只涉及到三张表:用户表(user)、角色表(role)、用户角色中间表(user_role)。

/*
 Navicat Premium Data Transfer

 Source Server         : test3
 Source Server Type    : MySQL
 Source Server Version : 80015
 Source Host           : localhost:3306
 Source Schema         : test2

 Target Server Type    : MySQL
 Target Server Version : 80015
 File Encoding         : 65001

 Date: 31/05/2020 22:01:56
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_vip0');
INSERT INTO `role` VALUES (2, 'ROLE_vip1');
INSERT INTO `role` VALUES (3, 'ROLE_vip2');
INSERT INTO `role` VALUES (4, 'ROLE_vip3');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
INSERT INTO `user` VALUES (3, '灰太狼', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
INSERT INTO `user` VALUES (4, '喜羊羊', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
INSERT INTO `user` VALUES (5, '懒羊羊', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
INSERT INTO `user` VALUES (6, '小灰灰', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) NULL DEFAULT NULL,
  `rid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 1, 3);
INSERT INTO `user_role` VALUES (4, 1, 4);
INSERT INTO `user_role` VALUES (5, 3, 2);
INSERT INTO `user_role` VALUES (6, 4, 3);
INSERT INTO `user_role` VALUES (7, 6, 4);
INSERT INTO `user_role` VALUES (8, 5, 1);

SET FOREIGN_KEY_CHECKS = 1;

具体数据表截图

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_13


注意:这里的role跟上面的例子相比多加了ROLE_前缀。这是因为之前的role都是通过springsecurity的api赋值过去的,他会自行帮我们加上这个前缀。但是现在我们使用的是自己的数据库里面读取出来的权限,然后封装到自己的实体类中。所以这时候需要我们自己手动添加这个ROLE_前缀。经过测试如果不加ROLE_前缀的话,可以做数据库的认证,但无法做授权

创建实体类User,注意User需要实现UserDetails接口,并且实现该接口下的7个接口

package com.zsc.po;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
    private Integer id;
    private String userName;//用户名
    private String passWord;//密码

    private List<Role> roles;//该用户对应的角色




    /**
     * 返回用户的权限集合。
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    /**
     * 返回账号的密码
     * @return
     */
    @Override
    public String getPassword() {
        return passWord;
    }

    /**
     * 返回账号的用户名
     * @return
     */
    @Override
    public String getUsername() {
        return userName;
    }

    /**
     * 账号是否失效,true:账号有效,false账号失效。
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }


    /**
     * 账号是否被锁,true:账号没被锁,可用;false:账号被锁,不可用
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 账号认证是否过期,true:没过期,可用;false:过期,不可用
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 账号是否可用,true:可用,false:不可用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }

}

角色表实体类Role,这个类不用实现上述接口

package com.zsc.po;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
    private Integer id;
    private String name;//角色的名字
}

接下来做数据库的查询,创建持久层接口(UserMapper和RoleMapper)

package com.zsc.mapper;

import com.zsc.po.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;

@Mapper
@Repository
public interface UserMapper {
    /**
     * 通过用户名获取用户信息
     *
     * @param username 用户名
     * @return User 用户信息
     */
    List<User> getUserByUsername(String username);
    
}
package com.zsc.mapper;

import com.zsc.po.Role;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;

@Mapper
@Repository
public interface RoleMapper {
    /**
     * 通过用户id获取用户角色集合
     *
     * @param userId 用户id
     * @return List<Role> 角色集合
     */
    List<Role> getRolesByUserId(Integer userId);
}

持久层接口对应配置文件(UserMapper.xml和RoleMapper.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zsc.mapper.UserMapper">
    <resultMap id = "userMap" type = "com.zsc.po.User">
        <id column="id" property="id"></id>
        <result column="username" property="userName"></result>
        <result column="password" property="passWord"></result>
        <collection property="roles" ofType="com.zsc.po.Role">
            <id property="id" column="rid"></id>
            <result column="rname"  property="name"></result>
        </collection>
    </resultMap>

    <select id="getUserByUsername" resultMap="userMap">
        select * from user where username = #{username}
    </select>


</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zsc.mapper.RoleMapper">
    <resultMap id = "roleMap" type = "com.zsc.po.Role">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
    </resultMap>

    <select id="getRolesByUserId" resultMap="roleMap">
        select * from role r,user_role ur where r.id = ur.rid and ur.uid = #{userId}
    </select>


</mapper>

创建服务层(UserService),该层获取数据库数据,将数据交给SpringSecurity做用户的认证与授权,为此,需要实现接口UserDetailsService,并且实现该接口下的loadUserByUsername方法,该方法获取数据库数据

package com.zsc.service;

import com.zsc.mapper.RoleMapper;
import com.zsc.mapper.UserMapper;
import com.zsc.po.User;
import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        List<User> users = userMapper.getUserByUsername(s);

        if (null == users || users.size() ==0) {
            throw new UsernameNotFoundException("该用户不存在!");
        }else{
            users.get(0).setRoles(roleMapper.getRolesByUserId(users.get(0).getId()));
            System.out.println("***********************"+users.get(0).getAuthorities());
            return users.get(0);
        }

    }
}

最后修改一下自定义的SecurityConfig配置类即可

package com.zsc.config;

import com.zsc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

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


/**
 * 没有添加改配置,页面会强制跳转到springsecurity自己的登录页面
 * 参考链接:
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;
    

    //配置忽略掉的 URL 地址,一般用于js,css,图片等静态资源
    @Override
    public void configure(WebSecurity web) throws Exception {
        //web.ignoring() 用来配置忽略掉的 URL 地址,一般用于静态文件
        web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");
    }

    // (认证)配置用户及其对应的角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
    }

    // (授权)配置 URL 访问权限,对应用户的权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();//开启运行iframe嵌套页面


        //身份验证
        http.authorizeRequests()
                .anyRequest().authenticated();//任何请求都必须经过身份验证

        //开启表单验证
        http.formLogin()
                .and()
                .formLogin()//开启表单验证
                .loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("name")//自定义表单的用户名的name,默认为username
                .passwordParameter("pwd")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/doLogin")//表单请求的地址,一般与form的action属性一致,注意:不用自己写doLogin接口,只要与form的action属性一致即可
                .successForwardUrl("/index")//登录成功后跳转的页面(重定向)
                .failureForwardUrl("/toLogin")//登录失败后跳转的页面(重定向)
                .and()
                .logout()//开启注销功能
                .logoutSuccessUrl("/toLogin")//注销后跳转到哪一个页面
                .logoutUrl("/logout") // 配置注销登录请求URL为"/logout"(默认也就是 /logout)
                .clearAuthentication(true) // 清除身份认证信息
                .invalidateHttpSession(true) //使Http会话无效
                .permitAll() // 允许访问登录表单、登录接口
                .and().csrf().disable(); // 关闭csrf
    }
}

大功告成,运行测试,访问首页index,被拦截重定向到登录页面进行用户认证,即登录认证

SpringEL 菜鸟 spring security菜鸟教程_java_14


随便输入数据库中存在的任一用户,密码是123,数据库存储的密码是经过加密的,登录成功,因为没做任何用户的授权,所以可访问任意页面

SpringEL 菜鸟 spring security菜鸟教程_spring boot_15


这里再重点记录一下,关于数据库存储用户权限必须要有ROLE_前缀,但在SecurityConfig中设置权限时可以不用加ROLE_前缀

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_16


这其实是授权部分的内容,下一节就是数据库用户的授权操作

(5)基于数据库的自定义表单授权

文件名:springboot_security3
关于授权部分,上一节其实已经有讲到一点了,实现起来页简单,基本的配置跟上一节一样保持不变,唯一变的就是将拦截所有请求改为对应权限的拦截,具体只要修改SecurityConfig配置类的部分内容即可

package com.zsc.config;

import com.zsc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


/**
 * 没有添加改配置,页面会强制跳转到springsecurity自己的登录页面
 * 参考链接:
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    



    //配置忽略掉的 URL 地址,一般用于js,css,图片等静态资源
    @Override
    public void configure(WebSecurity web) throws Exception {
        //web.ignoring() 用来配置忽略掉的 URL 地址,一般用于静态文件
        web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");
    }

    // (认证)配置用户及其对应的角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
    }

    // (授权)配置 URL 访问权限,对应用户的权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();//开启运行iframe嵌套页面


        //身份验证
        http.authorizeRequests()
//                .anyRequest().authenticated();//任何请求都必须经过身份验证
                .antMatchers("/vip/vip0/**").hasRole("vip0")//vip1具有的权限:只有vip1用户才可以访问包含url路径"/vip/vip0/**"
                .antMatchers("/vip/vip1/**").hasRole("vip1")//vip1具有的权限:只有vip1用户才可以访问包含url路径"/vip/vip1/**"
                .antMatchers("/vip/vip2/**").hasRole("vip2")//vip2具有的权限:只有vip2用户才可以访问url路径"/vip/vip2/**"
                .antMatchers("/vip/vip3/**").hasRole("vip3");//vip3具有的权限:只有vip3用户才可以访问url路径"/vip/vip3/**"

        //开启表单验证
        http.formLogin()
                .and()
                .formLogin()//开启表单验证
                .loginPage("/toLogin")//跳转到自定义的登录页面
                .usernameParameter("name")//自定义表单的用户名的name,默认为username
                .passwordParameter("pwd")//自定义表单的密码的name,默认为password
                .loginProcessingUrl("/doLogin")//表单请求的地址,一般与form的action属性一致,注意:不用自己写doLogin接口,只要与form的action属性一致即可
                .successForwardUrl("/index")//登录成功后跳转的页面(重定向)
                .failureForwardUrl("/toLogin")//登录失败后跳转的页面(重定向)
                .and()
                .logout()//开启注销功能
                .logoutSuccessUrl("/toLogin")//注销后跳转到哪一个页面
                .logoutUrl("/logout") // 配置注销登录请求URL为"/logout"(默认也就是 /logout)
                .clearAuthentication(true) // 清除身份认证信息
                .invalidateHttpSession(true) //使Http会话无效
                .permitAll() // 允许访问登录表单、登录接口
                .and().csrf().disable(); // 关闭csrf
    }
}

这样就完成了数据库用户的授权,测试运行,访问主页,跟之前一样任何人可以登录,因为没有对主页做限制,但是访问主页里面的vip0到vip3任意页面都需要相应的权限,如果没有会跳到登录页面进行登录认证

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_11

(6)获取当前登录用户的信息

登录授权后,很多时候都需要用户登录的用户的基本信息,比如判断用户是在线,获取当前登录用户的关联的信息等。都需要用到当前登录用户的信息,下面是获取当前登录用户信息的一种方法,主要是在控制层获取。

@GetMapping("/isLogin")
    @ResponseBody
    public Object getUserInfo(){
        if(!SecurityContextHolder.getContext().getAuthentication().getName().equals("anonymousUser")){
            //已登录
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//获取用户信息

            //获取登录的用户名
            String username = authentication.getName();
            System.out.println("username : "+username);

            //用户的所有权限
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            System.out.println("authorities : "+authorities);


            /**
             * 如果要获取更详细的用户信息可以采用下面这种方法
             */
            //用户的基本信息
            User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            System.out.println("user : "+user);

            //用户的id
            Integer userId = user.getId();
            System.out.println("userId: "+userId);

            //User其余信息可以用这种方式获取
            //List<Role> roles = user.getRoles();
            //String password = user.getPassword();
           //String username1 = user.getUsername();


            return "已登录账号:"+username;
        }else{
            //未登录
            return "请先登录";
        }

在没有登录的状态下访问上面的接口

SpringEL 菜鸟 spring security菜鸟教程_Security_18


登录灰太狼账号之后,查看页面,同时观察控制台输出

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_19


以上就是获取当前登录账号的个人信息的全部内容。

(7)前后端分离下的基于表单数据的登录验证

sql

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_20

这里新建了一个项目springboot_security4,采用springboot+mybatis-plus+security技术栈,实体类如下

@Data
@TableName("user")
public class User implements Serializable, UserDetails {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String username;
    private String nickname;
    private String password;
    private Boolean enabled;
    private String email;
    private String userface;

    @TableField(value = "create_time")//字段名与数据库字段名不一致时采用该形式进行映射
    private Date createTime;

    @TableField(value = "update_time")//字段名与数据库字段名不一致时采用该形式进行映射
    private Date updateTime;


    /**
     * 账号是否失效,true:账号有效,false账号失效。
     * @return
     */
    @Override
    @JsonIgnore//在json序列化时将pojo中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 号是否被锁,true:账号没被锁,可用;false:账号被锁,不可用
     * @return
     */
    @Override
    @JsonIgnore//在json序列化时将pojo中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 号认证是否过期,true:没过期,可用;false:过期,不可用
     * @return
     */
    @Override
    @JsonIgnore//在json序列化时将pojo中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 账号是否可用,true:可用,false:不可用
     * @return
     */
    @Override
    @JsonIgnore//在json序列化时将pojo中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
    public boolean isEnabled() {
        return this.enabled;
    }

	//如果没有设置权限的话,这里直接返回null即可
    @Override
    @JsonIgnore//在json序列化时将pojo中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    @JsonIgnore//在json序列化时将pojo中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
    public String getPassword() {
        return this.password;
    }

    @Override
    @JsonIgnore//在json序列化时将pojo中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
    public String getUsername() {
        return this.username;
    }
}

持久层mapper

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

user服务层,需要继承UserDetailsService类,并且实现里面的loadUserByUsername方法
接口

//UserServer接口
public interface UserService extends IService<User> {

}

实现类

@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {

    @Autowired
    private UserMapper userMapper;


    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Map<String,Object> map = new HashMap<>();
        map.put("username",s);

        List<User> users = userMapper.selectByMap(map);
        users.forEach(System.out::println);

        //注意这里必须保证数据库的用户名唯一
        if(users.size() == 0 || users.get(0) == null){
            System.out.println("用户为null");
            //避免返回null,这里返回一个不含有任何值的User对象,在后期的密码比对过程中一样会验证失败
            return new User();
        }

        return users.get(0);
    }

    //根据id查询用户
    public User selectById(Integer id){
        return userMapper.selectById(id);
    }
}

接下来是配置类WebSecurityConfig ,这个是核心

/**
 * @ClassName : WebSecurityConfig
 * @Description : security配置类
 * @Author : CJH
 * @Date: 2020-08-31 16:18
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserServiceImpl userService;

    /**
     * 用户认证
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());//使用BCryptPasswordEncoder进行加密
    }


    /**
     * 用户登录判断及响应
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()//所有的路径都是登录后即可访问
                .and().formLogin().loginPage("/doLogin")//如果是未登录的会自动跳到该接口(根据需要自己实现,返回页面或返回json)
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        out.write("{\"status\":\"success\",\"msg\":\"登录成功\"}");
                        out.flush();
                        out.close();
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        out.write("{\"status\":\"error\",\"msg\":\"登录失败!!\"}");
                        out.flush();
                        out.close();
                    }
                }).loginProcessingUrl("/login")//发起登录请求的接口
                .usernameParameter("username")//设置登录请求接口的参数(用户名)
                .passwordParameter("password")//设置登录请求接口的参数(密码)
                .permitAll()
                .and()
                .logout()//注销登录接口(/logout)
                .logoutUrl("/logout")
                .logoutSuccessHandler(new LogoutSuccessHandler() {//注销成功时的处理
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write("注销成功");
                        out.flush();
                        out.close();
                    }
                })
                .permitAll().and().csrf().disable().exceptionHandling()
                .accessDeniedHandler(getAccessDeniedHandler());//用户权限不足时的处理

    }

    /**
     * 用户权限不足时的处理
     *
     * @return
     */
    @Bean
    AccessDeniedHandler getAccessDeniedHandler() {
        return new AuthenticationAccessDeniedHandler();
    }
}

配置类中对应的用户权限不足时的处理器

/**
 * @ClassName : AuthenticationAccessDeniedHandler
 * @Description : security用户权限不足时的处理
 * @Author : CJH
 * @Date: 2020-08-31 16:59
 */
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        resp.setCharacterEncoding("UTF-8");
        PrintWriter out = resp.getWriter();
        out.write("权限不足,请联系管理员!");
        out.flush();
        out.close();
    }
}

控制器LoginController里面的doLogin接口对应上面的WebSecurityConfig里面配置的doLogin

@RestController
public class LoginController {

    /**
     * 如果自动跳转到这个页面,说明用户未登录,返回相应的提示即可
     * 如果要支持表单登录,可以在这个方法中判断请求的类型,进而决定返回JSON还是HTML页面
     * @return
     */
    @RequestMapping("/doLogin")
    public Map<String,String> doLogin(){
        Map<String,String> map = new HashMap<>();
        map.put("msg","尚未登录,请先登录");
        map.put("code","10001");
        return map;
    }
}
/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author 最强菜鸟
 * @since 2020-08-31
 */
@RestController
@RequestMapping("/user")
public class UserController {


    @GetMapping("/hello")
    public String hello(){
        return "hello world";
    }

    @GetMapping("/test")
    public void test(){
        System.out.println("this is test");
    }
}

上面所做的用户验证都是基于表单数据的验证,只有用表单数据(非json格式)发生post请求才有效,下面开始测试

SpringEL 菜鸟 spring security菜鸟教程_Security_21


SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_22


注意:如果用json格式发请求会验证失败,如下

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_23


注销接口

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_24

(8)前后端分离下的基于json数据的登录验证

基本的配置跟上面的(7)一样,不同的是WebSecurityConfig配置类有部分修改,以及增加了一个过滤器

/**
 * @ClassName : WebSecurityConfig
 * @Description : security配置类
 * @Author : CJH
 * @Date: 2020-08-31 16:18
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserServiceImpl userService;

    /**
     * 用户认证
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());//使用BCryptPasswordEncoder进行加密
    }



    /**
     * 基于json用户登录判断及响应
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()//所有的路径都是登录后即可访问
                .and().formLogin().loginPage("/doLogin")//如果是未登录的会自动跳到该接口(根据需要自己实现,返回页面或返回json)
                .loginProcessingUrl("/login")//发起登录请求的接口
                .usernameParameter("username")//设置登录请求接口的参数(用户名)
                .passwordParameter("password")//设置登录请求接口的参数(密码)
                .permitAll()
                .and()
                .logout()//注销登录接口(/logout)
                .logoutUrl("/logout")
                .logoutSuccessHandler(new LogoutSuccessHandler() {//注销成功时的处理
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write("注销成功");
                        out.flush();
                        out.close();
                    }
                })
                .permitAll().and().csrf().disable().exceptionHandling()
                .accessDeniedHandler(getAccessDeniedHandler());//用户权限不足时的处理

        http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

    }

    /**
     * 自定义security过滤器,以实现用post发起登录请求时,参数用json传递
     * @return
     * @throws Exception
     */
    @Bean
    CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();

        /**登录成功**/
        filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                out.write("{\"status\":\"success\",\"msg\":\"登录成功\"}");
                out.flush();
                out.close();
            }
        });

        /**登录失败**/
        filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                out.write("{\"status\":\"error\",\"msg\":\"登录失败!!\"}");
                out.flush();
                out.close();
            }
        });


        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }

    /**
     * 用户权限不足时的处理
     *
     * @return
     */
    @Bean
    AccessDeniedHandler getAccessDeniedHandler() {
        return new AuthenticationAccessDeniedHandler();
    }



}

过滤器

/**
 * @ClassName : CustomAuthenticationFilter
 * @Description : 自定义security过滤器,以实现用post发起登录请求时,参数用json传递
 * @Author : CJH
 * @Date: 2020-08-31 22:18
 */
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        //拦截请求头,可以自定义配置,如果想用表单数据也可同时用json也可以用MediaType类型配置,这里只配置了json
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authRequest = null;

            try (InputStream is = request.getInputStream()) {
                Map<String, String> authenticationBean = mapper.readValue(is, Map.class);

                authRequest = new UsernamePasswordAuthenticationToken(
                        authenticationBean.get("username"),
                        authenticationBean.get("password")
                );

            } catch (IOException e) {
                e.printStackTrace();
                authRequest = new UsernamePasswordAuthenticationToken("", "");

            } finally {
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }

        } else {
            return super.attemptAuthentication(request, response);
        }
    }
}

其余的跟上面(7)一样即可,下面开始测试

访问未登录时的接口

SpringEL 菜鸟 spring security菜鸟教程_java_25


访问登录接口

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_26


注销接口

SpringEL 菜鸟 spring security菜鸟教程_Security_27


注意,使用过滤器进行拦截可以自己根据需求配置表单数据接受或json数据接收或两者都可以接受等

三、SpringSecurity核心组件

这里列举出以下核心组件:SecurityContextSecurityContextHolderAuthenticationUserdetailsAuthenticationManager,下面开始对这些核心组件的详细介绍。

(1)Authentication

authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。Authentication也是一个接口,其定义如下:

SpringEL 菜鸟 spring security菜鸟教程_java_28


我们获取当前登录用户信息就是用的这个接口,如果有看上面入门系列的获取用户那一段,就可以知道获取用户信息也就是用的Authentication

SpringEL 菜鸟 spring security菜鸟教程_spring boot_29

(2)SecurityContext

安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中,SecurityContext的接口定义如下:

SpringEL 菜鸟 spring security菜鸟教程_spring boot_30


可以看到这里只定义了两个方法,主要都是用来获取或修改认证信息(Authentication)的,Authentication是用来存储着认证用户的信息,所以这个接口可以间接获取到用户的认证信息。还是以上面的入门系列的获取用户那一段来进行解析

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_31

(3)SecurityContextHolder

SecurityContextHolder看名字就知道跟SecurityContext实例相关的。在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。

但是在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession属性。它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolder

说到SecurityContextHolder就必须要说到一个过滤器,SecurityContextPersistenceFilter

SecurityContextPersistenceFilter:这个Filter是整个拦截过程的入口和出口 ,在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 SecurityContextHolder 所持有的SecurityContext ;

进入源码查看

SpringEL 菜鸟 spring security菜鸟教程_Security_32


同样的可以参考上面的入门系列的获取用户那一段来进行解析

SpringEL 菜鸟 spring security菜鸟教程_java_33


可以看整个登录用户的信息获取流程就十分清晰了。

(4)UserDetails

这个看着有点熟悉,在上面入门系列的基于数据库认证中,实体类User就必须实现这个接口

SpringEL 菜鸟 spring security菜鸟教程_spring boot_34


UserDetails存储的就是用户信息,其定义如下:

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_35

(5)UserDetailsService

在上面入门系列的基于数据库认证中,用户类必须要实现UserDetails接口,还需要实现UserDetailsService接口,与实体类User(实现了UserDetails接口)

相对应的还要在应用层中实现UserDetailsService接口

SpringEL 菜鸟 spring security菜鸟教程_spring boot_36


之前在入门系列中的基于数据库的认证中没有去深究其原理,到现在基本就可以知道其认证的流程,包括数据库用户的获取。

通常在spring security应用中,我们会自定义一个UserDetailsService来实现UserDetailsService接口,并实现其loadUserByUsername(final String login);方法。我们在实现loadUserByUsername方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails,(通常是一个org.springframework.security.core.userdetails.User,它继承自UserDetails) 并返回。

在实现loadUserByUsername方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException。

关于其源码估计能猜到,肯定有一个loadUserByUsername方法等着我们去实现

SpringEL 菜鸟 spring security菜鸟教程_SpringEL 菜鸟_37


通常基于数据库的认证,就要从数据库中获取要认证的用户信息,从数据库中获取用户信息就是通过服务处实现UserDetailsService接口,并重写其loadUserByUsername方法,这个方法用来获取数据库用户信息。

(6)AuthenticationManager

AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication,其定义如下:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

AuthenticationManager 的作用就是校验Authentication,如果验证失败会抛出AuthenticationException异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候。

四、部分源码解析

(1)用户认证流程

认证大致流程

关于SpringSecurity的用户认证流程,个人觉得是十分有必要了解的,尽管框架已经封装好,只要按照它定好的规则来做就好了。在查询了大量的博客和网上的大量视频讲解后,发现其实讲的基本都一样,当然有些自己还没搞懂,个人觉得任何东西如果自己不动手试一下是不可能真正懂的。

首先是大致流程,之前我想的是它可能是通过过滤器的方式去实现的拦截并且重定向到登录页面的方式进行认证的,在查阅了大量的资料发现,实现的方式确实是过滤器的方式,只不过它有很多个过滤器,形成一条过滤链,只有通过这条过滤链后才可以访问API

SpringEL 菜鸟 spring security菜鸟教程_spring boot_38


具体的验证流程可以用下图来表示

SpringEL 菜鸟 spring security菜鸟教程_java_39


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

SecurityContextPersistenceFilter:这个Filter是整个拦截过程的入口和出口 ,在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 SecurityContextHolder 所持有的SecurityContext ;

UsernamePasswordAuthenticationFilter:用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和 AuthenticationFailureHandler,这两个接口可以字配置,在上面入门系列的自定义的SecurityConfig配置类中可以自己配置

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

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

SpringEL 菜鸟 spring security菜鸟教程_spring boot_40

认证具体流程

这里推荐一篇个人觉得简单易懂认证流程的博客:

具体的认证流程需要看源码才能知道,这里引用一下之前看的视频的一张认证的图片,图片如下

SpringEL 菜鸟 spring security菜鸟教程_Security_41


从这里看到请求进来会经过UsernamePasswordAuthenticationFilter 过滤器,所以先用全局搜索(CTRL+N)找到该过滤器,并且打上断点,这里用的默认登录页面,密码用的控制台随机生成的密码

SpringEL 菜鸟 spring security菜鸟教程_spring boot_42


就像上面认证大致流程里面说的一样,用户身份的认证交给AuthenticationManager处理,AuthenticationManager又委托给DaoAuthenticationProvider 认证,所以在全局搜索找到DaoAuthenticationProvider并且打上断点进行调试

SpringEL 菜鸟 spring security菜鸟教程_java_43


注意这行代码:
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
它是通过UserDetailsService来加载要验证的用户
获取用户名后,将用户名传给preAuthenticationChecks.check()方法验证

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_44


进入preAuthenticationChecks.check(user);内部方法,可以看到有一些验证,如账号是否可用,是否被锁等等,这些参数就是上面数据库用户认证User类要认证的参数

SpringEL 菜鸟 spring security菜鸟教程_java_45


用户账号密码的验证则是由断点下面的additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);负责验证,进入该方法

SpringEL 菜鸟 spring security菜鸟教程_SpringSecurity_46


总结:

(1)UsernamePasswordAuthenticationFilter获取表单输入的用户名和密码等请求信息,并封装成Token

(2)AuthenticationManager负责将Token委托给DaoAuthenticationProvider进行认证

(3)DaoAuthenticationProvider通过UserDetailsService来加载要验证的用户

(4)最后先校验用户的账号是否被锁了等信息,再校验用户账号和密码

(5)校验成功则可以访问接口,失败则抛出异常

以上就是用户认证具体流程的全部内容,涉及到一些源码解读,有点累人,刚开始以为很复杂,但自己试着调试了一下,基本还是可以理解的。

(2)默认登录用户名与密码配置

如果成功引入Security依赖,MVC Security安全管理功能就行会自动生效,默认的安全配置是在UserDetailsServiceAutoConfiguration和SecurityAutoConfiguration中实现的,其中SecurityAutoConfiguration会导入并且自动配置,SpringBootWebSecurityConfiguration用于启动Web安全管理,UserDetailsServiceAutoConfiguration用于配置用户信息。

关于Security内部配置的用户名和密码可以进入源码查看它的配置,读源码真的是一种难受的事情。

SpringEL 菜鸟 spring security菜鸟教程_java_47