SpringSecurity基础入门
一个身份认证、授权、防御常见攻击的框架。
spring security 中文网:Spring Security中文网
自定义配置
基于内存的用户认证
实现步骤如下:
- 在配置类中创建security的配置类:
@Configuration //声明当前类为配置类
@EnableWebSecurity //开启spring security的自定义配置
public class WebSecurityConfig {@Beanpublic UserDetailsService userDetailsService() {// 1、创建基于内存的用户信息管理器InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();// 3、将UserDetails对象交给基于内存的用户信息管理器管理manager.createUser(// 2、创建UserDetails对象,用于管理用户名、用户密码、用户角色、用户权限等内容User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());// 当返回这个基于内存的用户信息管理器的时候,系统中默认的用户就被替换为了上面第二部中定义的UserDetails对象return manager;}
}
- 重启项目,在security默认提供的登录界面中,使用
UserDetails
中定义的用户信息登录。
实现原理分析:
- 应用程序启动时创建了
InMemoryUserDetailsManager
对象,该对象中管理了自定义的UserDetails
类的用户信息。 - 当访问web页面,校验用户信息的时候,security自动使用
InMemoryUserDetailsManager
的loadUserByUsername()
方法从内存中获取到UserDetails
对象,通过拿到的UserDetails
对象中定义的用户信息对web页面输入的信息进行校验。
基于数据库的数据源
在实际开发的过程中绝大多数情况都是需要基于数据库的数据源来做用户认证。
数据源实现案例如下:
- 在MySQL数据库中执行如下建表语句:
-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`; -- 创建用户表
CREATE TABLE `user`( `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) DEFAULT NULL, `password` VARCHAR(500) DEFAULT NULL, `enabled` BOOLEAN NOT NULL
); -- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`); -- 插入用户数据,密码已经被加密,密码为abc(注意:这里密码是示例,实际加密密码应不同)
INSERT INTO `user` (`username`,`password`,`enabled`) VALUES
('admin','{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0Fxo/BTk76lW',TRUE),
('Helen','{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0Fxo/BTk76lW',TRUE),
('Tom','{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0Fxo/BTk76lW',TRUE);
- 引入依赖:
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version>
</dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.4.1</version>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
- 在
application.properties
配置文件中配置数据源信息:
#MySQL数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security-demo
spring.datasource.username=root
spring.datasource.password=123456
#sql日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- 创建实体类:
@Data
public class User {@TableId(value = "id",type = IdType.AUTO)private Integer id;private String username;private String password;private Boolean enabled;
}
- 创建
UserMapper
接口,继承BaseMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {}
- 创建对应的
UserMapper.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="包名要对应到UserMapper.class"></mapper>
- 创建
UserService
接口,继承Iservice
接口
public interface UserService extends Iservice<User> {}
- 创建创建
UserService
的实现类UserServiceImpl
类,并继承ServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {}
- 创建
UserController
类,并注入UserService
@RestController
@RequestMapping("/user")
public class UserController {@AutoWiredprivate UserService userService;// 返回user的列表@GetMapping("/list")public List<User> getList(){return userService.list();}
}
到此数据源整合完毕。
注意:mybatis-plus可能与springboot存在版本冲突导致报错。
基于数据库的用户认证
上面已经创建了基于数据库的数据源的案例,我们继续上面的步骤,使用数据库来达到用户认证的目的。由于没有默认数据库实现,所以需要自己创建DBUserDetailsManager
对象,实现UserDetailsManager
、UserDetailsPasswordService
。
具体步骤如下:
- 创建
DBUserDetailsManager
对象,实现UserDetailsManager
、UserDetailsPasswordService
接口。并且实现这两个类中相关的抽象方法,空实现即可。
public class DBUserDetailsManager implements UserDetailsManager,UserDetailsPasswordService {}
- 在该类中注入
UserMapper
对象
@AutoWired
private UserMapper userMapper;
- 在该类的
loadUserByUsername
方法中根据username
获取UserDetails
对象
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 查询QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username",username);User user = userMapper.selectOne(queryWrapper);if(user == null){// 如果没有查到,则抛出异常throw new UsernameNotFoundException(username);}else{// 创建权限列表Collection<GrantedAuthority> authorities = new ArrayList<>();// 查到了,组装UserDetails对象并返回return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),user.getEnabled(), //是否启用true, //用户账号是否过期true, //用户凭证是否过期true, //用户是否未被锁定authorities //权限列表);}
}
- 创建security的配置类
@Configuration //声明当前类为配置类
@EnableWebSecurity //开启spring security的自定义配置
public class WebSecurityConfig {@Beanpublic UserDetailsService userDetailsService() {// 1、创建基于数据库的用户信息管理器DBUserDetailsManager manager = new DBUserDetailsManager();return manager;}
}
- 重启项目,在security默认提供的登录界面中,使用数据库中定义的用户名,密码登录,测试功能。
默认配置
WebSecurityConfig
配置类中只包含了关于如何验证用户的信息。实际上,有一个配置类(称为 SecurityFilterChain
)在幕后被调用。它被配置为以下的默认实现。该默认配置是为了控制security中的一些默认的过滤器链以及他们的详细信息。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 开启授权保护.authorizeRequests(authorize -> authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated())// 自动使用表单授权方式(生成默认的登录等出页面).formLogin(withDefaults())// 使用基本授权方式(如果没有表单使用浏览器默认提供的).httpBasic(withDefaults());return http.build();
}
添加用户功能
在上面的DBUserDetailsManager
类中已经实现了createUser
方法,该方法就是添加用户的方法,我们可以通过该方法来实现添加用户的功能。
步骤如下:
- 在
UserController
类中创建新增用户的相关代码:
@PostMapping("/add")
public void add(@RequestBody User user){userService.saveUserDetails(user);
}
- 对
UserService
中的saveUserDetails
方法做实现:在UserService
中添加抽象方法
public void saveUserDetails(User user);
- 在
UserServiceImpl
中添加方法实现,将user对象包装为userDetails
对象交给DBUserDetailsManager
的createUser
方法处理
// 先将DBUserDetailsManager注入到UserServiceImpl中
@AutoWired
private DBUserDetailsManager dbUserDetailsManager;// 实现saveUserDetails方法
public void saveUserDetails(User user){UserDetails userDetails = org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder().username(user.getUsername()).password(user.getPassword()).build();dbUserDetailsManager.createUser(userDetails);
}
- 在
DBUserDetailsManager
中为createUser
方法添加实现。
public void createUser(UserDetails userDetails){User user = new User();user.setUsername(userDetails.getUsername);user.setPassword(userDetails.getPassword);user.setEnabled(true);// 上面的步骤中已经注入了UserMapper对象,这里直接使用userMapper.insert(user);
}
- 重启应用程序,访问
UserController
中写的路径进行测试。
同样的原理,如果想要实现修改、删除用户也可以在DBUserDetailsManager
中的updateUser
、deleteUser
来做基于数据库的实现。
注意:
在测试的时候,可能会出现访问不进去的情况,这是因为Spring Security会防御常见攻击的原因,在他的web页面中会有一个name="_csrf"的隐藏表单,默认值为一串字符,在发起请求的时候会一同发回后端,从而实现对scrf攻击的防御手段。测试失败的原因就是因为测试的时候没有携带这一个字符串。解决方法是先将csrf攻击防御关掉:
在上面的默认配置部分讲解了SecurityFilterChain
,这里我们将SecurityFilterChain
添加到我们的WebSecurityConfig
配置类中,并在该方法中添加http.csrf(csrf->csrf.disable());
将csrf攻击防御关闭
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 开启授权保护.authorizeRequests(authorize -> authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated())// 自动使用表单授权方式(生成默认的登录等出页面).formLogin(withDefaults())// 使用基本授权方式(如果没有表单使用浏览器默认提供的).httpBasic(withDefaults());// 将csrf攻击防御关闭,有利于测试。http.csrf(csrf->csrf.disable());return http.build();
}
密码加密算法
以往密码都是以明文方式存储的,但是安全堪忧。为了解决安全问题发展出了密码加密。
Hash算法
Spring Security PasswordEncoder接口用于对密码进行单向转换,从而将密码安全地存储。对密码单向转换需要用到哈希算法,例如MD5、SHA-256、SHA-512等,哈希算法是单向的,只能加密,不能解密。
因此,敬据库中存储的是单向转换后的密码,Spring Security在进行用户身份验证时需要将用户输入的密码进行单向转换,然后与数据库的密码进行比较。
因此,如果发生数据泄露,只有密码的单向哈希会被暴露。由于哈希是单向的,并目在给定哈希的情况下只能通过暴力破解的方式猜测密码。
彩虹表
针对哈希加密算法,一些用户创建了一个名为‘彩虹表’的查找表。
彩虹表就是一个庞大的、针对各种可能的字母组合预先生成的哈希值集合,有了它可以快速破解各类密码。越是复杂的密码,需要的彩虹表就越大,主流的彩虹表都是1B0G以上,目前主要的算法有LM,NTLM,MD5,SHA1,MYSQLSHA1,HALFLMCHALL,NTLMCHALL,ORACLE-SYSTEM,MD5-HALF.
加盐加密
为了减轻彩虹表的效果,开发人员开始使用加盐密码。不再只使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为盐)。盐和用户的密码将一起经过哈希函数运算,生成一个唯一的哈希。盐将以明文形式与用户的密码一起存储。然后,当用户尝试进行身份验证时,盐和用户输入的密码一起经过哈希函数运算,再与存储的密码进行比较。唯一的盐意味着彩虹表不再有效,因为对于每个盐和密码的组合,哈希都是不同的。
自适应单向函数
随着硬件的不断发展,加盐哈希也不再球全。原因是,计算机可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地破解每个密码。
现在,开发人员开始使用自适应单向函数来存储密码。使用自适应单向函数验证密码时,故意占用资源(故意使用大量的CPU、内存或其他资源)。自适应单向函数允许配置一个“工作因子”,随着硬件的改进而增加。我们建议将“工作因子调整到系统中验证密码需要约一秒钟的时间。这种权衡是为了让攻击者难以破解密码。
自适应单向函数包括bcrypt
、PBKDF2
、scrypt和argon2
。
这些自适应单项函数都是PasswordEncoder
的实现类,也就是说想要使用这些函数可以使用PasswordEncoder
来创建它实现类的方式来使用这些函数。
密码加密算法体验
下面新建一个测试类对security的密码加密算法做一个体验:
创建一个SecurityDemoApplicationTests
的测试类,编写如下测试方法,并添加相应注解。
@SpringBootTest
class SecurityDemoApplicationTests {@Testvoid testPassword(){// 新建PasswordEncoder类的实现类BCryptPasswordEncodr实例// 参数部分为工作因子,最小值是4,默认值是10,最大值是31,值越大运算速度越慢PasswordEncoder encoder = new BCryptPasswordEncoder(4);// 明文:"password"// 密文:由于加盐加密每次加密生成的密文不相同// 调用encode方法对"password"进行加密String result = encoder.encode("password");System.out.println(result);// 密码校验Assert.isTrue(encoder.matches("password",result),"密码不一致");}
}
DelegatingPasswordEncoder
表中存储的密码形式:{bcrypt}一长串字符串
而存储密码中的前面的{bcrypt}部分是为了表明当前密码是使用那种PasswordEncoder
的实现类加密的。
这种存储格式的目的是,为了方便随时做密码策略的升级,兼容数据库中老版本密码生成策略生成的密文密码。
在DelegatingPasswordEncoder
类中有一个matches
方法,该方法就是比对明文密码和密文密码是否匹配的方法。而该方法中,会先将密文密码中的前缀取出用来根据这个前缀来生成明文密码的加密格式,将两个密文密码做比对,从而判断前端输入的密码是否正确。
自定义登录页面
下面是如何在前后端一体式项目中自定义登录界面的步骤:
- 创建一个
LoginController
类,在该类中编写login
方法,返回一个login视图
@Controller
public class LoginController {@GetMapping("/login")public String login(){return "login";}
}
- 在资源目录下创建一个
login.html
<html><body><h1>登录</h1><!-- th:if="${param.error}"作用:使用动态参数,表单会自动生成csrf字段,防止csrf攻击根据发布路径生成相对路径--><div th:if="${param.error}">错误的用户名和密码</div><form th:action="@{/login}" method="post"><div><input type="text" name="username" placeholder="用户名"></div><div><input type="password" name="password" placeholder="密码"></div><input type="submit" value="登录"></form></body>
</html>
- 在
WebSecurityConfig
配置文件中将formLogin中修改为自定义的配置:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 开启授权保护.authorizeRequests(authorize -> authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated())// 自动使用表单授权方式(生成默认的登录等出页面).formLogin(form->{// 重定向到"/login"页面form.loginPage("/login")// 设置无需授权即可访问.permitAll()// 设置账号和密码对应输入框的name.usernameParameter("username").passwordParameter("password")// 登录失败后的url地址,默认为"/longin?error".failureUrl("/longin?error");})// 使用基本授权方式(如果没有表单使用浏览器默认提供的).httpBasic(withDefaults());return http.build();
}
- 重启项目,测试。
前后端分离
登录反馈
前后端分离开发中,在登录成功后只需要给前端返回json数据即可,而AuthenticationSuccessHandler
就是定义登录成功后如何给前端返回json信息的,而AuthenticationFailureHandler
是定义登录失败后信息的。
步骤如下:
- 由于最后要返回给前端一个json格式的数据,所以先导入fastjson依赖
<dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.37</version>
</dependency>
- 在配置类中实现
AuthenticationSuccessHandler
接口,并实现其中的方法
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {public void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication) throws IOException{Object principal = authentication.getPrincipal(); // 获取用户身份信息// Object credentials = authentication.getCredentials(); // 获取用户凭证信息// Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); //获取用户权限信息HashMap result = new HashMap();result.put("code",0);result.put("message","登录成功");result.put("data",principal);// 将对象转换为json字符串String json = JSON.toJSONString(result);// 返回json数据到前端response.setContentType("application/json;charset=UTF-8");response.getWriter(json);}
}
- 将
MyAuthenticationSuccessHandler
类在WebSecurityConfig
配置类中进行配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {// 开启授权保护http.authorizeRequests(authorize -> {authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated();});// 使用表单授权方式http.formLogin(form -> {// 重定向到"/login"页面form.loginPage("/login")// 设置无需授权即可访问.permitAll()// 设置账号和密码对应输入框的name.usernameParameter("username").passwordParameter("password")// 登录失败后的url地址,默认为"/longin?error".failureUrl("/longin?error")// 认证成功处理.successHandler(new MyAuthenticationSuccessHandler());});// 关闭csrf攻击防御http.csrf(csrf -> csrf.disable());return http.build();
}
- 在配置类中实现
AuthenticationFailureHandler
接口,并实现其中的方法
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {public void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response,AuthenticationException exception) throws IOException{// 获取本地信息String localizedMessage = exception.getLocalizedMessage();HashMap result = new HashMap();result.put("code",-1);result.put("message",localizedMessage);// 将对象转换为json字符串String json = JSON.toJSONString(result);// 返回json数据到前端response.setContentType("application/json;charset=UTF-8");response.getWriter(json);}
}
- 将
MyAuthenticationFailureHandler
类在WebSecurityConfig
配置类中进行配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {// 开启授权保护http.authorizeRequests(authorize -> {authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated();});// 使用表单授权方式http.formLogin(form -> {// 重定向到"/login"页面form.loginPage("/login")// 设置无需授权即可访问.permitAll()// 设置账号和密码对应输入框的name.usernameParameter("username").passwordParameter("password")// 登录失败后的url地址,默认为"/longin?error".failureUrl("/longin?error")// 认证成功处理.successHandler(new MyAuthenticationSuccessHandler())// 认证失败处理.failureHandler(new MyAuthenticationFailureHandler());});// 关闭csrf攻击防御http.csrf(csrf -> csrf.disable());return http.build();
}
- 重新启动项目,测试。发现登录失败后前端会返回自定义的json格式数据
注销反馈
目前注销的时候还没有返回给前端结果,注销时候的处理类为LogoutSuccessHandler
。
实现注销反馈信息的步骤如下:
- 创建配置类实现
LogoutSuccessHandler
类,实现该类的方法。
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {public void onLogoutSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication){HashMap result = new HashMap();result.put("code",0);result.put("message","注销成功");// 将对象转换为json字符串String json = JSON.toJSONString(result);// 返回json数据到前端response.setContentType("application/json;charset=UTF-8");response.getWriter(json);}
}
- 将
MyLogoutSuccessHandler
类在WebSecurityConfig
配置类中进行配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {// 开启授权保护http.authorizeRequests(authorize -> {authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated();});// 使用表单授权方式http.formLogin(form -> {// 重定向到"/login"页面form.loginPage("/login")// 设置无需授权即可访问.permitAll()// 设置账号和密码对应输入框的name.usernameParameter("username").passwordParameter("password")// 登录失败后的url地址,默认为"/longin?error".failureUrl("/longin?error")// 认证成功处理.successHandler(new MyAuthenticationSuccessHandler())// 认证失败处理.failureHandler(new MyAuthenticationFailureHandler());});// 登出http.logout(logout -> {logout.logoutSuccessHandler(new MyLogoutSuccessHandler);});// 关闭csrf攻击防御http.csrf(csrf -> csrf.disable());return http.build();
}
- 重新启动项目,在注销以后就会看到后端发回的注销成功界面
请求未认证的接口
请求未认证接口:当访问一个需要认证之后才能访问的接口的时候,Spring Security会使用AuthenticationEntryPoint
将用户请求跳转到登录页面,要求用户提供登录凭证。
这里我们也希望系统返回json结果,因此我们定义类实现AuthenticationEntryPoint
接口。
实现步骤如下:
- 创建配置类实现
AuthenticationEntryPoint
接口,并实现其中的方法。
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {public void commence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException) throws IOException{String localizedMessage = "需要登录才能访问该资源";HashMap result = new HashMap();result.put("code",-1);result.put("message",localizedMessage);// 将对象转换为json字符串String json = JSON.toJSONString(result);// 返回json数据到前端response.setContentType("application/json;charset=UTF-8");response.getWriter(json);}
}
- 将
MyAuthenticationEntryPoint
类在WebSecurityConfig
配置类中进行配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {// 开启授权保护http.authorizeRequests(authorize -> {authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated();});// 使用表单授权方式http.formLogin(form -> {// 重定向到"/login"页面form.loginPage("/login")// 设置无需授权即可访问.permitAll()// 设置账号和密码对应输入框的name.usernameParameter("username").passwordParameter("password")// 登录失败后的url地址,默认为"/longin?error".failureUrl("/longin?error")// 认证成功处理.successHandler(new MyAuthenticationSuccessHandler())// 认证失败处理.failureHandler(new MyAuthenticationFailureHandler());});// 登出http.logout(logout -> {logout.logoutSuccessHandler(new MyLogoutSuccessHandler);}); // 异常情况处理http.exceptionHandling(exception -> {// 请求未认证exception.authenticationEntryPoint(new MyAuthenticationEntryPoint);});// 关闭csrf攻击防御http.csrf(csrf -> csrf.disable());return http.build();
}
- 重新启动,访问web页面测试功能。
跨域问题
跨域全称是跨域资源共享(Cross-Origin Resources Sharing,.CoRS),它是浏览器的保护机制,只允许网页请求统一域名下的服务,同一域名指=>协议、域名、端口号都要保持一致,如果有一项不同,那么就是跨域请求。在前后端分离的项目中,需要解决跨域的问题。
在security中解决跨域问题很简单,只需要在配置类中添加http.cors(withDefaults());
即可,具体如下:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {// 开启授权保护http.authorizeRequests(authorize -> {authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated();});// 使用表单授权方式http.formLogin(form -> {// 重定向到"/login"页面form.loginPage("/login")// 设置无需授权即可访问.permitAll()// 设置账号和密码对应输入框的name.usernameParameter("username").passwordParameter("password")// 登录失败后的url地址,默认为"/longin?error".failureUrl("/longin?error")// 认证成功处理.successHandler(new MyAuthenticationSuccessHandler())// 认证失败处理.failureHandler(new MyAuthenticationFailureHandler());});// 登出http.logout(logout -> {logout.logoutSuccessHandler(new MyLogoutSuccessHandler);}); // 异常情况处理http.exceptionHandling(exception -> {// 请求未认证exception.authenticationEntryPoint(new MyAuthenticationEntryPoint);});// 解决跨域请求http.cors(withDefaults());return http.build();
}
身份认证
获取用户相关信息
在Spring Security框架中,SecurityContextHolder
、SecurityContext
、Authentication
、Principal
和Credential
是一些与身份验证和授权相关的重要概念。它们之间的关系如下:
SecurityContextHolder
:SecurityContextHolder
是Spring Security存储已认证用户详细信息的地方。SecurityContext:SecurityContext
是从SecurityContextHolder
获取的内容,包含当前已认证用户的Authentication
信息。Authentication
:Authentication
表示用户的身份认证信息。它包含了用户的Principal
、Credentials
和Authorities
信息。
实际使用实例如下:
@RestController
public class IndexController {@GetMapping("/getUserData")public Map getUserData(){// 首先获取到SecurityContext,SecurityContext中包含了已认证的用户信息SecurityContext context = SecurityContextHolder.getContext();// 拿到用户信息AuthenticationAuthentication authentication context.getAuthentication();// 通过Authentication拿到用户的Principal、Credential和Authority信息Object principal = authentication.getPrincipal();Object credentials = authentication.getCredentials();// 获取到权限信息Cpllection<? extends GrantedAuthority> authorities = authentication.getAuthorities();// 通过authentication拿到用户名String username = authentication.getUsername();HashMap hashMap = new HashMap();hashMap.put("username",username);hashMap.put("authorities",authorities);return hashMap;}
}
重启项目后,访问前端的该路径就会看到前端呈现的json格式的用户名信息和权限信息。
会话并发处理
针对同一个账号后登录的账号会使先登录的账号失效,代码示例如下:
- 实现接口
SessionInformationExpiredStrategy
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException,ServletException {HashMap result = new HashMap();result.put("code",-1);result.put("message","该账号已在其他设备登录");String json = JSON.toJSONString(result);HttpServletResponse response = event.getResponse();response.setContentType("application/json;chartset-UTF-8");response.getWrite().println(json);}
}
- 将
MySessionInformationExpiredStrategy
类在WebSecurityConfig
配置类中进行配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {// 开启授权保护http.authorizeRequests(authorize -> {authorize// 对所有请求开启授权保护.anyRequest()// 已经认证的请求会被自动授权.authenticated();});// 使用表单授权方式http.formLogin(form -> {// 重定向到"/login"页面form.loginPage("/login")// 设置无需授权即可访问.permitAll()// 设置账号和密码对应输入框的name.usernameParameter("username").passwordParameter("password")// 登录失败后的url地址,默认为"/longin?error".failureUrl("/longin?error")// 认证成功处理.successHandler(new MyAuthenticationSuccessHandler())// 认证失败处理.failureHandler(new MyAuthenticationFailureHandler());});// 登出http.logout(logout -> {logout.logoutSuccessHandler(new MyLogoutSuccessHandler);}); // 异常情况处理http.exceptionHandling(exception -> {// 请求未认证exception.authenticationEntryPoint(new MyAuthenticationEntryPoint);});// 解决跨域请求http.cors(withDefaults());// 会话http.sessionManagement(session -> {// 会话并发处理,参数说明允许同一个账号同时在一台设备登录session.maximumSessions(1).expiredSessionStrategy(new MySessionInformationExpiredStrategy());});// 关闭csrf攻击防御http.csrf(csrf -> csrf.disable());return http.build();
}
- 重启项目后,在两个不同的浏览器登录的时候发现只能在其中一个登录
授权
授权管理的实现在SpringSecurity中非常灵活,可以帮助应用程序实现以下两种常见的授权需求:
- 用户权限资源:例如某个用户的权限是添加用户、查看用户列表,而另一个用户的权限是查看用户列表
- 用户角色权限资源:例如某个用户的角色是管理员、另一个用户的角色是普通用户,管理员能做所有操作,普通用户只能查看信息
基于Request的授权案例
用户-权限-资源
需求:
- 具有USER_LIST权限的用户可以访问/user/list接口
- 具有USER_ADD权限的用户可以访问/user/add接口
实现步骤:
- 在SecurityFilterChain方法中添加授权保护的配置
// 开启授权保护
http.authorizeRequests(authorize -> {// 具有USER_LIST权限的用户可以访问/user/listauthorize.requestMatchers("/user/list").hasAuthority("USER_LIST")// 具有USER_ADD的用户可以访问/user/add.requestMatchers("/user/add").hasAuthority("USER_ADD")// 对所有请求开启授权保护.anyRequest()// 已认证的请求会被自动授权.authenticated();
});
- 由于数据库没有添加相关的权限字段,所以我们在
DBUserDetailsManage
类中的loadUserByUsername
方法中修改代码,采用硬编码的方式提供权限。
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 查询QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username",username);User user = userMapper.selectOne(queryWrapper);if(user == null){// 如果没有查到,则抛出异常throw new UsernameNotFoundException(username);}else{// 创建权限列表Collection<GrantedAuthority> authorities = new ArrayList<>();// 向权限列表中添加权限authorities.add(new GrantedAuthority(){public String getAuthority(){return "USER_LIST"}});authorities.add(new GrantedAuthority(){public String getAuthority(){return "USER_ADD"}});// 查到了,组装UserDetails对象并返回return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),user.getEnabled(), //是否启用true, //用户账号是否过期true, //用户凭证是否过期true, //用户是否未被锁定authorities //权限列表);}
}
- 重启项目,在登陆后访问
/user/list
和/user/add
,发现正常访问。之后回到loadUserByUsername
方法中,把向权限列表中添加某个权限的代码注释以后,再重启代码并再次访问这两个路径,就会发现被注释掉的权限生效。 - 当访问未授权页面的时候返回403的页面,对用户很不友好。可以在
SecurityFilterChain
方法中异常情况处理部分添加请求未授权的接口时候的响应,代码如下:
// 异常情况处理
http.exceptionHandling(exception -> {// 请求未认证exception.authenticationEntryPoint(new MyAuthenticationEntryPoint);// 这里直接用匿名内部类的方式,就不再新建一个类了exception.accessDeniedHandler((request,response,e) -> {//创建结果对象HashMap result = new HashMap();result.put("code",-1);result.put("message","没有权限访问");// 将结果对象转换未json字符串String json = JSON.toJSONString(result);// 返回响应response.setContentType("application/json;charset=UTF-8");response.getWriter().println(json);});
});
- 之后重启项目,再次访问该路径,就会发现返回的json格式的数据。
用户-角色-资源
需求:角色为ADMIN
的用户才可以访问/user/**
路径下的资源
实现步骤如下:
- 在
SecurityFilterChain
方法中添加如下配置替换掉原有的授权保护配置
// 开启授权保护
http.authorizeRequests(authorize -> {// 具有ADMIN身份的用户可以访问/user/**authorize.requestMatchers("/user/**").hasRole("ADMIN")// 对所有请求开启授权保护.anyRequest()// 已认证的请求会被自动授权.authenticated();
});
- 由于数据库中也没有用户身份的相关字段,所以我们在
DBUserDetailsManage
类中的loadUserByUsername
方法中修改代码,采用硬编码的方式提供用户角色。
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 查询QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username",username);User user = userMapper.selectOne(queryWrapper);if(user == null){// 如果没有查到,则抛出异常throw new UsernameNotFoundException(username);}else{// 由于通过org.springframework.security.core.userdetails.User的构造方法没有办法添加角色,所以我们采用如下方法来做return org.springframework.security.core.userdetails.User// 用户名.withUsername(user.getUsername())// 密码.password(user.getPassword())// 用户是否禁用.disabled(!user.getEnabled())// 是否过期.credentialsExpired(false)// 用户是否被锁定.accountLocked(false)// 为该用户配置一个角色.roles("ADMIN").bulid();}
}
- 重新启动应用程序,测试功能。
用户-角色-权限-资源
RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。以下是一个基本的RBAC数据库设计的示例:
- 用户表:包含用户的基本信息,例如用户名、密码和其他身份信息
列名 | 数据类型 | 描述 |
---|---|---|
user_id | int | 用户id |
username | varchar | 用户名 |
password | varchar | 密码 |
varchar | 电子邮件地址 | |
… | … | … |
- 角色表:存储所有可能的角色及其描述
列名 | 数据类型 | 描述 |
---|---|---|
role_id | int | 角色id |
role_name | varchar | 角色名称 |
description | varchar | 角色描述 |
… | … | … |
- 权限表:定义系统中所有可能的权限
列名 | 数据类型 | 描述 |
---|---|---|
permission_id | int | 权限id |
permission_name | varchar | 权限名称 |
description | varchar | 权限描述 |
… | … | … |
- 用户角色关联表:将用户与角色关联起来
列名 | 数据类型 | 描述 |
---|---|---|
user_role_id | int | 用户角色关联id |
user_id | int | 用户id |
role_id | int | 角色id |
… | … | … |
- 角色权限关联表
列名 | 数据类型 | 描述 |
---|---|---|
role_permission_id | int | 角色权限关联id |
role_id | int | 角色id |
permission_id | int | 权限id |
… | … | … |
基于方法的授权
也就是在开启基于方法的授权以后,在controller类中通过在方法上添加注解的方式实现基于方法的授权。
步骤如下:
- 在WebSecurityConfig配置类种添加
@EnableMethodSecurity
注解开启方法授权,并删除filterChain
中的授权的配置
@Configuration //声明当前类为配置类
@EnableWebSecurity //开启spring security的自定义配置
@EnableMethodSecurity //开启基于方法的授权
public class WebSecurityConfig {}
- 给用户授予角色和权限,在
DBUserDetailsManager
中的loadUserByUsername
方法
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 查询QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username",username);User user = userMapper.selectOne(queryWrapper);if(user == null){// 如果没有查到,则抛出异常throw new UsernameNotFoundException(username);}else{// 由于通过org.springframework.security.core.userdetails.User的构造方法没有办法添加角色,所以我们采用如下方法来做return org.springframework.security.core.userdetails.User// 用户名.withUsername(user.getUsername())// 密码.password(user.getPassword())// 用户是否禁用.disabled(!user.getEnabled())// 是否过期.credentialsExpired(false)// 用户是否被锁定.accountLocked(false)// 为该用户配置一个角色.roles("ADMIN")// 添加单个权限 .authorities("权限名")// 添加多个权限 .authorities("权限名1","权限名2",...,"权限名n")// 如果想要添加多个,必须得按照该格式写,如果写多个.authorities("权限名"),后面的权限会把前面的权限覆盖。而且它还会覆盖角色.bulid();}
}
- 在controller类中的某个方法上添加
@PreAuthorize
注解
@RestController
@RequestMapping("/user")
public class UserController {@AutoWiredprivate UserService userService;@GetMapping("/list")// 这个注解表示只有ADMIN这个角色才能访问该资源@PreAuthorize("hasRole('ADMIN')")public List<User> getList(){}
}
- 重新启动程序,测试相关功能。
- 如果没有配置注解的话,默认都可以访问。但是如果添加了
@PreAuthorize
注解的话,只有满足注解中条件的用户才能访问。 @PreAuthorize
注解中还可以配置较为复杂的参数,例如:@PreAuthorize("hasRole('ADMIN') and authentication.name=='admin'")
。这个注解代表角色为ADMIN且用户名为admin的用户才能访问该资源。- 上面是角色-方法。如果想要配置权限-方法的话。可以在controller方法上添加
@PreAuthorize("hasAuthority('权限名')")
。这个注解可以配置哪种权限可以访问该资源。
OAuth2
OAuth2中分为四个角色:
- 资源所有者
- 客户应用
- 资源服务器
- 授权服务器
客户应用访问资源服务器需要令牌,这个令牌需要资源拥有者授权后从授权服务器获取,获取到令牌后,客户应用就可以使用令牌从资源服务器拿到想要的资源。
在以往的情况下,授权和校验都是在同一个后端代码中完成的。而OAuth2是把授权和校验分到了两个服务器来完成。
例如某些网站使用第三方账号的授权登录。
四种授权模式
四种授权模式:
- 授权码(authorization-code)
- 隐藏式(implicit)
- 密码式(password)
- 客户端凭证(client credentials)
授权码
授权码(authorization-code),指的是第三方应用先申请一个授权码,再通过该授权码获取令牌。
这种方式是最常用,最复杂,也是最安全的,它适用于那些有后端的Wb应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
过程:
- 发起授权请求
- A服务将用户重定向到B服务的授权端点(如/oauth/authorize)。
- 重定向时,A服务传递一些参数,如client_id(客户端ID)、response_type=code(表示使用授权码模式)、redirect_uri(回调地址)等。
- 用户同意授权
- 用户在B服务的授权页面上看到A服务的请求,并决定是否授权。
- 如果用户同意授权,B服务将生成一个授权码,并将其重定向回A服务指定的redirect_uri,同时带上授权码和其他可能的参数(如state)。
- 客户端请求访问令牌
- A服务在收到B服务的重定向请求后,从URL参数中提取授权码。
- A服务向B服务的令牌端点(如/oauth/token)发送一个请求,以获取访问令牌(token)。
- 请求中包含的参数有:grant_type=authorization_code(表示使用授权码模式)、code(从B服务获取的授权码)、redirect_uri(之前注册的回调地址,用于验证回调地址的正确性)、client_id和client_secret(用于验证客户端的身份)。
- 资源服务器返回访问令牌
- 如果验证成功,B服务将返回一个包含访问令牌(token)的响应。
- 访问令牌是客户端用于访问资源服务器上用户资源的凭证。
- 响应中可能还包含刷新令牌(refresh token),用于在访问令牌过期后获取新的访问令牌。
- 客户端使用访问令牌访问资源
- A服务使用从B服务获取的访问令牌来访问B服务上的用户资源。
- 在HTTP请求的头部(如Authorization头)中包含访问令牌。
- B服务验证访问令牌的有效性,并返回相应的资源给A服务。
隐藏式
OAuth2的隐藏式授权方式(也称为隐式授权模式或简化模式)是一种授权流程,它允许客户端应用程序直接从授权服务器获取访问令牌(Access Token),而无需与后端服务器进行交互。这种方式主要用于没有后端服务器的客户端应用程序,如单页面应用(SPA)或移动应用。
这种方式允许直接向前端颁发令牌。这种方式没有授权码这个步骤。
过程:
- 客户端发起请求:
- 客户端将用户重定向到授权服务器的授权端点。
- 重定向URL中包含客户端的client_id、请求的response_type=token(表示使用隐式授权模式)、redirect_uri(回调地址)等参数。
- 用户授权:
- 用户在授权页面上登录(如果尚未登录),并决定是否授权客户端访问其资源。
- 如果用户同意授权,授权服务器将直接生成访问令牌,并将其附加到重定向URI的片段(fragment)部分,然后重定向用户回客户端的redirect_uri。
- 客户端处理响应:
- 客户端在redirect_uri中接收到包含访问令牌的响应。
- 由于访问令牌在URL的片段部分,因此它不会暴露在浏览器的历史记录或服务器日志中,增加了安全性。
- 客户端从URL片段中提取访问令牌,并使用它访问受保护的资源。
密码式
OAuth2的密码式授权方式(也称为“Resource Owner Password Credentials Grant”)允许客户端应用程序在获得用户的用户名和密码后,直接向授权服务器请求访问令牌(Access Token)。这种方式通常用于那些用户高度信任客户端,并愿意直接提供其凭据的场景。
这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
过程:
- 用户提供凭据
- 用户直接向客户端应用程序提供其用户名和密码。
- 客户端应用程序需要确保用户明白其正在将凭据提供给该应用程序,并且用户需要明确授权应用程序使用该凭据进行身份验证。
- 客户端发送请求
- 客户端应用程序将用户名和密码,以及其他必要的参数(如client_id、client_secret、grant_type=password等),打包成一个HTTP请求,发送到授权服务器的令牌端点(Token Endpoint)。
- client_id和client_secret用于标识客户端应用程序的身份,并证明其有权请求访问令牌。
- grant_type=password表明这是一个密码式授权请求。
- 授权服务器验证凭据
- 授权服务器接收到请求后,会验证提供的用户名和密码的有效性。
- 如果凭据验证通过,授权服务器将生成一个访问令牌,并可能包括一个刷新令牌(Refresh Token),然后将其返回给客户端。
- 客户端使用访问令牌
- 客户端收到访问令牌后,可以在后续与资源服务器的交互中使用该令牌来访问受保护的资源。
- 访问令牌通常包含在HTTP请求的Authorization头部中,以“Bearer”为类型标识符,如“Authorization: Bearer ”。
客户端凭证
OAuth2的客户端凭证授权方式(Client Credentials Grant)是一种适用于机器到机器(M2M)通信的授权流程,特别是在客户端需要代表自身(而不是用户)访问受保护的资源时。适用于没有前端的命令行应用,即在命令行下请求令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
步骤:
- 客户端请求
- 客户端向授权服务器的令牌端点发送请求,请求中包含以下参数:
- grant_type:指定授权类型,对于客户端凭证模式,其值为client_credentials。
- client_id:客户端的ID,用于标识客户端的身份。
- client_secret:客户端的密钥,用于验证客户端的身份。
- scope(可选):定义客户端可以访问的资源的范围。
- 客户端向授权服务器的令牌端点发送请求,请求中包含以下参数:
- 授权服务器验证
- 授权服务器验证客户端ID和客户端密钥的有效性。
- 如果验证通过,授权服务器将生成一个访问令牌(Access Token)。
- 返回访问令牌
- 授权服务器将访问令牌返回给客户端。访问令牌是客户端用于访问受保护资源的凭证。
授权模式的选择
spring中的实现
Spring Security:
- 客户应用(OAuth2 Client):OAuth2客户端功能中包含OAuth2 Login
- 资源服务器(OAuth2 Resource Server)
Spring:
- 授权服务器(Spring Authorization Server):它是在Spring Security之上的一个单独的项目
相关依赖如下:
<!-- 资源服务器 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency><!-- 客户应用 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency><!-- 授权服务器 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
授权登录的实现思路
找到某第三方的OAuth2的第三方授权的配置界面完成相应的配置。
代码实现,之后自行搜索。
相关文章:
SpringSecurity基础入门
一个身份认证、授权、防御常见攻击的框架。 spring security 中文网:Spring Security中文网 自定义配置 基于内存的用户认证 实现步骤如下: 在配置类中创建security的配置类: Configuration //声明当前类为配置类 EnableWebSecurity //…...
MySQL的安装及相关操作
目录 一. 数据库产生的背景 二. 数据库操作系统的组成 2.1 数据库(Database) 2.2 数据库管理系统(DBMS, Database Management System) 2.3 应用程序(Application) 三. 数据库的分类 3.1 关系数据库 3.2 非关系数据库 四. MySQL安装 4.1yum安装 1. Ubuntu 2. cent…...
【Code】Foundations 2017- Catalogue, List of Tables, List of Figures
Foundations 2017 目录 | Catalogue表格目录 | List of Tables图表目录 | List of Figures 目录 | Catalogue 英文原文中文翻译词汇学习(音标和解释)1. General1. 总则1.1 Scope1.1 范围1.2 Glossary1.2 术语表Glossary [ˈɡlɒsəri] 术语表ÿ…...
【TCGA-CRC】TCGA数据读取
写在前面 参考已有的帖子写的,但是临床数据和UCSC的不同。有知道的小伙伴欢迎指正。 rm(list ls()); gc() test1 data.table::fread("./00_Rawdata/GDCdata/TCGA-COAD/Transcriptome_Profiling/Gene_Expression_Quantification/00ae9ab8-6eaa-4085-af72-26…...
BYUCTF 2025
几周没会的比赛了,都是一题游。这周的BYU还不错,难度适中,只是时间有点短。周末时间不够。 Crypto Many Primes from Crypto.Util.number import bytes_to_long, getPrime import randomflag open("flag.txt").read().encode()…...
【Linux】初见,基础指令(续)
前言: 上文讲解了部分指令,本文我们来讲解剩下的指令【Linux】初见,基础指令-CSDN博客 cat指令 语法:cat 选项 文件 功能:打印文件中的内容 选项: -b 对非空行输出进行编号 -n 对输出的说有行进行编号…...
《MambaLLIE:基于隐式Retinex感知的低光照增强框架与全局-局部状态空间建模》学习笔记
Paper:2405.16105 Github:GitHub - wengjiangwei/MambaLLIE 目录 摘要 一、介绍 二、相关工作 2.1 低光图像增强 2.2 视觉空间状态模型 三、方法 3.1 预备知识 3.2 整体流程 3.3 全局优先-局部次之状态空间块 四、实验 4.1 基准数据集与实施细节 4.2 对比实验 4…...
计算机图形学Games101笔记--几何
第二部分:几何 几何介绍 光栅化解决如何渲染,几何研究模型如何存储在GPU的内存中。几何主要分为两种:隐式几何和显式几何。 **隐士几何:**用点之间的关系存储,如球的计算公式。更一般的可以用f(x,y,z)。我们可以令f…...
Web开发-Python应用Flask框架Jinja模版绑定路由参数传递页面解析SSTI注入
知识点: 1、安全开发-Python-Flask&Jinja2 2、安全开发-Python-路由传参&SSTI注入 演示案例-WEB开发-Python-Flask框架&Jinja2模版&路由传参&SSTI注入 0、Pycharm 配置Python解析 新建Flask项目 1、路由传参 app.route(/) app.route(/<id…...
聚焦开放智能,抢占技术高地 | 2025 高通边缘智能创新应用大赛第五场公开课来袭!
随着2025高通边缘智能创新应用大赛的推进,越来越多的参赛者关注如何借助高性能硬件突破技术瓶颈、打造差异化作品。 5月27日晚8点,大赛将开启初赛阶段的第五场专题公开课——由美格软件研究院院长李书杰领衔,深入解析高通平台的底层架构与参…...
NMOS和PMOS的区别
1 区分NMOS和PMOS:衬底箭头指向G级的是NMOS,衬底箭头背向G级的是PMOS 2 区分D和S级:针对NMOS,体二极管的正方向为S级;针对PMOS,体二极管正的方向为D级 3 区分电流方向:针对NMOS,电…...
Paillier加密方案的原理、实现与应用(vs)
一、实验目的 1、掌握NTL的基本配置和方法(以下是以visualstudio为例) 2、掌握Paillier加密方案的原理与实现 ①钥匙生成:首先,生成一把钥匙,包括钥匙和私钥匙。钥匙由两个大素数(p,q)的乘积n和一个整数g组成&#…...
Metal入门,使用Metal绘制3D图形
这次是使用Metal绘制一个立方体,并且添加旋转效果,绘制正方形的步骤很简单,我们绘制一个正方形就相当于绘制两个三角形,那么绘制一个正方体,我们很容易想到需要绘制他六个面,很显然,我们也需要把…...
Java 04 API
API 简介 一些已经写好的应用程序编程接口Object toString 默认返回的是当前对象在堆内存中的地址值信息:类的全类名十六进制哈希值返回该对象的返回值 class A{ } //返回的是地址哦 String sA.toString(); //细节:使用打印语句,打印对象…...
基于Gitee 的开发分支版本管理规范
一、版本管理规范概述 目的:规范代码分支管理和版本发布流程,提高团队协作效率,确保代码质量和版本可追溯性。适用范围:基于 Gitee 平台开发的所有项目。分支策略:采用 Git Flow 模型的变体,主要分支包括 …...
HOW - 结合 AI 进行 Tailwind 样式开发
文章目录 情况 1:使用 Tailwind CSS 与手写传统 CSS 的开发效率对比情况 2:AI Tailwind 自动生成 UI 的效率如何?总结 在 WHAT - Tailwind 样式方案(不写任何自定义样式) 中我们已经简单介绍过 Tailwind。今天主要认识…...
系统数据对接-从获取到处理的全流程
在后端架构的复杂生态中,子系统间或与外部系统的对接是常态,其核心要义在于实现数据的精准传输。本文聚焦于数据传输后的运算逻辑与异常处理机制,旨在为后端开发者提供深度见解。 一、数据获取机制:触发式与定时任务的权衡 &…...
Java 09Stream流与File类
Stream流与File类 Stream流 简化集合和数组的操作,startWith(“张”) 第一个为这个返回true String1.获取Stream对象 单列集合 双列集合 先获得键值对 在遍历数组 零散的数据 Stream<Integer> arrStream.of(1,2,34,3); stream.forEach(sss); 即可2.中间…...
《光与影:33号远征队》栩栩如生的角色动画是如何创建的?
《光与影:33号远征队》是一款由Sandfall Interactive公司开发的回合制RPG游戏,背景是一个黑暗的幻想世界。游戏因其独特的艺术风格和引人注目的叙事赢得了无数赞誉,成为今年大热游戏中的一匹黑马。 在该游戏制作中Sandfall依靠包括Xsens在内的…...
GESP2024年12月认证C++二级( 第三部分编程题(1)寻找数字)
参考程序(枚举): #include <iostream> //#include <cmath> using namespace std;int main() {int t;cin >> t;while (t--) {long long a;cin >> a;bool found false;// 枚举 b for (long long b 1; b * b * b * b…...
《探索具身智能机器人视觉-运动映射模型的创新训练路径》
视觉 - 运动映射模型作为实现智能交互与精准行动的核心,吸引着全球科研人员与技术爱好者的目光。这一模型就像机器人的 “神经中枢”,连接着视觉感知与肢体运动,使机器人能够在复杂的现实环境中灵活应对各种任务。 传统的视觉 - 运动映射模型…...
Python打卡DAY31
今日的示例代码包含2个部分 notebook文件夹内的ipynb文件,介绍下今天的思路项目文件夹中其他部分:拆分后的信贷项目,学习下如何拆分的,未来你看到的很多大项目都是类似的拆分方法 知识点回顾 规范的文件命名规范的文件夹管理机器学…...
【SPIN】PROMELA远程引用与控制流验证(SPIN学习系列--5)
PROMELA语言提供了两种强大的机制用于验证并发系统:远程引用(remote references)和进程变量引用。这些机制使得在不引入额外状态变量的情况下,能够精确描述系统状态和属性。 远程引用(Remote References) 远程引用允许你直接引用进程中的控制位置(labe…...
GMSL:汽车里的音视频传输
参考链接: blog.csdn.net/weixin_50875614/article/details/119995651 blog.csdn.net/syjie19900426/article/details/145269782 SerDes 应用场景 WHAT GMSL是什么 GMSL(Gigabit Multimedia Serial Links),中文名称为千兆多媒体串行链路,是Maxim公司推出的一种…...
Java并发进阶系列:深度讨论jdk1.8 ConcurrentHashMap并发环境下transfer方法桶位分配过程
在前面有多篇关于jdk1.8的ConcurrentHashMap研究是基于源代码给出的深度分析,要知道多线程环境下的ConcurrentHashMap内部运行机制是相对复杂的,好在IDEA提供的相关断点和Debug功能确实好用,使得多线程调试起来直观,通过这种方式能…...
【深度学习-Day 14】从零搭建你的第一个神经网络:多层感知器(MLP)详解
Langchain系列文章目录 01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南 02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖 03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南 04-玩转 LangChai…...
fdisk和parted的区别
在Linux系统中,fdisk和parted是两种常用的分区工具。虽然它们都可以对硬盘进行分区,但在功能和适用范围上有显著的区别。 fdisk fdisk主要用于MBR(主引导记录)分区表的管理。MBR分区表有以下特点: 支持小于2TB的硬盘…...
springMVC拦截器,拦截器拦截策略设置
目录 1、MyInterceptor1 2、UserController 3、MvcConfig,拦截器4种拦截方法策略 做请求的校验,如果校验没有通过,直接返回,原来下面的处理,就不用处理了 将request进行拦截校验 将response进行拦截校验 preHandle…...
如何测试北斗卫星通讯终端的性能?
测试北斗卫星通讯终端的性能需从功能、性能、环境适应性、可靠性等多维度展开,以下是具体测试内容与方法: 一、基础功能测试 验证终端是否满足北斗系统的核心通讯功能。 (1)通信模式测试 短报文通信 测试终端发送 / 接收短报…...
基于MakeReal3D的虚拟预装系统:飞机装配效率与精度的双重突破
在航空制造领域,飞机部件的对接装配是飞机制造过程中的关键环节。传统的部件装配方式高度依赖操作人员的经验和反复调整,调姿过程耗时较长,且难以保证每次装配都能达到最优状态。随着虚拟现实技术的成熟,虚拟装配技术作为一种新兴…...
IP54是什么?
IP54是什么 定义 IP54是一种国际标准,用来指示设备的防护等级,该标准由国际电工委员会(IEC)制定,并在许多领域广泛使用13。IP是Ingress Protection的缩写,IP等级是针对电气设备外壳对异物侵入的防护等级。…...
Python异步编程详解
Python异步编程详解 引言 异步编程是Python中处理并发操作的重要方式,它允许程序在等待I/O操作时执行其他任务,从而提高程序的整体效率。本文将详细介绍Python异步编程的概念、实现方式以及实际应用场景。 1. 异步编程基础 1.1 什么是异步编程&#x…...
AUC与Accuracy的区别
下面分别解释下这两句话的含义及其原因,并说明 AUC 与 Accuracy(准确率)的区别: AUC 是阈值无关的指标 • 含义:在二分类问题中,模型通常会输出一个概率值或打分,需要设定一个阈值来将这些概…...
差分数组:原理与应用
一、什么是差分数组 差分数组是一种高效处理区间更新操作的数据结构技巧,特别适用于需要对数组的某个区间进行频繁增减操作的场景。差分数组的核心思想是通过存储相邻元素的差值而非元素本身,将区间操作转化为端点操作,从而将时间复杂度从O(…...
一些C++入门基础
关键字 图引自 C 关键词 - cppreference.com 命名空间 命名空间解决了C没办法解决的各类命名冲突问题 C的标准命名空间:std 命名空间中可以定义变量、函数、类型: namespace CS {//变量char cs408[] "DS,OS,JW,JZ";int cs 408;//函数vo…...
免费插件集-illustrator插件-Ai插件-路径尖角圆角化
文章目录 1.介绍2.安装3.通过窗口>扩展>知了插件4.功能解释5.总结 1.介绍 本文介绍一款免费插件,加强illustrator使用人员工作效率,实现图形编辑中路径尖角圆角化。首先从下载网址下载这款插件https://download.csdn.net/download/m0_67316550/87…...
数据分析_商务运营考核指标体系搭建
以抖音电商中的小学教辅书籍业务为例,搭建对接达人的商务运营团队能力考核指标体系,涵盖达人筛选、合作管理、效果追踪和长期价值维护等核心环节,结合教育产品特性和商务运营目标,设计分层量化指标: 一、考核目标 围绕…...
基于Java的校运会管理系统【附源码】
湄洲湾职业技术学院 毕业设计(论文) 课题名称: 系 别: 专 业: 年 级: 姓 名: 学 号: 指导教师: 摘 要 用传统的方式来管理信息,一是耗时较长,二是…...
保证数据库 + redis在读写分离场景中事务的一致性
在 Spring Boot 中实现数据库与 Redis 的一致性,特别是处理读写分离时,确保数据修改的事务一致性是一个常见的挑战。因为 Redis 是一个内存数据库,通常用于缓存,而关系型数据库是持久化存储,两者之间的数据同步和一致性…...
【Redis】跳表结构
目录 1、背景2、跳表【1】底层结构【2】关键操作【3】redis使用跳表原因【4】特性 1、背景 redis中的跳表是一种有序数据结构,主要用于实现有序集合(zset)。跳表通过多级索引实现高效查找(平均O(logN)时间复杂度)&…...
Semaphore解决高并发场景下的有限资源的并发访问问题
在高并发编程的领域中,我们常常面临着对有限资源的激烈抢夺问题。而 Java 的 java.util.concurrent 包提供的 Semaphore ,为我们提供了精准控制对有限资源并发访问的强大能力。 一、Semaphore? Semaphore,直译为 “信号量”&#…...
医学影像辅助诊断系统开发教程-基于tensorflow实现
源码下载地址: https://download.csdn.net/download/shangjg03/90873910 1. 简介 医学影像辅助诊断系统是利用计算机视觉和深度学习技术,帮助医生分析医学影像(如X光、CT、MRI等)并提供诊断建议的系统。本教程将指导你开发一个基于深度学习的胸部X光肺炎检测系统。 2. 准备…...
手动导出Docker进行并自动执行脚本命令的操作方法
若你已在 Docker 镜像里手动封装好文件,想让容器启动时自动执行 start.sh 脚本,可按以下步骤操作将镜像导出,同时确保启动时能自动执行脚本。 1. 提交当前容器为新镜像 假设你是在某个运行中的容器里进行文件封装操作的,要先把这个容器的当前状态提交为一个新的 Docker 镜…...
Mysql 中的日期时间函数汇总
前言 在 MySQL 中,处理日期和时间是非常常见的需求,MySQL中内置了大量的日期和时间函数,能够灵活、方便地处理日期和时间数据,本节就简单介绍一下 MySQL中内置的日期和时间函数,以便更好地利用这些函数来处理日期和时间…...
RabbitMQ Topic RPC
Topics(通配符模式) Topics 和Routing模式的区别是: topics 模式使⽤的交换机类型为topic(Routing模式使⽤的交换机类型为direct)topic 类型的交换机在匹配规则上进⾏了扩展, Binding Key⽀持通配符匹配(direct类型的交换机路 由规则是BindingKey和RoutingKey完全匹配) 在top…...
Conda环境管理:确保Python项目精准复现
探讨如何使用 Conda 有效地管理项目依赖,确保你的 Python 环境可以被精确复制和轻松共享 为什么依赖管理如此重要? 在开始具体操作之前,我们先来理解一下为什么环境依赖管理至关重要: 可复现性 (Reproducibility):无…...
基于PyTorch的医学影像辅助诊断系统开发教程
本文源码地址: https://download.csdn.net/download/shangjg03/90873921 1. 简介 本教程将指导你使用PyTorch开发一个完整的医学影像辅助诊断系统,专注于胸部X光片的肺炎检测。我们将从环境搭建开始,逐步介绍数据处理、模型构建、训练、评估以及最终的系统部署。...
Vue3——Pinia
目录 什么是 Pinia? 为什么选择 Pinia? 基本使用 安装pinia 配置pinia 定义store 使用 持久化插件 什么是 Pinia? Pinia 是一个轻量级的状态管理库,专为 Vue 3 设计。它提供了类似 Vuex 的功能,但 API 更加简…...
Java中Collections工具类中常用方法详解
文章从工具类的概述、常用方法的作用、实现原理到使用注意事项,都进行了详细说明,供你参考。 Java中Collections工具类中常用方法详解 在Java开发中,集合是存储和处理数据的重要容器,而java.util.Collections工具类则提供了一组静…...
面经总目录——持续更新中
说明 本面经总结了校招时我面试各个公司的面试题目,每场面试后我都及时进行了总结,同时后期补充扩展了同类型的相近面试题,校招时从两个方向进行投递,视觉算法工程师和软件开发工程师(C方向),所…...