SpringBoot整合Spring Security
1 快速入门
- 在项目中直接引入Spring Security的依赖
<!--springSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 启动项目,访问接口
引入Security之前在浏览器可以直接访问
但引入了Security之后访问这个接口跳转到了一个登陆页面
引入Security之后访问系统所以接口都需要认证,没有登陆需要先登陆
这个页面默认的用户名为 user ,密码在控制台可以看到
输入错误的密码会弹出提示
输入控制台中的正确密码就可以访问到接口了
2 Spring Security快速入门认证流程原理
- 前端提交用户名、密码先到了UsernamePasswordAuthenticationFilter中
- 在UsernamePasswordAuthenticationFilter中将用户名密码封装为Authentication对象
- 调用ProviderManager中的authenticate()方法进行认证
- 在ProviderManager中调用AbstractUserDetailsAuthenticationProvider中的authenticate方法
- 在AbstractUserDetailsAuthenticationProvider中调用它的子类DaoAuthenticationProvider重写的retrieveUser方法
- 在DaoAuthenticationProvider中会去调用实现了UserDeatilsService接口的InMemoryUserDetailsManager中的loadUserByUsername方法
- InMemoryUserDetailsManager是在内存中查找
- 找到用户之后将其用户信息封装成UserDetails对象返回给DaoAuthenticationProvider
- DaoAuthenticationProvider再将UserDetails对象返回给AbstractUserDetailsAuthenticationProvider
- AbstractUserDetailsAuthenticationProvider中再校验UserDetails对象的四种是否可用状态
然后调用DaoAuthenticationProvider的additionalAuthenticationChecks方法将UserDetails对象中的密码与前台传递的密码进行比较
如果正确则将UserDetails对象中的信息设置到Authentication对象中返回
3 从数据库中查询用户
1 问题分析
我们自己的项目肯定是不能从内存中去查询用户信息的,需要从库表中查询用户
从上面的流程我们可以看到Security查询用户是调用实现了UserDetilsService接口的InMemoryUserDetailsManager中的loadUserByUsername方法查询根据用户名查询用户
那么我们就可以通过自己实现UserDetilsService接口,重写其中的loadUserByUsername方法,在方法中去库表中查询用户信息,然后返回
2 实现步骤
1 因为loadUserByUsername返回的是UserDetails类型对象,所以我们需要自定义一个对象实现UserDetails,将学生作为其中的属性
public class LoginUser implements UserDetails {
private Student student;
public LoginUser() {
}
public LoginUser(Student student) {
this.student = student;
}
/**
* 获取用户的权限信息,后边需要授权的时候,就要在这个类中定义一个属性来存储权限
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return student.getPassword();
}
@Override
public String getUsername() {
return student.getSname();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2 实现UserDetilsService接口注入容器
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
StudentMapper studentMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//从数据库中 根据username查询用户信息
Student stu = studentMapper.findById(Integer.parseInt(username));
//查询为空,则抛出异常
if (Objects.isNull(stu)){
throw new RuntimeException("用户不存在");
}
//封装成UserDetails对象返回
LoginUser loginUser = new LoginUser(stu);
return loginUser;
}
}
3 库表数据
注意:Security默认的PasswordEncoder要求库中密码前有类型标识,需要如果你想让用户的密码是明文存储,需要在密码前加{noop}
4 测试
访问接口
输入库中的用户名密码
debug一下看看查询到的用户
成功访问到
4 前端请求登陆
问题分析
Security默认的是将登陆页面中的用户名密码给到UsernamePasswordAuthenticatieFilter中,然后将用户名密码封装成Authentication对象调用ProviderManager的authenticate方法进行认证
那么我们的前后端分离项目中,前端要带着用户名密码请求我们后端自定义的登陆接口,我们就可以在自己的接口中将前端传过来的用户名密码封装成Authentication对象,然后我们再自己调用ProviderManager的authenticate方法进行认证
注意这里我们需要用到Security中的ProviderManager,所以需要在Security的配置类中配置一个ProviderManager的Bean
1 自定义一个登陆接口
@RestController
@RequestMapping("/system")
public class CommonController {
@Autowired
CommonService commonService;
@PostMapping("/login")
public String login(@RequestBody Student student){
//调用service进行登陆认证
return commonService.login(student);
}
}
@Service
public class CommonService {
@Autowired
AuthenticationManager authenticationManager;
public String login(Student student) {
//将请求的用户名密码封装成Authentication对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(student.getSid(),student.getPassword());
//调用providerManager的authenticate进行认证
Authentication authenticateStudent = authenticationManager.authenticate(authenticationToken);
//从认证结果中取出用户
LoginUser logStudent = (LoginUser) authenticateStudent.getPrincipal();
//TODO()自定义其它操作
return "登陆成功!";
}
}
2 Security配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.formLogin();//security默认登陆页面,前后端分离项目用不到
http
.csrf().disable()//cxrf为了防止跨站伪造请求攻击,认证时还会认证一个csrf_token。前后端分离项目是天然能防止的,所以必须关闭csrf,否则认证不了,
.authorizeRequests()//配置请求认证
.antMatchers("/system/login").anonymous() //允许登陆接口可以匿名访问
.anyRequest().authenticated(); //其它所有请求都要认证
}
/**
* Security默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
* 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
* 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
*AuthenticationManager 是接口,ProviderManager是其实现类
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
3 修改库中密码
PasswordEncoder使用BCryptPasswordEncoder的话,库表里的密码就得是经过BCryptPasswordEncoder加密的,所以我们要把库表中的密码使用BCryptPasswordEncoder加密后存进去
4 模拟前端调用
5 登陆认证失败返回
如果密码输入错误的
只会返回403状态码,访问失败
我们怎样使登陆认证失败也能返回我们期望的数据格式呢?
要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
1 自定义认证失败处理的实现类
@Component
public class AuthenticationFaliure implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//设置状态码
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
//设置编码格式
response.setCharacterEncoding("utf-8");
//返回结果
PrintWriter writer = response.getWriter();
writer.print(authException.getMessage());
}
}
2 配置给SpringSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public AuthenticationEntryPoint authenticationEntryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.formLogin();//security默认登陆页面,前后端分享项目用不到
http
.csrf().disable()//cxrf为了防止跨站伪造请求攻击,认证时还会认证一个csrf_token。前后端分离项目是天然能防止的,所以必须关闭csrf,否则认证不了,
.authorizeRequests()//配置请求认证
.antMatchers("/system/login").anonymous() //允许登陆接口可以匿名访问
.anyRequest().authenticated(); //其它所有请求都要认证
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint);
}
/**
* Security默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
* 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
* 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
3 输入错误的密码测试
这个提示是Security校验密码后抛出的
4 输入错误的用户测试
这是个我们在自定义UserDetailsService中根据用户名没有查到用户自己抛出的
或者我们也可以不实现Security的异常处理,自己用try-catch来捕获异常进行处理返回
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤