Spring Security(基础笔记1)
前言
认证:
身份验证是验证用户身份的过程。也就是说,当用户通过应用程序进行身份验证时,他们在证明自己实际上就是他们所说的身份。
授权:
授权本质上是访问控制-控制用户可以在应用程序中访问的内容(例如资源,网页等)。大多数用户通过使用角色和权限等概念来执行访问控制。也就是说,通常根据分配给他们的角色和/或权限,允许用户执行某项操作或不执行某项操作。
一、Spring Security
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义需求
为Spring IO Platform提供安全服务
二、特征
- 对身份验证和授权的全面且可扩展的支持(认证 授权 )
- 防止会话固定、点击劫持、跨站点请求伪造等攻击(安全)
- Servlet API 集成
- 与 Spring Web MVC 的可选集成
三、spring security 和 shrio的区别
相同点
1:认证功能
2:授权功能
3:加密功能
4:会话管理
5:缓存支持
6:rememberMe功能
不同点
优点:
- spring Security基于spring 开发 如果是spring项目 配合spring security做权限根据更加方便 ,而shrio 需要和 spring进行整合开发
- spring Security 功能比Shiro更加丰富些 ,例如安全防护
- Spring Security 社区资源比shiro丰富
缺点 - shrio配置和使用比较简单,spring Security 比较复杂
- shrio依赖性低 不需要任何框架和容器,可以独立运行 ,而 spring Security 依赖于spring容器
四、实例
入门示例
- 创建springboot项目 选择依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
- 编写一个类
package com.aaa.sbs.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author zhangyifan * @version 8.0 * @description: * @date 2022/2/14 9:05 */ @RestController @RequestMapping("hello") public class HelloController { /** * 入门测试 * @return */ @GetMapping("helloSpringSecurity") public String hello(){ return "hello Security"; } }
一个方法就行
- 直接访问这个方法
http://localhost:18888/hello/helloSpringSecurity
加入security包后就需要认证后才可以访问,会跳转到自带登录页面
使用user 和 控制台打印的随机密码登录
4. 登录原码解析(使用debug)
当我们没有认证时,会请求一个默认过滤器DefaultLoginPageGeneratingFilter,
查看doFilter方法可以看到,如果登录成功直接放行,如果没有登录会调用generateLoginPageHtml(request, loginError, logoutSuccess)方法,判断this.formLoginEnabled就会看到登录界面(在servlet中学习过)
当点击登录时,会再经过一个过滤器UsernamePasswordAuthenticationFilter,执行它中的(直接双击shift搜索)
进行认证,当认证成功会调用该过滤器的父类AbstractAuthenticationProcessingFilter中的successfulAuthentication方法,失败时会调用unsuccessfulAuthentication方法。
认证时系统默认的密码会通过UserDetailsServiceAutoConfiguration中的getOrDeducePassword获取或者推断密码方法打印到控制台,从中可以看出在SecurityProperties类中有一个内部类User,我们登录的用户名和密码都是通过该实体产生。
自定义用户名和密码
- 在application.properties配置文件中配置:
spring.security.user.name=admin spring.security.user.password=tiger
- 使用配置类
package com.aaa.sbs.config; import lombok.extern.log4j.Log4j2; 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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author zhangyifan * @version 8.0 * @description: * @date 2022/2/14 10:33 */ @Configuration @Log4j2 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //实例化 BCryptPasswordEnder BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); //使用加密类生成加密密码 String password = bCryptPasswordEncoder.encode("tiger"); log.info("加密后的密码"+password); boolean matchesPassword = bCryptPasswordEncoder.matches("tiger",password); log.info("密码是否正确"+matchesPassword); //等于吧用户名密码直接配置 。使用时加载到内存中 //withUser配置 用户名 scott 密码 tiger auth.inMemoryAuthentication().withUser("scott").password(password).roles("guanliyuan"); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
- 使用配置类加UserDetailsService(常用):
- 配置类
package com.aaa.sbs.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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.Resource; /** * @author zhangyifan * @version 8.0 * @description: * @date 2022/2/14 10:32 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 配置获取用户信息接口 配置加密方式 auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
- 服务类
package com.aaa.sbs.service; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; 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.List; /** * @author zhangyifan * @version 8.0 * @description: 相当与 shrio的realm * @date 2022/2/14 10:34 */ @Service public class WebDetailServiceImpl implements UserDetailsService { //注入userService接口 /* * private UserService userService */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("guanliyuan,dailishang,shanghu,dept:query,dept:update"); //第一个参数:数据库取出用户名 第二个参数:加密密码 第三个参数:角色/权限集合 return new User("root",new BCryptPasswordEncoder().encode("tiger"),grantedAuthorities); } }
关键类
-
UsernamePasswordAuthenticationFilter
对登录信息进行拦截,检查校验表单中的用户名和密码的一个过滤器。
attemptAuthentication 认证方法
successfulAuthentication 认证成功方法
unsuccessfulAuthentication 认证失败方法 -
UserDetailsService
查询数据库数据的一个接口,返回一个UserDetails,UserDetails的子类User就是我们要使用的用户信息类(包含用户名,密码,权限信息等)
方法
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; -
PasswordEncoder
是Spring Security提供的密码加密方式的接口定义。
String encode(CharSequence rawPassword);
用来对明文密码进行加密,返回加密之后的密文。
//一个密码校对方法,在用户登录的时候,将用户传来的明文密码和数据库中保存的密文密码作为参数,传入到这个方法中去,根据返回的Boolean 值判断用户密码是否输入正确。
boolean matches(CharSequence rawPassword, String encodedPassword);
//如果解析的密码能够再次进行解析且达到更 安全的结果则返回 true,否则返回 false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) { return false; } -
Spring Security 提供了多种密码加密方案,
官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。
不同于 Shiro 中需要自己处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。
package com.aaa.sbs.test; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author zhangyifan * @version 8.0 * @description: 测试BCryptPasswordEncoder * @date 2022/2/14 15:37 */ public class PasswordEncoderTest { public static void main(String[] args) { PasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); //加密密码 String password = bCryptPasswordEncoder.encode("tiger"); System.out.println("密码"+password); //验证密码是否正确 boolean isSuc = bCryptPasswordEncoder.matches("tiger",password); System.out.println("密码是否正确"+isSuc); boolean isSucl = bCryptPasswordEncoder.matches("tiger","$2a$10$kkB4WomyMeYRhg.iC6g0OubsrZGBvAfX7Cuq0mtDkIbMUpLTahDpy"); System.out.println("比对过去生成的密码是否正确"+isSucl); } }
自定义登录页面,用户登出和未认证授权的错误页面配置(403)
- 配置 WebSecuityConfig
package com.aaa.sbs.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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.Resource; /** * @author zhangyifan * @version 8.0 * @description: * @date 2022/2/14 10:32 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 配置获取用户信息接口 配置加密方式 auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 自定义用户登录,注销 角色 /权限 授权配置 都在该方法配置 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //配置登录页面信息 http.formLogin()//总体form方式 .loginPage("/html/login.html") //配置登录页面路劲 .loginProcessingUrl("/user/login") //登录页面中form 配置的请求地址 .failureUrl("/html/login.html?error")//登录失败路劲配置 .defaultSuccessUrl("/html/indexl.html").permitAll()//默认登录成功的 .and().authorizeRequests()//逻辑所有的授权请求 .antMatchers("/user/login","/","/css/**","/js/**").permitAll() .anyRequest().authenticated()//除了上面配置的,其他请求都需要认证 .and().csrf().disable();//关闭csrf概念 如果不关闭 //配置未授权跳转页面 http.exceptionHandling().accessDeniedPage("/html/unauthorized.html"); //注销配置 如果使用默认 /logout 可以不用配置 http.logout().logoutUrl("/logout")//用户请求注销 请求地址 .logoutSuccessUrl("/html/login.html").permitAll();//注销成功跳转地址 } }
- 登录页面和未授权页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>员工登录</title> <script> function load(){ var url = location.href; //判断url中是否含有? if(url.indexOf("?")!=-1){ // 原始js jquery //原始JS赋值 <a>url</ a> .innerText .text() // input .value= .val() // div .innerHTML .html document.getElementById("errorInfo").innerHTML="用户名或者密码错误!!"; } } </script> </head> <body onload="load()"> <center> <h3>登录页面</h3> <form action="/user/login" method="post"> <div id="errorInfo" style="color: red"></div> <table border="1" > <!--用户名和密码的name一定是 username password 大小写也有区分--> <tr><td>用户名</td><td><input type="text" name="username"> </td></tr> <tr><td>密码</td><td><input type="text" name="password"> </td></tr> <tr><td colspan="2" align="center"><input type="submit" value="登录"></td></tr> </table> </form> </center> </body> </html>
unauthorized.html(注意页面名称中不能含有-否则404)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>错误页面</title> </head> <body> 访问了未授权的地址。。。。。。。。。。。 </body> </html>
- 测试
n:root
p:tiger
直接访问刚才的hello方法
直接访问,看是否访问了已经访问的页面
先请求登录
在另外窗口退出
再访问原来页面,发现需要登录,说明已经退出
授权操作
1. hasAuthority(有权限)和 hasAnyAuthority (有任何权限)用法
编写模拟控制器
@RestController @RequestMapping("dept") public class DeptController { /** * 模拟部门查询 * @return */ @GetMapping("queryAll") public String queryAll(){ return "模拟部门查询"; } }
授权配置
服务配置
@Service public class WebDetailServiceImpl implements UserDetailsService { //注入userService接口 /* * private UserService userService */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_guanliyuan,ROLE_dailishang,shanghu,dept:query,dept:update"); //第一个参数:数据库取出用户名 第二个参数:加密密码 第三个参数:角色/权限集合 return new User("root",new BCryptPasswordEncoder().encode("tiger"),grantedAuthorities); } }
如果配置拥有某一个或者在多个权限中任意一个,就可以访问,否则不可以
hasRole(有角色)和hasAnyRole(有任何角色)方法
- 模拟控制器
/** * 根据编号查部门 * @param deptNo * @return */ @GetMapping("queryById") public String queryById(Integer deptNo){ return "根据编号查询部门"; }
- 权限配置
// .antMatchers("/dept/queryById").hasRole("guanliyuan")//拥有管理角色访问该路径 .antMatchers("/dept/queryById").hasAnyRole("guanliyuan","dailishang")//设置拥有多个中某一个角色,才可以访问
- 角色
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_guanliyuan,ROLE_dailishang,shanghu,dept:query,dept:update"); //第一个参数:数据库取出用户名 第二个参数:加密密码 第三个参数:角色/权限集合 return new User("root",new BCryptPasswordEncoder().encode("tiger"),grantedAuthorities); }
测试
http://localhost:18888/dept/queryById?deptNo=1
hasIpAddress
- 编写模拟控制器
@GetMapping("add") public String add(){ return "模拟部门添加"; }
- 配置
.antMatchers("/dept/add").hasIpAddress("192.168.220.1")//固定的ip才允许
http://localhost:18888/dept/add
http://192.168.220.1:18888/dept/add(正确)
授权注解
@Secured
验证当前用户是否具备某一角色,拥有可以访问,否则不可以。
注意
当@EnableGlobalMethodSecurity(securedEnabled=true)的时候,@Secured可以使用
/** * 模拟用户查询 * @return */ //这两个有任意一个就可以访问 @Secured({"ROLE_guanliyuan","ROLE_dailishang"}) @GetMapping("queryAll") public String queryAll(){ return "模拟用户查询"; }
说明:拥有商户或者代理商角色的用户都可以方法queryAll方法。另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。
如果我们要求,只有同时拥有商户和代理商的用户才能方法queryAll()方法,这时候@Secured就无能为力了。
修改UserDetailServiceImpl 中的AuthorityUtils.commaSeparatedStringToAuthorityList(“dept:update,ROLE_dailishang,ROLE_shanghu”);进行测试
@ PreAuthorize
该注解在方法执行之后进行校验。
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限,Spring EL 提供 返回对象能够在表达式语言中获取返回的对象returnObject。当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PostAuthorize可以使用:
//拥有一个权限就可以 //@PreAuthorize("hasRole('ROLE_guanliyuan')") //两个必须都有 //@PreAuthorize("hasRole('ROLE_guanliyuan') and hasRole('ROLE_dailishang')") //任意拥有一个就可以 //@PreAuthorize("hasAnyRole('ROLE_guanliyuan','ROLE_dailishang')") //该方法可以通过returnObject获取到返回值做判断 @PostAuthorize("returnObject!=null") @GetMapping("add") public String add(){ return "模拟用户添加"; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报