当前位置: 代码迷 >> 综合 >> SpringSecurity 学习笔记
  详细解决方案

SpringSecurity 学习笔记

热度:17   发布时间:2023-12-03 23:36:56.0

文章目录

  • 一、基本概念
    • 1.认证
    • 2.会话
    • 3.授权
      • 3.1 授权方式
  • 二、与`Shiro`对比
    • 1.`Shiro`
    • 2.`Spring Security`
  • 三、`Spring Security`原理
    • 1.重点理解过滤器
    • 2.`UserDetailsService`
    • 2.`PasswordEncoder`
  • 四、`SpringSecurity Web` 权限方案
    • 1. 设置登录系统的账号、密码
      • 1.1 方式一:通过配置文件修改
      • 1.2 方式二: 通过配置类自定义内存用户
    • 2.实现数据库认证来完成用户登录
      • 2.1 整合`Mybatis-plus`
        • 2.1.1 maven 依赖
        • 2.1.2 配置类
        • 2.1.3 项目配置文件配置
      • 2.2 准备`SQL`
      • 2.3 准备用户实体
      • 2.4 用户查询服务
      • 2.5 配置自定义登录
    • 3.自定义登录页
      • 3.1 前端页面
      • 3.2 控制器
      • 3.3 配置
    • 4.授权配置
    • 5.基于角色或权限进行访问控制
    • 6. 基于数据库完成权限认证
      • 6.1 完整`SQL`
      • 6.2 关键代码
    • 7.自定义403页面
    • 3.注解使用
      • 3.1 @Secured
      • 3.2 @PreAuthorize
      • 3.3 @PostAuthorize
      • 3.4 @PostFilter
      • 3.5 @PreFilter
    • 4.基于数据库的记住我
      • 4.1配置`PersistentTokenRepository`
      • 4.2 `configure(HttpSecurity http)`
    • 5.用户注销
    • 6.`CSRF`
    • 7.自定义成功失败处理
      • 7.1自定义成功处理
      • 7.2自定义失败处理
      • 7.3 配置
    • 8.添加验证码
      • 8.1 前端准备
      • 8.2 `controller`准备
      • 8.3 自定义验证码错误异常
      • 8.4 验证码过滤器
  • 五、测试
    • 5.1 登录页
    • 5.2 错误提示
    • 5.3访问资源
    • 5.4 未授权页面
    • 5.5 记住我
    • 5.6 退出登录
  • 六、存在问题
  • 八、完整代码地址

一、基本概念

1.认证

用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。

2.会话

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

3.授权

授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

3.1 授权方式

  • RBAC基于资源的访问控制

RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权。可扩展性好,当人员角色发生改变时无需修改代码。

  • RBAC基于角色的访问控制

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

二、与Shiro对比

1.Shiro

Apache 旗下的轻量级权限控制框架

  • 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求
    的互联网应用有更好表现
  • 通用性
    • 不局限于 Web 环境,可以脱离 Web 环境使用
    • 在 Web 环境下一些特定的需求需要手动编写代码定制

2.Spring Security

Spring Security 是 Spring 家族中的一个安全管理框架

  • 和 Spring 无缝整合
  • 全面的权限控制
  • 专门为 Web 开发而设计
    • 旧版本不能脱离 Web 环境使用
    • 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独
      引入核心模块就可以脱离 Web 环境。
  • 重量级

三、Spring Security原理

Spring Security 本质是一个过滤器链

1.重点理解过滤器

  • FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部

    • super.beforeInvocation(fi) 表示查看之前的 filter 是否通过
    • fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务
    • ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
  • UsernamePasswordAuthenticationFilter:对/login 的 POST 请求做拦截,校验表单中用户 名,密码

2.UserDetailsService

完成用户名、密码、角色权限的认证服务接口

  • 返回值 UserDetails :系统默认的用户的主体 :用户信息
  • 方法参数username:通过用户名查询用户信息

2.PasswordEncoder

Spring Security 的密码加密解密器

  • BCryptPasswordEncoder:BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单 向加密。可以通过 strength 控制加密强度,默认 10。

  • 示例

        /*** 密码编码器测试*/@Testvoid bCryptPasswordEncoderTest(){
          BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);String encodePassword = bCryptPasswordEncoder.encode("123456");log.info("加密之后的密码:{}",encodePassword);boolean matches = bCryptPasswordEncoder.matches("123456",encodePassword);log.info("密码比较结果:{}",matches);}
    

四、SpringSecurity Web 权限方案

1. 设置登录系统的账号、密码

默认用户名为:user,密码见项目启动之后的控制台打印。每次都不一样。

1.1 方式一:通过配置文件修改

  security:user:name: dingwenpassword: 123456

1.2 方式二: 通过配置类自定义内存用户

package com.dingwen.spsest.config;import com.dingwen.spsest.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;/*** security 配置类** @author dingwen* 2021.05.17 15:31*/@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /*** 用户详细信息服务** @return {@link UserDetailsService}*/@Beanpublic UserDetailsService userDetailsService() {
    // 内存用户测试InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();// 逗号分隔的字符串到授权列表List<GrantedAuthority> authorityList = Authority    Utils.commaSeparatedStringToAuthorityList("admin,user");// 内存用户inMemoryUserDetailsManager.createUser(User.withUsername("dingwen").password(bCryptPasswordEncoder().encode("123456")).authorities(authorityList).build());return inMemoryUserDetailsManager;}/*** 配置** @param http http* @throws Exception 异常*/@Overrideprotected void configure(HttpSecurity http) throws Exception {
    http.formLogin() //表单登录.and().authorizeRequests() // 认证配置.anyRequest() // 任何请求.authenticated(); // 都需要认证通过}
}

2.实现数据库认证来完成用户登录

项目结构

在这里插入图片描述

2.1 整合Mybatis-plus

2.1.1 maven 依赖

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.5</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.dingwen</groupId><artifactId>spring-security-study</artifactId><version>1.0</version><name>spring-security-study</name><description>spring-security-study</description><properties><java.version>1.8</java.version><mybatis.plus.boot.starter.version>3.4.2</mybatis.plus.boot.starter.version><mysql.connector.java.version>8.0.15</mysql.connector.java.version></properties><dependencies><!--boot--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--security--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.connector.java.version}</version></dependency><!--mybatis - plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis.plus.boot.starter.version}</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

2.1.2 配置类

package com.dingwen.spsest.config;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** mybatis-plus 配置** @author dingwen* 2021.05.11 17:00*/
@Configuration
// 注意此处配置了包扫描,无需再启动类中再配置
@MapperScan("com.dingwen.spsest.mapper")
public class MybatisPlusConfig {
    @Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){
    MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();// 自动分页插件mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));// 防止全表更新、删除插件mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());return  mybatisPlusInterceptor;}
}

2.1.3 项目配置文件配置

spring:datasource:url: jdbc:mysql://192.168.233.128:3306/spring-security-study?characterEncoding=utf-8&useSSL=true&serverTimezone=GMTdriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456mybatis-plus:configuration:map-underscore-to-camel-case: trueglobal-config:banner: falsemapper-locations: classpath*:mapper/*.xmllogging:config: classpath:log/logback-spring.xml

2.2 准备SQL

-- 用户表-- 密码: 123456
create table if not exists `sss_user`(`id` bigint primary key auto_increment comment '用户表主键ID',`username` varchar(20) unique not null comment '用户名',`password` varchar(100) not null comment '密码'
)engine=innodb auto_increment=10 default charset=utf8 comment='用户表';insert into `sss_user` (`username`,`password`) values
('admin','$2a$10$U1Ef55PpIqaUHBqmip8Lc.ld22RtMRtNEsVFLk0kw5XTIBCAm84Eu'),
('xiaoming','$2a$10$U1Ef55PpIqaUHBqmip8Lc.ld22RtMRtNEsVFLk0kw5XTIBCAm84Eu'),
('lihua','$2a$10$U1Ef55PpIqaUHBqmip8Lc.ld22RtMRtNEsVFLk0kw5XTIBCAm84Eu'),
('lucy','$2a$10$U1Ef55PpIqaUHBqmip8Lc.ld22RtMRtNEsVFLk0kw5XTIBCAm84Eu');

2.3 准备用户实体

package com.dingwen.spsest.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;import java.io.Serializable;/*** 用户实体** @author dingwen* 2021.05.18 11:07*/
@Getter
@Setter
@ToString
@TableName(value = "sss_user")
public class UserEntity implements Serializable {
    private static final long serialVersionUID = 9127742447760082647L;/*** id*/@TableId(value = "id",type = IdType.AUTO)private Integer id;/*** 用户名*/@TableField(value = "username")private String username;/*** 密码*/@TableField("password")private String password;}

2.4 用户查询服务

package com.dingwen.spsest.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dingwen.spsest.entity.MenuEntity;
import com.dingwen.spsest.entity.RoleEntity;
import com.dingwen.spsest.entity.UserEntity;
import com.dingwen.spsest.mapper.MenuMapper;
import com.dingwen.spsest.mapper.RoleMapper;
import com.dingwen.spsest.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;
import java.util.Optional;/*** user service impl** @author dingwen* 2021.05.18 11:15*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    /*** 用户映射器*/private final UserMapper userMapper;@Autowiredpublic UserDetailsServiceImpl(UserMapper userMapper) {
    this.userMapper = userMapper;}/*** 根据用户名查询用户** @param username 用户名* @return {@link UserDetails}* @throws UsernameNotFoundException 找不到用户*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserEntity userEntity = userMapper.selectOne(new QueryWrapper<UserEntity>().eq("username", username));Optional.ofNullable(userEntity).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList("admin");return User.withUsername(userEntity.getUsername()).password(userEntity.getPassword()).authorities(authorityList).build();}
}

2.5 配置自定义登录

注意:自定义userDetailsService时不设置AuthenticationManagerBuilder会出现 “No AuthenticationProvider found”错误。

package com.dingwen.spsest.config;import com.dingwen.spsest.mapper.MenuMapper;
import com.dingwen.spsest.mapper.RoleMapper;
import com.dingwen.spsest.mapper.UserMapper;
import com.dingwen.spsest.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;/*** security 配置类** @author dingwen* 2021.05.17 15:31*/@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /*** 用户映射器*/private final UserMapper userMapper;@AutowiredSecurityConfig(UserMapper userMapper) {
    this.userMapper = userMapper;}/*** 密码编码器** @return {@link BCryptPasswordEncoder}*/@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder(10); // strength: 加密强度[-1,31]}/*** 用户详细信息服务** @return {@link UserDetailsService}*/@Beanpublic UserDetailsService userDetailsService() {
    //数据库用户查询return new UserDetailsServiceImpl(userMapper,roleMapper,menuMapper);}/*** 配置** @param http http* @throws Exception 异常*/@Overrideprotected void configure(HttpSecurity http) throws Exception {
    http.formLogin() //表单登录.and().authorizeRequests() // 认证配置.anyRequest() // 任何请求.authenticated(); // 都需要认证}/*** 配置* 自定义 userDetailsService时不设置AuthenticationManagerBuilder会出现 “No AuthenticationProvider found” 错误** @param auth 身份验证* @throws Exception 异常*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService()).passwordEncoder(bCryptPasswordEncoder());}
}

3.自定义登录页

3.1 前端页面

注意:请求url为/login,请求方法必须为POST,表单提交变量为username,password。其他变量名称需要单独设置。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登录</title>
</head>
<body>
<form action="/login" method="post"><label>用户名:<input type="text" name="username"></label><br/><label>密码:<input type="password" name="password"></label><br/><button type="submit">登录</button>
</form>
</body>
</html>

3.2 控制器

 /*** 登录** @return {@link String}*/@GetMapping("/index")public String index() {
    return "login";}

3.3 配置

    /*** 配置** @param http http* @throws Exception 异常*/@Overrideprotected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // 配置登录.loginPage("/index") // 登录页面的请求url(跳转页面).loginProcessingUrl("/login") // 哪个请求是登录页面的url(提交表单).successForwardUrl("/success") // 登录成功之后需要跳转的url
// .usernameParameter("自定义用户名变量名称")
// .passwordParameter("自定义密码变量名称").failureForwardUrl("/fail"); // 登录失败之后需要跳转的url}

4.授权配置

    /*** 配置** @param http http* @throws Exception 异常*/@Overrideprotected void configure(HttpSecurity http) throws Exception {
    http.csrf() //跨站请求伪造.disable()// 关闭跨站请求伪造.authorizeRequests() // 认证配置.antMatchers("/index").permitAll() // 指定url放行.antMatchers("/find").hasAuthority("menu:user").antMatchers("/test1").hasAnyRole("普通用户","管理员").antMatchers("/findAll").hasRole("管理员").antMatchers("/test2").hasAnyAuthority("menu:user","menu:test").anyRequest() // 其他请求.authenticated();//都需要认证通过}

5.基于角色或权限进行访问控制

  • hasAuthority 方法: 如果当前的主体具有指定的权限,则返回 true,否则返回 false
  • hasAnyAuthority : 如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true
  • hasRole 方法: 如果用户具备给定角色就允许访问,否则出现 403
  • hasAnyRole: 表示用户具备任何一个条件都可以访问

6. 基于数据库完成权限认证

6.1 完整SQL

-- 用户表-- 密码: 123456
create table if not exists `sss_user`(`id` bigint primary key auto_increment comment '用户表主键ID',`username` varchar(20) unique not null comment '用户名',`password` varchar(100) not null comment '密码'
)engine=innodb auto_increment=10 default charset=utf8 comment='用户表';insert into `sss_user` (`username`,`password`) values
('admin','$2a$10$U1Ef55PpIqaUHBqmip8Lc.ld22RtMRtNEsVFLk0kw5XTIBCAm84Eu'),
('xiaoming','$2a$10$U1Ef55PpIqaUHBqmip8Lc.ld22RtMRtNEsVFLk0kw5XTIBCAm84Eu'),
('lihua','$2a$10$U1Ef55PpIqaUHBqmip8Lc.ld22RtMRtNEsVFLk0kw5XTIBCAm84Eu'),
('lucy','$2a$10$U1Ef55PpIqaUHBqmip8Lc.ld22RtMRtNEsVFLk0kw5XTIBCAm84Eu');select * from `sss_user`delete from `sss_user`-- 角色表create table if not exists `sss_role` (`id` bigint primary key auto_increment comment '角色ID',`name` varchar(20) comment '角色名称'
)engine=innodb auto_increment=20 default charset=utf8 comment '角色表';insert into `sss_role` (`name`) values ('普通用户'),('管理员');select * from `sss_role`-- 用户角色关系表
create table if not exists `sss_role_user`(user_id bigint comment '用户ID',role_id bigint comment '角色ID'
)engine=innodb auto_increment=20 default charset=utf8 comment '用户角色关系表';insert into `sss_role_user`values (14,21),(17,20);select * from `sss_role_user`-- 菜单create table if not exists `sss_menu`(`id` bigint primary key auto_increment comment '菜单表主键ID',`name` varchar(20) comment '菜单名称',`url` varchar(100) comment '地址',`parentId` bigint comment '父节点ID',`permission` varchar(20) comment '权限'
)engine=innodb auto_increment=20 default charset=utf8 comment '菜单表';insert into `sss_menu` values  (1,'系统管理','',0,'menu:system'),(2,'用户管理','',0,'menu:user');select * from `sss_menu`-- 菜单角色表create table if not exists `sss_menu_role`(menu_id bigint comment '菜单ID',role_id bigint comment '角色ID'
)engine=innodb auto_increment=30 default charset=utf8 comment '菜单角色表';insert into `sss_menu_role` values (1,21),(2,21),(2,20);select * from `sss_menu_role`-- 根据用户ID查询角色信息select role.* from `sss_role` role  inner join `sss_role_user` rous on rous.role_id = role.id 
where rous.user_id = '17'  -- 14 /17-- 根据用户ID查询权限信息select menu.* from `sss_menu` menu inner join `sss_menu_role` mero on menu.id = mero.menu_id inner join `sss_role` role on role.id = mero.role_idinner join `sss_role_user` rous on  rous.role_id = mero.role_idwhere rous.user_id = '14'

6.2 关键代码

package com.dingwen.spsest.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dingwen.spsest.entity.MenuEntity;
import com.dingwen.spsest.entity.RoleEntity;
import com.dingwen.spsest.entity.UserEntity;
import com.dingwen.spsest.mapper.MenuMapper;
import com.dingwen.spsest.mapper.RoleMapper;
import com.dingwen.spsest.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;
import java.util.Optional;/*** user service impl** @author dingwen* 2021.05.18 11:15*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    /*** 用户映射器*/private final UserMapper userMapper;/*** 角色映射器*/private final RoleMapper roleMapper;/*** 菜单映射器*/private final MenuMapper menuMapper;@Autowiredpublic UserDetailsServiceImpl(UserMapper userMapper,RoleMapper roleMapper,MenuMapper menuMapper) {
    this.userMapper = userMapper;this.roleMapper = roleMapper;this.menuMapper = menuMapper;}/*** 根据用户名查询用户** @param username 用户名* @return {@link UserDetails}* @throws UsernameNotFoundException 找不到用户*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserEntity userEntity = userMapper.selectOne(new QueryWrapper<UserEntity>().eq("username", username));Optional.ofNullable(userEntity).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));// 获取用户角色、菜单列表List<RoleEntity> roleEntityList = roleMapper.selectRoleByUserId(userEntity.getId());List<MenuEntity> menuEntityList = menuMapper.selectMenuByUserId(userEntity.getId());// 权限列表List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();roleEntityList.forEach(roleEntity -> {
    grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_" + roleEntity.getName()));});menuEntityList.forEach(menuEntity -> {
    grantedAuthorityList.add(new SimpleGrantedAuthority(menuEntity.getPermission()));});return User.withUsername(userEntity.getUsername()).password(userEntity.getPassword()).authorities(grantedAuthorityList).build();}
}

7.自定义403页面

    /*** 配置** @param http http* @throws Exception 异常*/@Overrideprotected void configure(HttpSecurity http) throws Exception {
    /*http.formLogin() //表单登录.and().authorizeRequests() // 认证配置.anyRequest() // 任何请求.authenticated(); // 都需要认证通过*/// 自定义403页面http.exceptionHandling().accessDeniedPage("/unauth");}

3.注解使用

3.1 @Secured

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。

  • 开启 :@EnableGlobalMethodSecurity(securedEnabled=true)(启动类或者配置类)
  • 使用
    /*** ·@secured ·注解测试测试** @return {@link String}*/@GetMapping("/secured")@ResponseBody@Secured({
    "ROLE_管理员","ROLE_user"})public String securedTest(){
    return "secured test";}

3.2 @PreAuthorize

注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用
户的 roles/permissions 参数传到方法中

  • 开启: @EnableGlobalMethodSecurity(prePostEnabled = true)
  • 使用
    /*** .@PreAuthorize 注解测试** @return {@link String}*/@GetMapping("/preAuthorize")@ResponseBody
// @PreAuthorize("hasRole('ROLE_管理员')")@PreAuthorize("hasAnyAuthority('menu:system')")public String preAuthorizeTest(){
    return "preAuthorize test";}

3.3 @PostAuthorize

在方法执行后再进行权限验证,适合验证带有返回值
的权限

  • 开启 @EnableGlobalMethodSecurity(prePostEnabled = true)

  • 使用

/*** .@PostAuthorize 注解测试** @return {@link String}*/@GetMapping("/postAuthorize")@ResponseBody@PostAuthorize("hasAnyAuthority('menu:user')")public String postAuthorizeTest(){
    return "postAuthorize test";}

3.4 @PostFilter

权限验证之后对数据进行过滤 留下用户名是 dingwen的数据

  • 开启 @EnableGlobalMethodSecurity(prePostEnabled = true)

  • 使用

    /*** `@PostFilter 注解测试** @return {@link List<UserEntity>}*/@GetMapping("/postFilter")@ResponseBody@PreAuthorize("hasAnyRole('ROLE_管理员')")@PostFilter("filterObject.username == 'dingwen'")public List<UserEntity> postFilterTest() {
    List<UserEntity> userEntityList = new ArrayList<>();userEntityList.add(UserEntity.builder().username("test").build());userEntityList.add(UserEntity.builder().username("dingwen").build());return userEntityList;}

3.5 @PreFilter

进入控制器之前对数据进行过滤,可对入参进行过滤

  • 开启 @EnableGlobalMethodSecurity(prePostEnabled = true)

  • 使用

    /*** `@PreFilter 注解测试** @param userEntityList 用户实体列表*/@GetMapping("/preFilter")@ResponseBody@Secured({
    "ROLE_管理员"})@PreFilter(value = "filterObject.username == 'dingwen'")public void preFilterTest(List<UserEntity> userEntityList){
    System.out.println();}

4.基于数据库的记住我

4.1配置PersistentTokenRepository

    /*** 基于数据库的记住我功能实现** @return {@link PersistentTokenRepository}*/@Beanpublic PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();// 设置数据源jdbcTokenRepository.setDataSource(dataSource);// 自动创建表,第一次执行会创建,以后要执行就要删除掉!jdbcTokenRepository.setCreateTableOnStartup(true);return jdbcTokenRepository;}

4.2 configure(HttpSecurity http)

 // 基于数据库的记住我http.rememberMe().tokenValiditySeconds(60 * 10) // 设置有效期,单位秒.tokenRepository(persistentTokenRepository()).userDetailsService(userDetailsService());

###4.3前端准备

<input class="input-checkbox100" id="ckb1" type="checkbox" name="remember-me" value="true">

5.用户注销

        // 退出登录.logoutUrl("/logout").logoutSuccessUrl("/index")// .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) //crsf.permitAll();

6.CSRF

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已 登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。 跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个 自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买 商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的 浏览器,却不能保证请求本身是用户自愿发出的。

可以理解为非用户本人利用服务器存储到浏览器的认证信息进行操作。

                <!--CSRF 防护配置--><input type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}"name="_csrf"/>
// http.csrf().disable();

7.自定义成功失败处理

7.1自定义成功处理

目前达到禁用SpringSecurity登录失败重定向到登录页

package com.dingwen.spsest.handler;import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 登录成功处理** @author dingwen* 2021.06.01 09:10*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
    }
}

7.2自定义失败处理

package com.dingwen.spsest.handler;import cn.hutool.http.HttpStatus;
import com.dingwen.spsest.exception.ValidateCodeException;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;/*** 自定义认证失败处理** @author dingwen* 2021.05.20 09:24*/
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    /*** 在身份验证失败** @param httpServletRequest http servlet请求* @param httpServletResponse http servlet响应* @param e e* @throws IOException io exception* @throws ServletException servlet异常*/@Overridepublic void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
    httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);String msg = "";// 验证码自定义异常的处理if (e instanceof ValidateCodeException) {
    msg = e.getMessage();} else if (e instanceof LockedException) {
    msg = "账户被锁定请联系管理员!";} else if (e instanceof CredentialsExpiredException) {
    msg = "密码过期请联系管理员!";} else if (e instanceof AccountExpiredException) {
    msg = "账户过期请联系管理员!";} else if (e instanceof DisabledException) {
    msg = "账户被禁用请联系管理员!";} else if (e instanceof BadCredentialsException) {
    msg = "用户名或密码输入错误,请重新输入!!";}PrintWriter printWriter = httpServletResponse.getWriter();printWriter.write(msg);printWriter.flush();printWriter.close();}
}

7.3 配置

    /*** HttpSecurity 配置** @param http http* @throws Exception 异常*/@Overrideprotected void configure(HttpSecurity http) throws Exception {
    // 添加验证码过滤http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);http.formLogin() // 配置登录.loginPage("/index") // 登录页面的请求url(跳转页面).loginProcessingUrl("/login") // 哪个请求是登录页面的url(提交表单).successForwardUrl("/success") // 登录成功之后需要跳转的url
// .usernameParameter("自定义用户名变量名称")
// .passwordParameter("自定义密码变量名称")
// .failureUrl("/fail")// 登录失败之后需要跳转的url.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailureHandler); //登录失败处理}

8.添加验证码

本案例使用hutool生成验证码,在后端完成校验

8.1 前端准备

<div class="wrap-input100 validate-input m-b-18" data-validate="验证码不能为空"><span class="label-input100">验证码</span><input class="input100" name="code" placeholder="请输入验证码" type="text"><span class="focus-input100"></span></div><div class="div-img"><img id="img" style="width:60%;height:40%" alt="验证码" src="/code/get">
</div>
    /** 为了使每次生成图片不一致,即不让浏览器读缓存,所以需要加上时间戳* */function changeUrl(url) {
    let timestamp = (new Date()).valueOf();url = url + "".substring(0, 17);if ((url.indexOf("&") >= 0)) {
    url = url + "×tamp=" + timestamp;} else {
    url = url + "?timestamp=" + timestamp;}return url;}function changeImg() {
    let imgSrc = $("#img");let src = imgSrc.attr("src");imgSrc.attr("src", changeUrl(src));}$(".div-img").on('click',function () {
    changeImg();})

8.2 controller准备

package com.dingwen.spsest.controller;import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
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.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.io.IOException;/*** 验证码 controller** @author dingwen* 2021.05.19 17:15*/
@RestController
@RequestMapping("/code")
@Slf4j
public class CodeController {
    public static final String CODE_KEY = "code";/*** 获取验证码** @param session 会话* @param response 响应*/@GetMapping("/get")public void get(HttpSession session, HttpServletResponse response) {
    //定义图形验证码的长、宽、验证码字符数、干扰元素个数CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);session.setAttribute(CODE_KEY, captcha.getCode());response.setContentType(MediaType.IMAGE_JPEG_VALUE);// 禁用页面缓存response.setHeader("Pragma", "No-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expire", 0);try {
    captcha.write(response.getOutputStream());} catch (IOException e) {
    e.printStackTrace();}}
}

8.3 自定义验证码错误异常

	package com.dingwen.spsest.exception;import org.springframework.security.core.AuthenticationException;/*** 验证码验证异常类** @author dingwen* 2021.05.20 09:19*/
public class ValidateCodeException extends AuthenticationException {
    private static final long serialVersionUID = -2425114031010495169L;public ValidateCodeException(String msg){
    super(msg);}
}

8.4 验证码过滤器

package com.dingwen.spsest.filter;import com.dingwen.spsest.controller.CodeController;
import com.dingwen.spsest.exception.ValidateCodeException;
import com.dingwen.spsest.handler.MyAuthenticationFailureHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;/*** 验证代码过滤* 验证码验证过滤器** @author dingwen* 2021.05.19 17:28* @date 2021/05/19*/
@Component
public class VerifyCodeFilter extends GenericFilterBean {
    private final MyAuthenticationFailureHandler myAuthenticationFailureHandler;@AutowiredVerifyCodeFilter(MyAuthenticationFailureHandler myAuthenticationFailureHandler) {
    this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;}@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;String defaultFilterProcessUrl = "/login";try {
    if (HttpMethod.POST.name().equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getServletPath())) {
    // 验证码验证String code = request.getParameter("code");String genCaptcha = (String) request.getSession().getAttribute(CodeController.CODE_KEY);Optional.ofNullable(code).orElseThrow(() -> new ValidateCodeException("验证码不能为空!"));if (!genCaptcha.toLowerCase().equals(code.toLowerCase())) {
    throw new ValidateCodeException("验证码错误");}}} catch (AuthenticationException authenticationException) {
    // 捕获异常交给失败处理器处理myAuthenticationFailureHandler.onAuthenticationFailure(request, response, authenticationException);}chain.doFilter(request, response);}
}

五、测试

5.1 登录页

在这里插入图片描述

5.2 错误提示

在这里插入图片描述
在这里插入图片描述

5.3访问资源

在这里插入图片描述

5.4 未授权页面

在这里插入图片描述

5.5 记住我

在有效期内,即使关闭浏览器也无需登录,用户信息存储到了数据库
在这里插入图片描述

5.6 退出登录

http://localhost:8080/logout

如果出现404参照这篇文章。
https://editor.csdn.net/md?articleId=117016004

六、存在问题

在认证失败的处理器中自定义了失败消息,没能返回到前端登录页给用户做相对应的提示.

八、完整代码地址

https://gitee.com/dingwen-gitee/spring-security-study

  相关解决方案