SpringSecurity快速上手

简介: SpringSecurity 本质是一个过滤器链

依赖

       <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-security</artifactId>

       </dependency>

基本原理

过滤器链

SpringSecurity 本质是一个过滤器链,有三个重要的过滤器:

  • FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。
  • ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
  • UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码

两个接口

UserDetailsService 接口

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。

如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

publicinterfaceUserDetailsService {

   UserDetailsloadUserByUsername(Stringvar1) throwsUsernameNotFoundException;

}

返回值 UserDetails ,这个类是系统默认的用户“主体”。接口实现类User

// 表示获取登录用户所有权限

Collection<?extendsGrantedAuthority>getAuthorities();

// 表示获取密码

StringgetPassword();

// 表示获取用户名

StringgetUsername();

// 表示判断账户是否过期

booleanisAccountNonExpired();

// 表示判断账户是否被锁定

booleanisAccountNonLocked();

// 表示凭证{密码}是否过期

booleanisCredentialsNonExpired();

// 表示当前用户是否可用

booleanisEnabled();

PasswordEncoder 接口

// 表示把参数按照特定的解析规则进行解析

Stringencode(CharSequencerawPassword);

// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个

参数表示存储的密码。

booleanmatches(CharSequencerawPassword, StringencodedPassword);

// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回false。默认返回 false。

defaultbooleanupgradeEncoding(StringencodedPassword) {

   returnfalse;

}

接口实现类

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。

BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10

使用示例:

@Test

publicvoidtest01(){

// 创建密码解析器

BCryptPasswordEncoderbCryptPasswordEncoder=newBCryptPasswordEncoder();

// 对密码进行加密

Stringatguigu=bCryptPasswordEncoder.encode("atguigu");

// 打印加密之后的数据

System.out.println("加密之后数据:\t"+atguigu);

//判断原字符加密后和加密之前是否匹配

booleanresult=bCryptPasswordEncoder.matches("atguigu", atguigu);

// 打印比较结果

System.out.println("比较结果:\t"+result);

}

Web 权限方案

  1. 认证
  2. 授权

查询数据库进行认证

  1. 编写userDetailsService接口的实现类并装载进容器,返回User对象,User对象有用户名密码和操作权限
  2. 创建配置类,使用返回的User对象进行认证

packagecn.upeveryday.service.impl;

@Service("userDetailsService")

publicclass_MyUserDetailsServiceImplimplementsUserDetailsService {

   @Autowired

   privateUserAuthMapperuserAuthMapper;

   List<GrantedAuthority>auths=AuthorityUtils.commaSeparatedStringToAuthorityList("role");

   @Override

   publicUserDetailsloadUserByUsername(Stringusername) throwsUsernameNotFoundException {

       //根据用户名查询数据库

       QueryWrapperwrapper=newQueryWrapper();

       wrapper.eq("username", username);

       UserAuthuserAuth=userAuthMapper.selectOne(wrapper);

       //判断

       if (userAuth==null){//数据库无此用户名,认证失败

           thrownewUsernameNotFoundException("用户名不存在!");

       }

       //权限参数不能为null

       returnnewUser(userAuth.getUsername(), newBCryptPasswordEncoder().encode(userAuth.getPassword()), auths);

   }

}

packagecn.upeveryday.config;

@Configuration

publicclassSecurityConfigextendsWebSecurityConfigurerAdapter {

   //注入我们写的实现类,内有用户名密码和操作权限

   @Autowired

   privateUserDetailsServiceuserDetailsService;

   @Override

   protectedvoidconfigure(AuthenticationManagerBuilderauth) throwsException {

       auth.userDetailsService(userDetailsService).passwordEncoder(password());

   }

   /**

    * 将密码加密器装载进容器

    * @return

    */

   @Bean

   PasswordEncoderpassword(){

       returnnewBCryptPasswordEncoder();

   }

}

其他配置

同样在配置类中

   @Override

   protectedvoidconfigure(HttpSecurityhttp) throwsException {

       // 配置认证

       http.formLogin()

               .loginPage("/index") // 配置哪个 url 为登录页面

               .loginProcessingUrl("/login") // 设置哪个是登录的 url。

               .successForwardUrl("/success") // 登录成功之后跳转到哪个 url

               .failureForwardUrl("/fail");// 登录失败之后跳转到哪个 url

       http.authorizeRequests()

               .antMatchers("/layui/**","/index") //表示配置请求路径

               .permitAll() // 指定 URL 无需保护。

               .anyRequest() // 其他请求

               .authenticated(); //需要认证

       // 关闭 csrf

       http.csrf().disable();

   }

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

hasAuthority 方法

如果当前的主体具有指定的权限,则返回 true,否则返回 false

  • 修改配置类

   @Override

   protectedvoidconfigure(HttpSecurityhttp) throwsException {

       // 配置认证

       http.formLogin()

               .loginPage("/index") // 配置哪个 url 为登录页面

               .loginProcessingUrl("/login") // 设置哪个是登录的 url。

               .successForwardUrl("/success") // 登录成功之后跳转到哪个 url

               .failureForwardUrl("/fail");// 登录失败之后跳转到哪个 url

       http.authorizeRequests()

               .antMatchers("/layui/**","/index").permitAll() //指定的 URL 无需保护

               .antMatchers("/test").hasAuthority("admin")//当前登录用户,只有具有admin权限才能访问这个路径

               .anyRequest() // 其他请求

               .authenticated(); //需要认证

       // 关闭 csrf

       http.csrf().disable();

   }

hasAnyAuthority 方法

满足任意一个权限即可访问

   @Override

   protectedvoidconfigure(HttpSecurityhttp) throwsException {

       // 配置认证

       http.formLogin()

               .loginPage("/index")

               .loginProcessingUrl("/login")

               .successForwardUrl("/success")

               .failureForwardUrl("/fail");

       http.authorizeRequests()

               .antMatchers("/layui/**","/index").permitAll()

//                .antMatchers("/test").hasAuthority("admin")

               .antMatchers("/test").hasAnyAuthority("admin,manager")//当前登录用户,具有其中一个权限即可访问这个路径

               .anyRequest()

               .authenticated();

       http.csrf().disable();

   }

hasRole 方法

如果当前主体具有指定的角色,则返回 true。

底层源码:

   privatestaticStringhasRole(Stringrole) {

       Assert.notNull(role, "role cannot be null");

       Assert.isTrue(!role.startsWith("ROLE_"), () -> {

           return"role should not start with 'ROLE_' since it is automatically inserted. Got '"+role+"'";

       });

       return"hasRole('ROLE_"+role+"')";

   }

注意:这里与添加权限不同,添加角色需要加上"ROLE_"前缀

admin是权限

ROLE_sale是角色

@Service("userDetailsService")

publicclass_MyUserDetailsServiceImplimplementsUserDetailsService {

   @Autowired

   privateUserAuthMapperuserAuthMapper;

   List<GrantedAuthority>auths=AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");

   ……

}

注意配置文件中不需要添加”ROLE_“,因为上述的底层代码会自动添加与之进行匹配

   @Override

   protectedvoidconfigure(HttpSecurityhttp) throwsException {

       // 配置认证

       http.formLogin()

               .loginPage("/index")

               .loginProcessingUrl("/login")

               .successForwardUrl("/success")

               .failureForwardUrl("/fail");

       http.authorizeRequests()

               .antMatchers("/layui/**","/index").permitAll()

//                .antMatchers("/test").hasAuthority("admin")

//                .antMatchers("/test").hasAnyAuthority("admin,manager")//当前登录用户,具有其中一个权限即可访问这个路径

               .antMatchers("/test").hasRole("sale")

               .anyRequest()

               .authenticated();

       http.csrf().disable();

   }

hasAnyRole方法

表示用户具备任何一个条件都可以访问。

注解使用

@Secured

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

使用注解先要开启注解功能!

在启动类或配置类添加注解都可

@SpringBootApplication

@MapperScan("cn.upeveryday.mapper")

@EnableGlobalMethodSecurity(securedEnabled=true)//开启security的注解功能

public class MyApplication {

   public static void main(String[] args) {

       SpringApplication.run(MyApplication.class, args);

   }

}

控制器方法上添加注解

   @RequestMapping("testSecured")

   @ResponseBody

   @Secured({"ROLE_normal","ROLE_admin"})

   public String helloUser() {

       return "hello,user";

   }

@PreAuthorize

先开启注解功能:

@EnableGlobalMethodSecurity(prePostEnabled = true)

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

四个方法都可使用

   @RequestMapping("/preAuthorize")

   @ResponseBody

   //@PreAuthorize("hasRole('ROLE_管理员')")

   @PreAuthorize("hasAnyAuthority('menu:system')")

   public String preAuthorize(){

       System.out.println("preAuthorize");

       return "preAuthorize";

   }

@PostAuthorize

先开启注解功能:

@EnableGlobalMethodSecurity(prePostEnabled = true)

@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限.

   @RequestMapping("/testPostAuthorize")

   @ResponseBody

   @PostAuthorize("hasAnyAuthority('menu:system')")

   public String preAuthorize(){

       System.out.println("test--PostAuthorize");

       return "PostAuthorize";

   }

@PostFilter

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

表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素

   @RequestMapping("getAll")

   @PreAuthorize("hasRole('ROLE_管理员')")

   @PostFilter("filterObject.username == 'admin1'")

   @ResponseBody

   public List<UserInfo> getAllUser(){

       ArrayList<UserInfo> list = new ArrayList<>();

       list.add(new UserInfo(1l,"admin1","6666"));

       list.add(new UserInfo(2l,"admin2","888"));

       return list;

   }

@PreFilter

@PreFilter: 进入控制器之前对数据进行过滤

   @RequestMapping("getTestPreFilter")

   @PreAuthorize("hasRole('ROLE_管理员')")

   @PreFilter(value = "filterObject.id%2==0")

   @ResponseBody

   public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo>

                                                  list){

       list.forEach(t-> {

           System.out.println(t.getId()+"\t"+t.getUsername());

       });

       return list;

   }

用户注销

在配置类中添加退出映射地址

http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll();

自动登录

创建表

CREATE TABLE `persistent_logins` (

`username` varchar(64) NOT NULL,

`series` varchar(64) NOT NULL,

`token` varchar(64) NOT NULL,

`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE

CURRENT_TIMESTAMP,

PRIMARY KEY (`series`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

修改安全配置类

package cn.upeveryday.config;


@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

   //注入我们写的实现类,内有用户名密码和操作权限

   @Autowired

   private UserDetailsService userDetailsService;


   @Override

   protected void configure(AuthenticationManagerBuilder auth) throws Exception {

       auth.userDetailsService(userDetailsService).passwordEncoder(password());

   }

   /**

    * 将密码加密器装载进容器

    * @return

    */

   @Bean

   PasswordEncoder password(){

       return new BCryptPasswordEncoder();

   }

//---------------------------------------------------------------------------

   //注入yaml配置的数据源(记住我的操作)

   @Autowired

   private DataSource dataSource;

   @Bean

   public PersistentTokenRepository persistentTokenRepository(){

       JdbcTokenRepositoryImpl jdbcTokenRepository = new

               JdbcTokenRepositoryImpl();

       // 赋值数据源

       jdbcTokenRepository.setDataSource(dataSource);

       // 自动创建表,第一次执行会创建,以后要执行就要删除掉!

       jdbcTokenRepository.setCreateTableOnStartup(true);

       return jdbcTokenRepository;

   }



   @Override

   protected void configure(HttpSecurity http) throws Exception {

       // 配置认证

       http.formLogin()

               .loginPage("/index")

               .loginProcessingUrl("/login")

               .successForwardUrl("/success")

               .failureForwardUrl("/fail");

       http.authorizeRequests()

               .antMatchers("/layui/**","/index").permitAll()

//                .antMatchers("/test").hasAuthority("admin")

//                .antMatchers("/test").hasAnyAuthority("admin,manager")//当前登录用户,具有其中一个权限即可访问这个路径

               .antMatchers("/test").hasRole("sale")

               .anyRequest()

               .authenticated()

            //设置记住我的操作

               .and().rememberMe().tokenRepository(persistentTokenRepository())//s

               .userDetailsService(userDetailsService)//指定用户

               .tokenValiditySeconds(60);//设置有效时长,单位s

   }

}

页面添加记住我复选框

此处:name 属性值必须位 remember-me.不能改为其他值

记住我:<input type="checkbox"name="remember-me"title="记住密码"/><br/>

登录结果返回json

由于前后端分离,我们登录成功或失败,只需返回json数据

步骤:

导入fastjson依赖

       <!--fastJson-->

       <dependency>

           <groupId>com.alibaba</groupId>

           <artifactId>fastjson</artifactId>

           <version>1.2.62</version>

       </dependency>

编写配置类

  • cn.upeveryday.config.SecurityConfig

认证过程需要successHandler和failureHandler,查看源码,分别是AuthenticationSuccessHandler接口和AuthenticationFailureHandler接口

package cn.upeveryday.config;


@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

   //注入我们写的实现类,内有用户名密码和操作权限

   @Autowired

   private UserDetailsService userDetailsService;


   @Override

   protected void configure(AuthenticationManagerBuilder auth) throws Exception {

       auth.userDetailsService(userDetailsService).passwordEncoder(password());

   }

   /**

    * 将密码加密器装载进容器

    * @return

    */

   @Bean

   PasswordEncoder password(){

       return new BCryptPasswordEncoder();

   }


//--------------------------------------------

   //注入处理器

   @Autowired

   private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;


   @Autowired

   private AuthenticationFailHandlerImpl authenticationFailHandler;


   @Override

   protected void configure(HttpSecurity http) throws Exception {

       // 对认证进行配置

       http.formLogin()

               .loginProcessingUrl("/login")

               .successHandler(authenticationSuccessHandler)

               .failureHandler(authenticationFailHandler)

       ;

       http.csrf().disable();

   }

}

编写接口实现类

  • cn.upeveryday.handler.AuthenticationSuccessHandlerImpl

package cn.upeveryday.handler;


/**

* 登录成功处理

*/

@Component

public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {

   @Override

   public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

       //返回json数据

       httpServletResponse.setContentType("application/json;charset=UTF-8");

       httpServletResponse.getWriter().write(JSON.toJSONString(Result.success().message(ResultInfo.LOGIN_SUCCESS)));

   }

}

  • cn.upeveryday.handler.AuthenticationFailHandlerImpl

package cn.upeveryday.handler;


/**

* 登录失败处理

*/

@Component

public class AuthenticationFailHandlerImpl implements AuthenticationFailureHandler {

   @Override

   public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {

       //返回json数据

       httpServletResponse.setContentType("application/json;charset=UTF-8");

       httpServletResponse.getWriter().write(JSON.toJSONString(Result.error().message(ResultInfo.LOGIN_FAILURE)));

   }

}

在module中通过mapState、mapGetters、mapActions和mapMutations等辅助函数来绑定要触发的函数



目录
相关文章
|
4月前
|
人工智能 JavaScript Devops
云效 MCP Server:AI 驱动的研发协作新范式
云效MCP Server是阿里云云效平台推出的模型上下文协议(Model Context Protocol)标准化接口系统,作为AI助手与DevOps平台的核心桥梁。通过该协议,AI大模型可无缝集成云效DevOps平台,直接访问和操作包括项目管理、代码仓库、工作项等关键研发资产,实现智能化全生命周期管理。其功能涵盖代码仓库管理、代码评审、项目管理和组织管理等多个方面,支持如创建分支、合并请求、查询工作项等具体操作。用户可通过通义灵码内置的MCP市场安装云效MCP服务,并配置个人访问令牌完成集成。实际场景中,AI助手可自动分析需求、生成代码、创建功能分支并提交合并请求,极大提升研发效率。
聊天框(番外篇)—如何实现@功能的整体删除
上一篇文章中,我们已经初步实现了聊天输入框,但其@功能是不完善的,例如无法整体删除、无法获取除用户名以外的数据(假设用户名不是唯一的)。有问题就要想办法解决,在网上百度了一圈后,倒是有一些收获。本文就着重解决@的整体删除以及获取额外数据。
1380 0
聊天框(番外篇)—如何实现@功能的整体删除
|
数据可视化 编译器 C语言
Qt实战:Qt5.11.1安装与MSVC配置
Qt实战:Qt5.11.1安装与MSVC配置
1827 1
Qt实战:Qt5.11.1安装与MSVC配置
|
监控 小程序 前端开发
微服务架构 Microservice 的典型应用场景 | 学习笔记
快速学习微服务架构 Microservice 的典型应用场景。
微服务架构 Microservice 的典型应用场景  | 学习笔记
|
12天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1255 5
|
1天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
11天前
|
机器学习/深度学习 人工智能 前端开发
通义DeepResearch全面开源!同步分享可落地的高阶Agent构建方法论
通义研究团队开源发布通义 DeepResearch —— 首个在性能上可与 OpenAI DeepResearch 相媲美、并在多项权威基准测试中取得领先表现的全开源 Web Agent。
1275 87