SpringBoot整合Spring Security

1 快速入门

  1. 在项目中直接引入Spring Security的依赖
<!--springSecurity-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 启动项目,访问接口

    引入Security之前在浏览器可以直接访问
    但引入了Security之后访问这个接口跳转到了一个登陆页面

    引入Security之后访问系统所以接口都需要认证,没有登陆需要先登陆
    这个页面默认的用户名为 user ,密码在控制台可以看到

    输入错误的密码会弹出提示

    输入控制台中的正确密码就可以访问到接口了

2 Spring Security快速入门认证流程原理

  1. 前端提交用户名、密码先到了UsernamePasswordAuthenticationFilter中
  2. 在UsernamePasswordAuthenticationFilter中将用户名密码封装为Authentication对象
  3. 调用ProviderManager中的authenticate()方法进行认证
  4. 在ProviderManager中调用AbstractUserDetailsAuthenticationProvider中的authenticate方法
  5. 在AbstractUserDetailsAuthenticationProvider中调用它的子类DaoAuthenticationProvider重写的retrieveUser方法
  6. 在DaoAuthenticationProvider中会去调用实现了UserDeatilsService接口的InMemoryUserDetailsManager中的loadUserByUsername方法
  7. InMemoryUserDetailsManager是在内存中查找
  8. 找到用户之后将其用户信息封装成UserDetails对象返回给DaoAuthenticationProvider
  9. DaoAuthenticationProvider再将UserDetails对象返回给AbstractUserDetailsAuthenticationProvider
  10. 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来捕获异常进行处理返回

posted @   程长新  阅读(425)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤
点击右上角即可分享
微信分享提示